本文csdn
背景 公司最近出了 golang 语言规范,大部分参考 uber 的 go 语言规范(原版 和翻译 ),以及官方的 Effective Go 。这里分享一下自己之前没注意的点,查漏补缺
主要内容包括:go 语言特性中 defer、Mutex、interface 和 channel 的使用注意点,高性能场景中 string 和 byte 数组的相互转换,以及协程池的使用
方法和函数 defer 和返回值 对应知识点为方法返回值是有名还是无名的时候,defer 的顺序的差异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainfunc deferWithAnonymous () int { ret := 1 defer func () { ret++ }() return ret } func deferWithNamed () (ret int ) { ret = 1 defer func () { ret++ }() return } func main () { println (deferWithAnonymous()) println (deferWithNamed()) }
defer 和返回值之间的关系: 设置函数返回值 -> 执行 defer -> 最终返回给调用方
关键在第一步,匿名返回值函数中,设置的返回值就是具体的值,而在有名返回值函数,设置的是返回值的引用(即 ret 的引用)
所以有名返回值函数的 defer 会影响最后的返回值
对 defer 的编译后字节码解析可以参考这篇文章
sync.Mutex 作为传参的时候,需要传指针,否则可能导致死锁 Mutex 的加锁和释放锁逻辑是通过内部的state和sema两个整数对象 控制的,直接拷贝 Mutex 只是复制了锁的状态,但和原来的锁并不是同一个,所以释放复制后的 Mutex 并不能解锁原来的 Mutex
一个复现这个问题的示例,是通过 pointer receiver 占锁,通过 value receiver 释放锁,由于 value receiver 会拷贝调用者对象,所以释放的锁对象和外面的不同,导致死锁
参考-Detect locks passed by value in Go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "sync" type T struct { lock sync.Mutex } func (t *T) Lock () { t.lock.Lock() } func (t T) Unlock () { t.lock.Unlock() } func main () { t := T{lock: sync.Mutex{}} t.Lock() t.Unlock() t.Lock() }
基本类型 interface 的判空 interface 表示 golang 的接口类型,它和其他语言的“基类”(如 Java 的 interface)相比,在空对象的判空和调用方法的表现上不太一样
示例代码: 思考以下代码会输出什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 type MyError struct { msg string } func (err *MyError) Error () string { return err.msg } func workWithBalance () bool { return true } func workTooHard () bool { return false } func getError (f func () bool ) error { var err *MyError if !f() { err = &MyError{ msg: "need relax" , } } return err } func main () { if err := getError(workTooHard); err != nil { println ("work too hard caused " + err.Error()) } if getError(workWithBalance) == nil { println ("work with balance" ) } }
以上代码对自定义错误 MyError 进行了判空,预期 getError(workWithBalance) 获取到的 error 为空,但结果却不为空(work with balance 不会打印)
那么为什么声明未赋值的 err 判空得到的是 false 呢?我们可以从 interface 的内部结构 iface、eface 可以了解到端倪
1 2 3 4 5 6 7 8 9 10 11 type eface struct { _type *_type data unsafe.Pointer } type iface struct { tab *itab data unsafe.Pointer }
其中,iface 包含了接口的类型、方法和数据 ,iface 的 tab 描述了接口的类型和方法,data 则指向实际的接口数据
itab 的结构如下:
1 2 3 4 5 6 7 type itab struct { inter *interfacetype _type *_type hash uint32 _ [4 ]byte fun [1 ]uintptr }
而 eface 的数据结构就简单很多了,只包含具体类型 _type 和数据指针 data ,不包含方法信息
不包含方法的 eface 对应 var i interface{} 这种对象声明,主要用于传参、序列化和泛型 场景
那么 go 是如何判断一个 interface 类型对象是否为空呢?需要两个条件:data 对应的值为空,且 _type 具体类型也为空
通过 getError(workWithBalance) 获取的 error,虽然没有被初始化,但它有具体实现类型(MyError)而不是原始接口类型(error),所以 err == nil 为 false
想要判断 interface 背后的对象的值确实为空,有两种办法:先强转成具体的类型指针再判断,或者是通过反射方法 reflact.ValueOf 获取到内部的值来判断
1 2 3 4 5 6 7 8 9 10 11 e := getError(workWithBalance) v := reflect.ValueOf(e) if e.(*MyError) == nil { println ("err is nil" ) } if v.Kind() == reflect.Pointer { if v.IsNil() { println ("err is nil" ) } }
扩展: 空接口对象,是否可以调用接口方法呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type MyError struct { msg string } func (err *MyError) Error () string { if err == nil { return "empty error" } return err.msg } func main () { var emptyErr *MyError println (emptyErr.Error()) }
结论是可以调用,这一点和其他语言很不同。一个有具体类型的空接口对象调用 pointer receiver 不会报空指针,但注意只是能调用,如果 pointer receiver 内部有获取对象属性的操作,还是会报空指针错误
参考-nil receiver in GoLang
参考-Calling a method on a nil struct pointer doesn’t panic. Why not?
nil channel 的使用场景 在公司规范中,说明“禁止对 nil 或已关闭的 channel 进行读写关闭操作”,这算是为数不多需要指正的一点:nil channel 在特定场景也是可以操作的
先了解一下各种特殊情况下使用 channel 会出现什么情况:
closed channel: 读不阻塞(会读完剩下的数据,之后返回零值)、写 panic、再次 close panic
nil channel: 读阻塞、写阻塞、close panic
对于 nil channel 读写都会阻塞的特性,有一个使用场景是 合并多个 channel 数据的时候,对于已经取完数据的 channel 可以置为空,这样在继续使用 select 的同时也不影响其他还有数据的 channel 的读取,参考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func merge (a, b <-chan int ) <-chan int { c := make (chan int ) go func () { defer close (c) for a != nil || b != nil { select { case v, ok := <-a: if !ok { fmt.Println("a is done" ) a = nil continue } c <- v case v, ok := <-b: if !ok { fmt.Println("b is done" ) b = nil continue } c <- v } } }() return c }
高性能场景 使用 sync.Pool 获取需要频繁申请的对象 比较典型的场景是在高并发的数据流读取和写入场景中,通过 pool 缓存 buffer,避免每次都申请新的 buffer 造成频繁内存资源申请
在框架层代码中会比较容易看到 pool 的使用,如 gin 用来缓存处理请求的 Context 对象,gorm 用来缓存序列化对象(SerializerInterface)等
性能测试结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func BenchmarkByteBufferWithoutPool (b *testing.B) { for i := 0 ; i < b.N; i++ { buf := bytes.Buffer{} buf.WriteString(longStr) io.Copy(io.Discard, &buf) } } func BenchmarkByteBufferWithPool (b *testing.B) { pool := sync.Pool{ New: func () any { return new (bytes.Buffer) }, } for i := 0 ; i < b.N; i++ { buf := pool.Get().(*bytes.Buffer) buf.WriteString(longStr) io.Copy(io.Discard, buf) buf.Reset() pool.Put(buf) } }
从执行次数和内存开销来看,pool 在多协程下达到的对象复用的效果,都能带来很大的提升
关于 sync.Pool 值得留意的还有在 1.13 之后的性能提升,可以参考这篇文章
bytes 和 string 的 0 内存申请方法 直接看无内存开销的转换方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func ByteSliceToString (bytes []byte ) string { var s string sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&bytes)) stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) stringHeader.Data = sliceHeader.Data stringHeader.Len = sliceHeader.Len return s } func StringToByteSlice (s string ) (bytes []byte ) { bh := (*reflect.SliceHeader)(unsafe.Pointer(&bytes)) sh := *(*reflect.StringHeader)(unsafe.Pointer(&s)) bh.Data = sh.Data bh.Len = sh.Len bh.Cap = sh.Len return }
参考
两者的相互转换都用到了反射包中表示底层结构的对象,如 slice 的 SliceHeader ,以及 string 的 StringHeader
string 和 byte 数组两者的底层数据结构非常相似,只是 slice 多了 cap,所以转换逻辑并不复杂
string 和 slice 的底层结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type stringStruct struct { str unsafe.Pointer len int } type slice struct { array unsafe.Pointer len int cap int } type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int }
go 1.20 之后,StringHeader 和 SliceHeader 被标注为 Deprecated,改为推荐使用 StringData 和 SliceData ,写法上更简单了
1 2 3 4 5 6 7 func byteSliceToString (bytes []byte ) string { return unsafe.String(unsafe.SliceData(bytes), len (bytes)) } func stringToByteSlice (s string ) (bytes []byte ) { return unsafe.Slice(unsafe.StringData(s), len (s)) }
实测: 直接强转和通过反射转换的benchmark测试结果对比
bytes 转 string
1 2 3 BenchmarkForceConvertBytesToString-8 66501550 178.7 ns/op 1024 B/op 1 allocs/op BenchmarkConvertBytesToString-8 1000000000 0.3236 ns/op 0 B/op 0 allocs/op
可以看到,强转的方式执行速度(平均每次 178ns)远小于通过反射方式执行的,并且强转每次需要申请 1kb 内存,刚好和转换的字符串大小对应
string 转 bytes
1 2 3 BenchmarkForceConvertStringToBytes-8 67139846 200.6 ns/op 1024 B/op 1 allocs/op BenchmarkConvertStringToBytes-8 1000000000 0.3230 ns/op 0 B/op 0 allocs/op
结果和 bytes 转 string 类似,不再赘述
高并发的任务(如接口)创建协程池去消费和执行 协程确实很”轻“,相比操作系统线程默认大小为1M 来说,它的初始大小只有 2k,确实很小(但随着栈空间扩大可能会扩缩容),不过在高并发场景下还是需要对开启协程进行控制的
协程池的选型有很多,常见的开源项目有 tunny 和 ants ,两者实现方式略有区别,tunny 提交任务时是同步提交,可以拿到执行后的返回值,ants 是异步提交,不支持获取返回值,要拿到返回值的话得自己实现。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import ( "github.com/Jeffail/tunny" "github.com/panjf2000/ants/v2" ) func TestTunnyPool (t *testing.T) { wg := sync.WaitGroup{} wg.Add(100 ) pool := tunny.NewFunc(10 , func (payload interface {}) interface {} { time.Sleep(3 * time.Second) wg.Done() return payload }) defer pool.Close() for i := 0 ; i < 100 ; i++ { go func (i int ) { pool.Process(i) }(i) } wg.Wait() } func TestAntsPool (t *testing.T) { wg := sync.WaitGroup{} wg.Add(100 ) pool, _ := ants.NewPoolWithFunc(10 , func (i interface {}) { fmt.Printf("%d execute\n" , i) time.Sleep(3 * time.Second) fmt.Printf("%d finish\n" , i) wg.Done() }) defer pool.Release() for i := 0 ; i < 100 ; i++ { pool.Invoke(i) } wg.Wait() }
当然,对于 web 框架来说,这种控制并发的功能官方都有。如 gin 通过 limit 插件,本质也是通过 channel 控制并发协程数