在 Go 中结构体可以比较吗?这其实是我曾经面试过的一个问题,我们来做一个实验:
定义如下结构体:go
复制代码type Normal struct {
a string
B int
}
使用这个结构体分别声明 3 个变量 n1
、n2
、n3
,然后进行比较:go
复制代码n1 := Normal{
a: "a",
B: 10,
}
n2 := Normal{
a: "a",
B: 10,
}
n3 := Normal{
a: "b",
B: 20,
}
fmt.Println(n1 == n2)
fmt.Println(n1 == n3)
执行示例代码,输出结果如下:bash
复制代码$ go run main.go
true
false
可见 Normal
结构体是可以比较的。
那么所有结构体都可以比较吗?显然不是,如果都可以比较,那么 reflect.DeepEqual()
就没有存在的必要了。
定义如下结构体:go
复制代码type NoCompare struct {
a string
B map[string]int
}
使用这个结构体分别声明 2 个变量 n1
、n2
,然后进行比较:go
复制代码n1 := NoCompare{
a: "a",
B: map[string]int{
"a": 10,
},
}
n2 := NoCompare{
a: "a",
B: map[string]int{
"a": 10,
},
}
fmt.Println(n1 == n2)
执行示例代码,输出结果如下:bash
复制代码$ go run main.go
./main.go:59:15: invalid operation: n1 == n2 (struct containing map[string]int cannot be compared)
这里程序直接报错了,并提示结构体包含了 map[string]int
类型字段,不可比较。
所以小结一下:
结构体是否可以比较,不取决于字段是否可导出,而是取决于其是否包含不可比较字段。
如果全部字段都是可比较的,那么这个结构体就是可比较的。
如果其中有一个字段不可比较,那么这个结构体就是不可比较的。
不过虽然我们不可以使用 ==
对 n1
、n2
进行比较,但我们可以使用 reflect.DeepEqual()
对二者进行比较:go
复制代码fmt.Println(reflect.DeepEqual(n1, n2))
执行示例代码,输出结果如下:bash
复制代码$ go run main.go
true
最近我在使用 Go 官方出品的结构化日志包 slog 时,看到 slog.Value
源码:go
复制代码// A Value can represent any Go value, but unlike type any,
// it can represent most small values without an allocation.
// The zero Value corresponds to nil.
type Value struct {
_ [0]func() // disallow ==
// num holds the value for Kinds Int64, Uint64, Float64, Bool and Duration,
// the string length for KindString, and nanoseconds since the epoch for KindTime.
num uint64
// If any is of type Kind, then the value is in num as described above.
// If any is of type *time.Location, then the Kind is Time and time.Time value
// can be constructed from the Unix nanos in num and the location (monotonic time
// is not preserved).
// If any is of type stringptr, then the Kind is String and the string value
// consists of the length in num and the pointer in any.
// Otherwise, the Kind is Any and any is the value.
// (This implies that Attrs cannot store values of type Kind, *time.Location
// or stringptr.)
any any
}
可以发现,这里有一个匿名字段 _ [0]func()
,并且注释写着 // disallow ==
。
_ [0]func()
的目的显然是为了禁止比较。
我们来实验一下,_ [0]func()
是否能够实现禁止结构体相等性比较:go
复制代码v1 := Value{
num: 1,
any: 2,
}
v2 := Value{
num: 1,
any: 2,
}
fmt.Println(v1 == v2)
执行示例代码,输出结果如下:bash
复制代码$ go run main.go
./main.go:109:15: invalid operation: v1 == v2 (struct containing [0]func() cannot be compared)
可以发现,的确有效。因为 func()
是一个函数,而函数在 Go 中是不可比较的。
既然使用 map[string]int
和 _ [0]func()
都能实现禁止结构体相等性比较,那么我为什么说 _ [0]func()
是更优雅的做法呢?
_ [0]func()
有着比其他实现方式更优的特点:
它不占内存空间!
使用匿名字段 _
语义也更强。
而且,我们直接去 Go 源码里搜索,能够发现其实 Go 本身也在多处使用了这种用法:
所以推荐使用 _ [0]func()
来实现禁用结构体相等性比较。