如何将下划线字段在Golang结构体中的应用扩展为?
摘要:最近公司里的新人问了我一个问题:这段代码是啥意思。这个问题很普通也很常见,我还是个新人的时候也经常问,当然,现在我不是新人了但我也经常发出类似的提问。 代码是长这样的: type BussinessObject struct { _ [0]
最近公司里的新人问了我一个问题:这段代码是啥意思。这个问题很普通也很常见,我还是个新人的时候也经常问,当然,现在我不是新人了但我也经常发出类似的提问。
代码是长这样的:
type BussinessObject struct {
_ [0]func()
ID uint64
FieldA string
FieldB *int64
...
}
新人问我_ [0]func()是什么。不得不说这是个好问题,因为这样的代码第一眼看上去谁都会觉得很奇怪,这种叫没有名字只有一个下划线占位符的我们暂且叫做“下划线字段”,下划线字段会占用实际的空间但又不能被访问,使用这样一个字段有什么用呢?
今天我就来讲讲下划线字段在Golang中的实际应用,除了能回答上面新人的疑问,还能帮你了解一些开源项目中的golang惯用法。
使结构体不能被比较
默认情况下golang的结构体是可以进行相等和不等判断的,编译器会自动生成比较每个字段的值的代码。
这和其他语言是很不一样的,在c语言里想要比较两个结构体你需要自写比较函数或者借助memcmp等标准库接口,在c++/Java/python中则需要重载/重写指定的运算符或者方法,而在go里除了少数特殊情况之外这些工作都由编译器代劳了。
然而天下没有免费的午餐,让编译器代劳等价于失去对比较操作的控制权。
举个简单的例子,你有一个字段都是指针类型的结构体,这些结构体可以进行等值判断,判断的依据是指针指向的实际内容:
type A struct {
Name *string
Age int
}
这种结构体在JSON序列化和数据库操作中很常见,理想中的判断操作应该是先解引用Name,比较他们指向的字符串的值,然后再比较Age是否相同。
但编译器生成的是先比较Name存储的地址值而不是他们指向的字符串的具体内容,然后再比较Age。这样当你使用==来处理结构体的时候就会得到错误的结果:
func (a *A) Equal(b *A) bool {
if b == nil || a.Name == nil || b.Name == nil {
return false
}
return *a.Name == *b.Name && a.Age == b.Age
}
//go:noinline
func getString(s string) *string {
buff := strings.Builder{}
buff.WriteString(s)
result := buff.String()
return &result
}
func main() {
a := A{getString("test"), 100}
b := A{getString("test"), 100}
fmt.Println(a == b, (*A).Equal(&a, &b)) // false, true
}
函数getString模拟了序列化和反序列化时的场景:相同内容的字符串每次都是独立分配的,导致了他们的地址不同。从结果可以看到golang默认生成的比较是不正确。
更糟糕的是这个默认生成的行为无法禁止,会导致==的误用。
实际生产中还有另一种情况,编译器觉得结构体符合比较的规则,但逻辑上这种结构体的等值比较没有实际意义。显然放任编译器的默认行为没有任何好处。
这时候新人问的那行代码就发挥用处了,我们把那行代码加进结构体里:
type A struct {
_ [0]func()
Name *string
Age int
}
现在程序会报错了:invalid operation: a == b (struct containing [0]func() cannot be compared)。
这就是之前说的少数几种特殊情况:函数、切片、map是不能比较的,包含这些类型字段的结构体或者数组也不可以进行比较操作。
我们的下划线字段是一个元素为函数的数组。在Go中,数组可以进行等值比较,但函数不能,因此[0]func()类型的下划线
字段将无法参与比较。接着由于go语法的规定,只要有一个字段不能进行比较,那么整个结构体也不能,所以==不再能应用在结构体A上。
