首页 >后端开发 >Golang >解密Go:空结构

解密Go:空结构

PHPz
PHPz原创
2024-09-11 12:30:32873浏览

Decrypt Go: empty struct

在 Go 中,普通的结构体通常会占用一块内存。但是,有一种特殊情况:如果它是空结构,则其大小为零。这怎么可能,空结构有什么用?

本文首发于Medium MPP计划。如果您是Medium用户,请在Medium上关注我。非常感谢。

type Test struct {
    A int
    B string
}

func main() {
    fmt.Println(unsafe.Sizeof(new(Test)))
    fmt.Println(unsafe.Sizeof(struct{}{}))
}

/*
8
0
*/

空结构的秘密

特殊变量:zerobase

空结构是没有内存大小的结构。这种说法是正确的,但更准确地说,它实际上有一个特殊的起点:zerobase 变量。这是一个uintptr全局变量,占用8个字节。每当定义无数个 struct {} 变量时,编译器都会分配这个零基变量的地址。换句话说,在 Go 中,任何大小为 0 的内存分配都使用相同的地址 &zerobase。

示例

package main

import "fmt"

type emptyStruct struct {}

func main() {
    a := struct{}{}
    b := struct{}{}
    c := emptyStruct{}

    fmt.Printf("%p\n", &a)
    fmt.Printf("%p\n", &b)
    fmt.Printf("%p\n", &c)
}

// 0x58e360
// 0x58e360
// 0x58e360

空结构体的变量的内存地址都是相同的。这是因为编译器在编译过程中遇到这种特殊类型的内存分配时会分配&zerobase。这个逻辑在mallocgc函数中:

//go:linkname mallocgc  
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {  
    ...
    if size == 0 {  
       return unsafe.Pointer(&zerobase)  
    }
    ...

这是 Empty 结构的秘密。通过这个特殊的变量,我们可以完成很多功能。

空结构和内存对齐

通常,如果空结构是较大结构的一部分,它不会占用内存。但是,有一种特殊情况,即空结构是最后一个字段;它会触发内存对齐。

示例

type A struct {
    x int
    y string
    z struct{}
}
type B struct {
    x int
    z struct{}
    y string
}

func main() {
    println(unsafe.Alignof(A{}))
    println(unsafe.Alignof(B{}))
    println(unsafe.Sizeof(A{}))
    println(unsafe.Sizeof(B{}))
}

/**
8
8
32
24
**/

当存在指向字段的指针时,返回的地址可能位于结构体之外,如果在释放结构体时未释放内存,则可能导致内存泄漏。因此,当空结构是另一个结构的最后一个字段时,为了安全起见,会分配额外的内存。如果空结构体位于开头或中间,则其地址与下一个变量相同。

type A struct {  
    x int  
    y string  
    z struct{}  
}  
type B struct {  
    x int  
    z struct{}  
    y string  
}  

func main() {  
    a := A{}  
    b := B{}  
    fmt.Printf("%p\n", &a.y)  
    fmt.Printf("%p\n", &a.z)  
    fmt.Printf("%p\n", &b.y)  
    fmt.Printf("%p\n", &b.z)  
}

/**
0x1400012c008
0x1400012c018
0x1400012e008
0x1400012e008
**/

空结构的用例

空struct struct{}存在的核心原因是为了节省内存。当您需要一个结构体但不关心其内容时,请考虑使用空结构体。 Go的核心复合结构如map、chan、slice都可以使用struct{}。

映射和结构{}

// Create map
m := make(map[int]struct{})
// Assign value
m[1] = struct{}{}
// Check if key exists
_, ok := m[1]

陈和结构{}

一个经典场景结合了channel和struct{},其中struct{}通常用作信号而不关心其内容。正如前面文章分析的那样,通道的本质数据结构是管理结构加上环形缓冲区。如果 struct{} 用作元素,则环形缓冲区为零分配。

chan 和 struct{} 一起使用的唯一用途是信号传输,因为空结构本身不能携带任何值。一般来说,它是在没有缓冲通道的情况下使用的。

// Create a signal channel
waitc := make(chan struct{})

// ...
goroutine 1:
    // Send signal: push element
    waitc <- struct{}{}
    // Send signal: close
    close(waitc)

goroutine 2:
    select {
    // Receive signal and perform corresponding actions
    case <-waitc:
    }    

在这种情况下,struct{} 是绝对必要的吗?事实并非如此,节省的内存可以忽略不计。关键是不关心chan的元素值,所以使用struct{}。

概括

  1. 空结构体仍然是一个结构体,只是大小为 0。
  2. 所有空结构共享相同的地址:zerobase 的地址。
  3. 我们可以利用空结构体不占用内存的特性来优化代码,比如使用map来实现集合和通道。

参考

  1. 空结构,戴夫·切尼
  2. Go 最细节篇— struct{} 空结构体到底啥是啥?

以上是解密Go:空结构的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn