Gopher 需要知道的几个结构体骚操作
我们知道 Go 没有继承的概念,接口结构体多使用组合,很多开源产品或是源代码都有大量的内嵌 (embeded field) 字段,用于特殊目的。本次分享的内容来自 grpc 与 go 源码
NoCopy
1 | package main |
这是非常经典的 case, 程序执行报错 all goroutines are asleep - deadlock!
, 解决也很简单,把 wg
由值传递变成指针类型即可。本质是 WaitGroup
内部维护了计数,不允许 copy 变量,还有 sync.Mutex
锁也是不允许 copy 的
解决办法很简单,需要 CI 时由 linter 检测出来,最好运行时也能有检测机制,这方面的讨论请参考issue 8005
1 | zerun.dong$ go vet aaa.go |
这是 go vet 结果,报错己经很明显了
1 | type noCopy struct{} |
noCopy
定义非常简单,空结构体,zero size 不占用空间(前提是非结构体的最后一个字段,否则还要是有 8 byte 空间开销)
sync.WaitGroup 内嵌 noCopy
字段,防止 Cond
变量被复制
1 | type WaitGroup struct { |
上面是 sync.WaitGroup
结构体的定义,同时注意 noCopy
是源码中不可导出的定义。如果用户代码也想实现 NoCopy 呢?可以参考 grpc DoNotCopy
1 | // DoNotCopy can be embedded in a struct to help prevent shallow copies. |
非常简单,Mutex
零长数组,不占用空间。由于 vet checker 会检测 Mutex
,相当于替我们实现了 noCopy
功能
DoNotCompare
Golang Sepc Comparison_operators 官方文档描述常见类型比较运算( == != > < <= >=)的结果,详细内容看官方文档 https://go.dev/ref/spec#Comparison_operators
In any comparison, the first operand must be assignable to the type of the second operand, or vice versa.
The equality operators == and != apply to operands that are comparable. The ordering operators <, <=, >, and >= apply to operands that are ordered.
Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.
Slice, map, and function values are not comparable. However, as a special case, a slice, map, or function value may be compared to the predeclared identifier nil. Comparison of pointer, channel, and interface values to nil is also allowed and follows from the general rules above.
对于 struct 来讲,只有所有字段全部 comparable 的(不限大小写是否导出),那么结构体才可以比较。同时只比较 non-blank 的字段,举个例子:
1 | type T struct { |
运行后,结果均为 true
Slice, Map, Function 均是不可比较的,只与判断是否为 nil. 所以我们可以利用这两个特性,内嵌函数来实现不可比较,参考 protobuf DoNotCompare
1 | // DoNotCompare can be embedded in a struct to prevent comparability. |
如果比较会报错
1 | type DoNotCompare [0]func() |
NoUnkeyedLiterals
结构体初始化有两种:指定字段名称,或者按顺序列出所有字段,不指定名称
1 | type User struct{ |
这样写的问题非常大,如果新增字段会不兼容
1 | type User struct{ |
上面的例子,能在编译期报错还是可接受的,如果同类型的调换顺序,那才叫坑爹… 所以这时需要 NoUnkeyedLiterals
1 | // NoUnkeyedLiterals can be embedded in a struct to prevent unkeyed literals. |
很简单,就是一个空结构体,这是 Protobuf 的实现。很多时候我们都用空的结构体占位符实现
1 | type User struct{ |
报错很明显了,字段类型不匹配,有人会说初始化写上 struct{}
不就可以了?
1 | _ = &User{struct{}{}, 21, "beijing"} |
这样确实可以工作,但是占位符 _
的字段是不可导出的,所以 import 其它包的 NoUnkeyedLiterals
结构体同样会报错
Copier 库
最后推荐一个非常实用的 copier 库,CRUD Boy 经常结构体转来转去的,比如 dto, dao 互转,或是 dao 与其它互转,如果修改了 dao 结构体,还要记得修改其它转换逻辑,非常繁琐
1 | package main |
打印 Employee
发现 name, age 字段己经赋值了,非常好用。感兴趣的可以查看官网,支持非常多的高级玩法
注意:这里是隐式的,有人觉得所有字段都要显示赋值,大家怎么看?
小结
写文章不容易,如果对大家有所帮助和启发,请大家帮忙点击在看
,点赞
,分享
三连
关于 struct 骚操作
大家有什么看法,欢迎留言一起讨论,大牛多留言 ^_^