https://mp.weixin.qq.com/s/lZMZroOhqvDKDCEsaGPfrQ

  1. type User struct {
  2. }
  3. func FPrint(u User) {
  4. fmt.Printf("main: %p\n", &u)
  5. }
  6. func main() {
  7. u := User{}
  8. FPrint(u)
  9. fmt.Printf("main: %p\n", &u)
  10. }
  11. //main: 0x13ada90
  12. //main: 0x13ada90

看了运行结果,大多数朋友应该和我一样,一脸懵逼?Go语言不是只有值传递嘛?之前我还写过一篇关于”Go语言参数传递是传值还是传引用吗?“,已经得出明确的结论,Go语言的确是只有值传递,这不是打脸了嘛。。。
既然已经出现了这样的结果,那么就要给出一个合理的解释,不要再让气氛尴尬下去,于是我给出了我的猜想,如下:

  • 猜想一:这是一个bug
  • 猜想二:结构体的特殊特性导致的

猜想一有点天马行空的感觉,暂时也无法验证,所以我们先来验证猜想二,请开始我的表演,都坐下,我要装逼了。

验证猜想二:结构体的特殊特性导致的

上面的那道题中传参是一个空结构体,如果改成一个带字段的结构体会是什么样呢?我们来看一下:

  1. type UserIsEmpty struct {
  2. }
  3. type UserHasField struct {
  4. Age uint64 `json:"age"`
  5. }
  6. func FPrint(uIsEmpty UserIsEmpty, uHasField UserHasField) {
  7. fmt.Printf("FPrint uIsEmpty:%p uHasField:%p\n", &uIsEmpty, &uHasField)
  8. }
  9. func main() {
  10. uIsEmpty := UserIsEmpty{}
  11. uHasField := UserHasField{
  12. Age: 10,
  13. }
  14. FPrint(uIsEmpty, uHasField)
  15. fmt.Printf("main: uIsEmpty:%p uHasField:%p\n", &uIsEmpty, &uHasField)
  16. }
  17. // 运行结果:
  18. //FPrint uIsEmpty:0x118fff0 uHasField:0xc0000ba008
  19. //main: uIsEmpty:0x118fff0 uHasField:0xc0000ba000

从结果我们可以看出来,带字段的结构体确实是值传递,那么就证明空结构体有猫腻,有进展了,带着这个线索,我们来看一看这段代码的汇编部分,执行go tool compile -N -l -S test.go,可以得到汇编部分,截取重要部分:
image.png

从结果上我们看到有调用runtime.newobject(SB)来进行分配内存,顺着这个在runtme/malloc.go中找到了他的实现:

  1. func newobject(typ *_type) unsafe.Pointer {
  2. return mallocgc(typ.size, typ, true)
  3. }

newobject()中主要是调用了mallocgc()方法,在这里我找到了答案。因为mallocgc()代码比较长,这里我截取关键部分:

  1. func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
  2. if gcphase == _GCmarktermination {
  3. throw("mallocgc called with gcphase == _GCmarktermination")
  4. }
  5. if size == 0 {
  6. return unsafe.Pointer(&zerobase)
  7. }
  8. //....
  9. }

如果 size0 的时候,统一返回的都是全局变量 zerobase 的地址。到这里可能还会有一些伙伴有疑惑,这个跟上面的题有什么关系?那是因为你还不知道一个知识点:正常struct是占用一小块内存的,并且结构体的大小是要经过边界,长度的对齐的,但是“空结构体”是不占内存的size0。现在一切都可以说的清了,总结原因:

因为空结构体是不占用内存的,所以size为0,在内存分配时,size0会统一返回zerobase的地址,所以空结构体在进行参数传递时,发生值拷贝后地址都是一样的,才造成了这个质疑Go不是值传递的假象。

空结构体特性延伸

既然说到了空结构体,就在这里补充一个关于空结构体的知识点:空结构体做为结构体内置字段时是否进行内存对齐。
先来看一个例子:

  1. type Test1 struct {
  2. s struct{}
  3. n byte
  4. m byte
  5. }
  6. type Test2 struct {
  7. n byte
  8. s struct{}
  9. c byte
  10. }
  11. type Test3 struct {
  12. b byte
  13. s struct{}
  14. }
  15. func main(){
  16. fmt.Println(unsafe.Sizeof(Test1{}))
  17. fmt.Println(unsafe.Sizeof(Test2{}))
  18. fmt.Println(unsafe.Sizeof(Test3{}))
  19. }
  20. //运行结果
  21. //2
  22. //2
  23. //2

根据运行结果我们可以得出结论:
空结构体在结构体中的前面和中间时,是不占用空间的,但是当空结构体放到结构体中的最后时,会进行特殊填充,struct { } 作为最后一个字段,会被填充对齐到前一个字段的大小,地址偏移对齐规则不变;

总结

  1. 空结构体也是一个结构体,不过他的size为0,所有的空结构体内存分配都是同一个地址,都是zerobase的地址;
  2. 空结构体作为内嵌字段时要注意放置的顺序,当作为最后一个字段时会进行特殊填充,会被填充对齐到前一个字段的大小,地址偏移对齐规则不变;