[Go]零大小的变量/空结构体及其对应的切片/数组类型中的元素可能具有相同的地址

陪她去流浪 桃子 2020年07月07日 阅读次数:1689

最近在修复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

最后

这样设计的最原始想法是什么?

这篇文章的内容已被作者标记为“过时”/“需要更新”/“不具参考意义”。

标签:C++ · Go