golang summary
map
map是一个key,value形式的hash表,从而将key,value进行一一映射
golang中的map并不是并发安全的
golang中sync.Map是并发安全的
数组
- 数组变量即表示整个数组,是一个完整的值。当i个数组变量被赋值 或者被传递的时候,实际上会复制整个数组。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组
- 数组的定义方式:
var a [3]int // 定义长度为3的int型数组, 元素全部为0 var b = [...]int{1, 2, 3} // 定义长度为3的int型数组, 元素为 1, 2, 3 var c = [...]int{2: 3, 1: 2} // 定义长度为3的int型数组, 元素为 0, 2, 3 var d = [...]int{1, 2, 4: 5, 6} // 定义长度为6的int型数组, 元素为 1, 2, 0, 0, 5, 6
字符串
- Go字符串底层结构
reflect.StringHeader
:
字符串结构有两个信息构成:第一个是字符串指向的底层字节数组,第二个是字符串的字节长度。字符串 其实是一个结构体。因此字符串的赋值操作也就是reflect.StringHeader结构体的复制过程,并不会涉 及底层字节数组的复制type StringHeader struct { Data uintptr Len int }
- 字符串虽然不是切片,但是支持切片操作,因为字符串是只读的,相同的字符串面值常量通常对应同一个字符串常量
切片
-
Go切片的底层结构,
reflect.SliceHeader
:type SliceHeader struct { Data uintptr Len int Cap int }
-
切片的定义方式
var ( a []int // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片 b = []int{} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合 c = []int{1, 2, 3} // 有3个元素的切片, len和cap都为3 d = c[:2] // 有2个元素的切片, len为2, cap为3 e = c[0:2:cap(c)] // 有2个元素的切片, len为2, cap为3 f = c[:0] // 有0个元素的切片, len为0, cap为3 g = make([]int, 3) // 有3个元素的切片, len和cap都为3 h = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3 i = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3 )
-
添加切片元素
内置的泛型函数append可以在切片的尾部追加N个元素:
var a []int a = append(a, 1) // 追加1个元素 a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式 a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
不过要注意的是,在容量不足的情况下,append的操作会导致重新分配内存,可能导致巨大的内存分配和 复制数据代价。即使容量足够,依然需要用append函数的返回值来更新切片本身,因为新切片的长度已经 发生了变化。
除了在切片的尾部追加,我们还可以在切片的开头添加元素:
var a = []int{1,2,3} a = append([]int{0}, a...) // 在开头添加1个元素 a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元 素的性能一般要比从尾部追加元素的性能差很多。
由于append函数返回新的切片,也就是它支持链式操作。我们可以将多个append操作组合起来,实现在 切片中间插入元素:
var a []int a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
每个添加操作中的第二个append调用都会创建一个临时切片,并将a[i:]的内容复制到新创建的切片中,
然后将临时创建的切片再追加到a[:i]。可以用copy和append组合可以避免创建中间的临时切片,同样是完成添加元素的操作:
a = append(a, 0) // 切片扩展1个空间 copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置 a[i] = x // 设置新添加的元素
第一句append用于扩展切片的长度,为要插入的元素留出空间。第二句copy操作将要插入位置开始之后 的元素向后挪动一个位置。第三句真实地将新添加的元素赋值到对应的位置。操作语句虽然冗长了一点, 但是相比前面的方法,可以减少中间创建的临时切片。
用copy和append组合也可以实现在中间位置插入多个元素(也就是插入一个切片):
a = append(a, x...) // 为x切片扩展足够的空间 copy(a[i+len(x):], a[i:]) // a[i:]向后移动len(x)个位置 copy(a[i:], x) // 复制新添加的切片
稍显不足的是,在第一句扩展切片容量的时候,扩展空间部分的元素复制是没有必要的。没有专门的内置 函数用于扩展切片的容量,append本质是用于追加元素而不是扩展容量,扩展切片容量只是append的一 个副作用。
-
删除切片元素
根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。其中删除切片尾部 的元素最快:
a = []int{1, 2, 3} a = a[:len(a)-1] // 删除尾部1个元素 a = a[:len(a)-N] // 删除尾部N个元素
删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3} a = a[1:] // 删除开头1个元素 a = a[N:] // 删除开头N个元素
删除开头的元素也可以不移动数据指针,但是将后面的数据向开头移动。可以用append原地完成(所谓原 地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):
a = []int{1, 2, 3} a = append(a[:0], a[1:]...) // 删除开头1个元素 a = append(a[:0], a[N:]...) // 删除开头N个元素
假设切片里存放的是指针对象,那么下面删除末尾的元素后,被删除的元素依然被切片底层数组引用,
从而导致不能及时被自动垃圾回收器回收(这要依赖回收器的实现方式):var a []*int{ ... } a = a[:len(a)-1] // 被删除的最后一个元素依然被引用, 可能导致GC操作被阻碍
保险的方式是先将需要自动内存回收的元素设置为nil,保证自动回收器可以发现需要回收的对象,然后再进行切片的删除操作:
var a []*int{ ... } a[len(a)-1] = nil // GC回收最后一个元素内存 a = a[:len(a)-1] // 从切片删除最后一个元素
当然,如果切片存在的周期很短的话,可以不用刻意处理这个问题。因为如果切片本身已经可以被GC回收的话,切片对应的每个元素自然也就是可以被回收的了。
也可以用copy完成删除开头的元素:
a = []int{1, 2, 3} a = a[:copy(a, a[1:])] // 删除开头1个元素 a = a[:copy(a, a[N:])] // 删除开头N个元素
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成:
a = []int{1, 2, 3, ...} a = append(a[:i], a[i+1:]...) // 删除中间1个元素 a = append(a[:i], a[i+N:]...) // 删除中间N个元素 a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素 a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况。
函数
-
当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果:
func main() { var a = []interface{}{123, "abc"} Print(a...) // [123 abc] Print(a) // [[123 abc]] } func Print(a ...interface{}) { fmt.Println(a...) }
第一个Print调用时传入的参数是a...,等价于直接调用Print(123, "abc")。第二个Print调用传入的 是未解包的a,等价于直接调用Print([]interface{}{123, "abc"})
-
defer语句延迟执行了一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问
闭包的这种引用方式访问外部变量的行为可能会导致一些隐含的问题:
func main() { for i := 0; i < 3; i++ { defer func(){ println(i) } () } } // Output: // 3 // 3 // 3
因为是闭包,在for迭代语句中,每个defer语句延迟执行的函数引用的都是同一个i迭代变量,在循环结束后这个变量的值为3,因此最终输出的都是3。
修复的思路是在每轮迭代中为每个defer函数生成独有的变量。可以用下面两种方式:
func main() { for i := 0; i < 3; i++ { i := i // 定义一个循环体内局部变量i defer func(){ println(i) } () //因为是引用,所以这里的局部变量i每次循环地址均不同 } } func main() { for i := 0; i < 3; i++ { // 通过函数传入i // defer 语句会马上对调用参数求值 defer func(i int){ println(i) } (i) } }
方法
- Go语言中,通过在结构体内置匿名的成员来实现继承:
type Cache struct {
m map[string]string
sync.Mutex
}
func (p *Cache) Lookup(key string) string {
p.Lock()
defer p.Unlock()
return p.m[key]
}
Cache结构体类型通过嵌入一个匿名的sync.Mutex来继承它的Lock和Unlock方法. 但是在调用 p.Lock()和p.Unlock()时, p并不是Lock和Unlock方法的真正接收者, 而是会将它们展开为 p.Mutex.Lock()和p.Mutex.Unlock()调用. 这种展开是编译期完成的, 并没有运行时代价.
接口
- 有时候对象和接口之间太灵活了,导致我们需要人为地限制这种无意之间的适配
- 常见的做法是定义一个含特殊方法来区分接口。
- 再严格一点的做法是给接口定义一个私有方法。只有满足了这个私有方法的对象才可能满足这个接口,而私有方法的名字是包含包的绝对路径名的,因此只能在包内部实现这个私有方法才能满足这个接口。
type testing.TB interface { Error(args ...interface{}) Errorf(format string, args ...interface{}) ... // A private method to prevent users implementing the // interface and so future additions to it will not // violate Go 1 compatibility. private() }
不过这种通过私有方法禁止外部对象实现接口的做法也是有代价的:首先是这个接口只能包内部使用,外部包正常情况下是无法直接创建满足该接口对象的;其次,这种防护措施也不是绝对的,恶意的用户依然可以绕过这种保护机制,如下
package main
import (
"fmt"
"testing"
)
type TB struct {
testing.TB //隐式转换了TB接口为testing.TB
}
func (p *TB) Fatal(args ...interface{}) {
fmt.Println("TB.Fatal disabled!")
}
func main() {
var tb testing.TB = new(TB)
tb.Fatal("Hello, playground")
}
这种通过嵌入匿名接口或嵌入匿名指针对象来实现继承的做法其实是一种纯虚继承,我们继承的只是接口指定的规范,真正的实现在运行的时候才被注入。
并发
-
在Go语言中,同一个Goroutine线程内部,顺序一致性内存模型是得到保证的。但是不同的Goroutine之间,并不满足顺序一致性内存模型,需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序,那么就说这两个事件是并发的。为了最大化并行,Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序(CPU也会对一些指令进行乱序执行)。
package main import ( "fmt" ) var d string var b bool func setup() { d = "hello world" fmt.Println(d) b = true } func main() { done := make(chan int) go func() { setup() done <- 1 //传入信号说明setup执行完毕 }() <- done //接收到信号说明b的值已经刷新完毕,主进程能正确监听到b的值 if !b {} print(d) }
var d string var b bool func setup() { d = "hello world" fmt.Println(d) b = true } func main() { var wg sync.WaitGroup wg.Add(1) //1表示1次done之后关闭wait go func() { setup() wg.Done() }() wg.Wait() //能否接受到数据 if !b {} print(d) }
var limit = make(chan int, 3) var work =[]func()time.Time {time.Now} func main() { for _, w := range work { go func(w func()time.Time) { limit <- 1 w() }(w) // 传入w以防只取for循环最后一个w } for{ // 死循环判断limit是否有信号 select { case <-limit: // 接收到信号进入case fmt.Println("done") return default: fmt.Println("not done") } } }
并发模型
- 生产者消费者模型
package main
import (
"fmt"
"time"
)
func main() {
var ch = make(chan int,100)
go producer(1,ch) // 生成1的倍数序列
go producer(2,ch) // 生成2的倍数序列
go consumer(ch) //消费生成的队列
time.Sleep(time.Millisecond)
}
func producer(factor int, out chan<- int) {
for i := 0; ; i++ {
out <- i * factor
}
}
func consumer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
- 发布订阅模型
发布者
// Package pubsub implements a simple multi-topic pub-sub library.
package pubsub
import (
"sync"
"time"
)
type (
subscriber chan interface{} // 订阅者为一个管道
topicFunc func(v interface{}) bool // 主题为一个过滤器
)
// 发布者对象
type Publisher struct {
m sync.RWMutex // 读写锁
buffer int // 订阅队列的缓存大小
timeout time.Duration // 发布超时时间
subscribers map[subscriber]topicFunc // 订阅者信息
}
// 构建一个发布者对象, 可以设置发布超时时间和缓存队列的长度
func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
return &Publisher{
buffer: buffer,
timeout: publishTimeout,
subscribers: make(map[subscriber]topicFunc),
}
}
// 添加一个新的订阅者,订阅全部主题
func (p *Publisher) Subscribe() chan interface{} {
return p.SubscribeTopic(nil)
}
// 添加一个新的订阅者,订阅过滤器筛选后的主题
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
ch := make(chan interface{}, p.buffer)
p.m.Lock()
p.subscribers[ch] = topic
p.m.Unlock()
return ch
}
// 退出订阅
func (p *Publisher) Evict(sub chan interface{}) {
p.m.Lock()
defer p.m.Unlock()
delete(p.subscribers, sub)
close(sub)
}
// 发布一个主题
func (p *Publisher) Publish(v interface{}) {
p.m.RLock()
defer p.m.RUnlock()
var wg sync.WaitGroup
for sub, topic := range p.subscribers {
wg.Add(1)
go p.sendTopic(sub, topic, v, &wg) // wg不是引用类型,一定要传指针
}
wg.Wait()
}
// 关闭发布者对象,同时关闭所有的订阅者管道。
func (p *Publisher) Close() {
p.m.Lock()
defer p.m.Unlock()
for sub := range p.subscribers {
delete(p.subscribers, sub)
close(sub)
}
}
// 发送主题,可以容忍一定的超时
func (p *Publisher) sendTopic(
sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup,
) {
defer wg.Done()
if topic != nil && !topic(v) { //如果topic是空,会将所有v 传到sub里面去,如果不为空并且topicfunc将v传入后的返回值为true时才会把v传到sub
return
}
select {
case sub <- v:
case <-time.After(p.timeout):
}
}
订阅者:
package main
import (
"demo/demo2/pub"
"fmt"
"strings"
"time"
)
func main() {
pub := pubsub.NewPublisher(100*time.Millisecond,3)
defer pub.Close()
all := pub.Subscribe()
golang := pub.SubscribeTopic(func(v interface{}) bool{
if s,ok := v.(string);ok {
return strings.Contains(s,"golang")
}
return false
})
pub.Publish("hello world")
pub.Publish("hello tanxin")
pub.Publish("hello golang")
a:= <-all
b:= <- golang
fmt.Println(a)
fmt.Println(b)
go func() {
for msg := range all{
fmt.Println("all:",msg)
}
}()
go func() {
for msg := range golang{
fmt.Println("golang:",msg)
}
}()
time.Sleep(1* time.Second)
}
并发的安全退出
- 使用select安全退出
package main
import (
"fmt"
"sync"
"time"
)
func worker(wg *sync.WaitGroup, cannel chan bool) {
defer wg.Done()
for {
select {
default:
fmt.Println("hello")
case <-cannel:
return
}
}
}
func main() {
cancel := make(chan bool)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(&wg, cancel)
}
time.Sleep(time.Second)
close(cancel)
wg.Wait()
}
- 使用context包来安全退出
在Go1.7发布时,标准库增加了一个context包,用来简化对于处理单个请求的多个Goroutine之间与请求域的数据、超时和退出等操作
func worker(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
for {
select {
default:
fmt.Println("hello")
case <-ctx.Done():
return ctx.Err()
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(ctx, &wg)
}
time.Sleep(time.Second)
cancel()
wg.Wait()
}
异常
Go语言函数调用的正常流程是函数执行返回语句返回结果,在这个流程中是没有异常的,因此在这个流程中执行recover异常捕获函数始终是返回nil。另一种是异常流程: 当函数调用panic抛出异常,函数将停止执行后续的普通语句,但是之前注册的defer函数调用仍然保证会被正常执行,然后再返回到调用者。对于当前函数的调用者,因为处理异常状态还没有被捕获,和直接调用panic函数的行为类似。在异常发生时,如果在defer中执行recover调用,它可以捕获触发panic时的参数,并且恢复到正常的执行流程。
func main() {
if r := recover(); r != nil {
log.Fatal(r)
}
panic(123)
if r := recover(); r != nil {
log.Fatal(r)
}
}
上面程序中两个recover调用都不能捕获任何异常。在第一个recover调用执行时,函数必然是在正常的非异常执行流程中,这时候recover调用将返回nil。发生异常时,第二个recover调用将没有机会被执行到,因为panic调用会导致函数马上执行已经注册defer的函数后返回。
其实recover函数调用有着更严格的要求:我们必须在defer函数中直接调用recover。如果defer中调用的是recover函数的包装函数的话,异常的捕获工作将失败!
在嵌套的defer函数中调用recover也将导致无法捕获异常:
func main() {
defer func() {
defer func() {
// 无法捕获异常
if r := recover(); r != nil {
fmt.Println(r)
}
}()
}()
panic(1)
}
2层嵌套的defer函数中直接调用recover和1层defer函数中调用包装的MyRecover函数一样,都是经过了2个函数帧才到达真正的recover函数,这个时候Goroutine的对应上一级栈帧中已经没有异常信息。
直接在defer语句中调用MyRecover函数又可以正常工作了:
func MyRecover() interface{} {
return recover()
}
func main() {
// 可以正常捕获异常
defer MyRecover()
panic(1)
}
但是,如果defer语句直接调用recover函数,依然不能正常捕获异常:
func main() {
// 无法捕获异常
defer recover()
panic(1)
}
必须要和有异常的栈帧只隔一个栈帧,recover函数才能正常捕获异常。换言之,recover函数捕获的是祖父一级调用函数栈帧的异常(刚好可以跨越一层defer函数)!
当希望将捕获到的异常转为错误时,如果希望忠实返回原始的信息,需要针对不同的类型分别处理:
func foo() (err error) {
defer func() {
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = fmt.Errorf("Unknown panic: %v", r)
}
}
}()
panic("TODO")
}
数据类型
golang中分为值类型和引用类型:
-
值类型分别有:int系列、float系列、bool、string、数组和结构体
-
引用类型有:指针、slice切片、管道channel、接口interface、map、函数等
值类型的特点是:变量直接存储值,内存通常在栈中分配
引用类型的特点是:变量存储的是一个地址,这个地址对应的空间里才是真正存储的值,内存通常在堆中分配
go mod
- 开启go mod
go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,https://goproxy.io,direct //1.13 后
- 初始化moudle
go mod init 模块名
- 下载modules到本地cache
go mod download //目前所有模块版本数据均缓存在 $GOPATH/pkg/mod和 $GOPATH/pkg/sum 下
- 编辑go modules
go mod edit // -json、-require和-exclude
- 以文本模式打印模块需求图
go mod graph
- 删除错误或者不使用的modules
go mod graph
- 验证依赖是否正确
go mod verify
- 查找依赖
go mod why
持续学习.....