最近在修复[grpc-gateway 的一个 bug][grpc-gateway-bug]时,发现原作者写的测试用例根本就是无效的(总是通过)。 原因却是因为其代码“触碰”到了 Golang 中关于**取地址**的一个有点匪夷所思的“特性”。 [grpc-gateway-bug]: https://github.com/grpc-ecosystem/grpc-gateway/issues/1501 ## 问题描述 作者定义了一个具有 3 个**空结构体**的切片(数组),然后对这 3 个元素分别取地址,期望得到**不同**的地址,但是结果却是不行的。 我把[原问题][grpc-gateway-bug]简化并抽象出来了([Go Playground](https://play.golang.org/p/HPwRXhccsU9)): ```go package main import ( "fmt" ) type S struct { } type T struct { b bool } func main() { var s [3]S var t [3]T fmt.Printf("%p,%p,%p\n", &s[0], &s[1], &s[2]) fmt.Printf("%p,%p,%p\n", &t[0], &t[1], &t[2]) } ``` 输出结果: ```- 0x58fd18,0x58fd18,0x58fd18 0xc00009400b,0xc00009400c,0xc00009400d ``` 具体的值不重要,重要的是:前三者相同,后三者不同。 ## 规范是怎样的? 到了这里,我其实也是非常疑惑的。 如果仅是几个普通的结构体变量,其地址相同的话,那还可以接受。 居然,居然切片/数组中的不同元素的地址也相同?简直太不可思议了。 后来我查了[语言规范(位于规范的最后一行)][golang-spec]中关于大小和对齐的章节, 原文如是说: > A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. > > 一个不包含大小大于零的字段的结构体或数组的大小是零。 > > Two distinct zero-size variables may have the same address in memory. > > 两个零大小的不同的变量在内存中可能有相同的地址。 [golang-spec]: https://golang.org/ref/spec#Size_and_alignment_guarantees 看起来像那么回事,但是没有直接说明位于切片/数组中的元素也具体和变量类似的相同的地址。 关于这一点,Golang 在语言标准/实现上似乎跟其它语言(比如 C++)不同, C++ 明确规定: - 空结构体的大小至少是 1 - `new` 出来的两个新对象具有不同的地址 几年前,我在文章「[一个空类/结构体的大小是多少?](/687/)」中也说明过这一点。 ## 有人利用这个特性吗? 还真有。 比如下面这个简单到无可挑剔的包 [bradfitz/iter](https://github.com/bradfitz/iter/blob/e8f45d346db8021e0dd53899bf55eb6e21218b33/iter.go#L15-L30): ```go package iter // N returns a slice of n 0-sized elements, suitable for ranging over. // // For example: // // for i := range iter.N(10) { // fmt.Println(i) // } // // ... will print 0 to 9, inclusive. // // It does not cause any allocations. func N(n int) []struct{} { return make([]struct{}, n) } ``` 作者已经在注释中清楚地说明了此函数的作用及用法,以及为什么可以这样写。 注意最后那一句注释:**无任何内存分配**。 感觉这里可以作为一个面试考点?哈哈哈哈。 有没有人用这个包?有,还不少! ```go func (me *tokenServer) ValidToken(token string, addr Addr) bool { t := me.getTimeNow() for range iter.N(me.maxIntervalDelta + 1) { if me.createToken(addr, t) == token { return true } t = t.Add(-me.interval) } return false } ``` 来自于:[github.com/anacrolix/dht/tokens.go#L41-L50](https://github.com/anacrolix/dht/blob/31f2edf965e68ab644fe08d049318ad56b373dda/tokens.go#L41-L50)。 ## 最后 这样设计的最原始想法是什么?