[Go]零大小的变量/空结构体及其对应的切片/数组类型中的元素可能具有相同的地址
最近在修复grpc-gateway 的一个 bug时,发现原作者写的测试用例根本就是无效的(总是通过)。 原因却是因为其代码“触碰”到了 Golang 中关于取地址的一个有点匪夷所思的“特性”。
问题描述
作者定义了一个具有 3 个空结构体的切片(数组),然后对这 3 个元素分别取地址,期望得到不同的地址,但是结果却是不行的。 我把原问题简化并抽象出来了(Go Playground):
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
具体的值不重要,重要的是:前三者相同,后三者不同。
规范是怎样的?
到了这里,我其实也是非常疑惑的。 如果仅是几个普通的结构体变量,其地址相同的话,那还可以接受。 居然,居然切片/数组中的不同元素的地址也相同?简直太不可思议了。
后来我查了语言规范(位于规范的最后一行)中关于大小和对齐的章节, 原文如是说:
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 在语言标准/实现上似乎跟其它语言(比如 C++)不同, C++ 明确规定:
- 空结构体的大小至少是 1
new
出来的两个新对象具有不同的地址
几年前,我在文章「一个空类/结构体的大小是多少?」中也说明过这一点。
有人利用这个特性吗?
还真有。
比如下面这个简单到无可挑剔的包 bradfitz/iter:
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)
}
作者已经在注释中清楚地说明了此函数的作用及用法,以及为什么可以这样写。 注意最后那一句注释:无任何内存分配。 感觉这里可以作为一个面试考点?哈哈哈哈。
有没有人用这个包?有,还不少!
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。
最后
这样设计的最原始想法是什么?