空结构体 empty struct 也是结构体类型,不过呢它不包含有任何字段。声明定义如下:

1
2
3
type Q struct{}

var q struct{}

So, 一个没有包含任意字段的结构体能干什么用?或者说它的用途是什么呢?

在进入 empty struct 之前,先了解下 Width(宽度)

Width(宽度)

宽度 和其他大多数术语一样,来源于 gc 编译器,其起源可追溯到几十年前了。

宽度,指的是存储一个 类型实例 所占用的字节数。由于处理器的地址空间是一维的,所以这里用宽度会比用大小一词更适用点。

宽度是一个类型的属性。Go 程序中,每个值都会有一个类型,值的宽度就取决于其定义的类型,通常是 8bits 的整数倍。

我们可以通过unsafe.Sizeof(v) 来获取值的宽度:

1
2
3
4
5
6
7
// Sizeof takes an expression x of any type and returns the size in bytes
// of a hypothetical variable v as if v was declared via var v = x.
// The size does not include any memory possibly referenced by x.
// For instance, if x is a slice, Sizeof returns the size of the slice
// descriptor, not the size of the memory referenced by the slice.
// The return value of Sizeof is a Go constant.
func Sizeof(x ArbitraryType) uintptr

for examples:

整数类型占用的宽度

1
2
	var i int
	fmt.Printf("width=%d\n", unsafe.Sizeof(i)) // prints 8

bool 类型占用的宽度

1
2
  var b bool
  fmt.Printf("width=%d\n", unsafe.Sizeof(b)) // prints 1

string 类型占用的宽度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// string type defined in /go/src/runtime/string.go
type stringStruct struct {
    str unsafe.Pointer
    len int
}

func main() {
    // string 类型的变量占 16 bytes 与其底层的数据结构
    // 有关,如下 stringStruct 定义,由 unsafePointer
    // 个 int 类型两个字段组成,因此 string 类型的变量的
    // 宽度是其多个元素类型宽度之和
    var s string
    fmt.Printf("width=%d\n", unsafe.Sizeof(s)) // prints 16
    var ss stringStruct
    fmt.Printf("width=%d\n", unsafe.Sizeof(ss)) // prints 16
}

数组类型占用的宽度

1
2
3
4
5
func main() {
    // 数组类型变量的宽度也是其多个元素之和
    var arr [3]uint32
    fmt.Printf("width=%d\n", unsafe.Sizeof(arr)) // prints 12
}

结构体类型提供了更灵活方式,可以定义组合类型。

1
2
3
4
5
6
7
8
func main() {
	type S struct {
		a uint64 // 8
		b uint32 // 4
	}
	var s S
	fmt.Printf("width=%d\n", unsafe.Sizeof(s)) // prints 16, not 12
}

Note: 有关内存对齐的解释

空结构体

现在我们已经探讨了宽度,很明显,空结构的宽度为 0。它占据了零字节的存储空间。

1
2
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // prints 0

空结构体所占用的宽度为 0,所以作为结构体的字段时其宽度也不会叠加,即为 0。因此,由空结构组成的结构所占用的存储空间也为 0 bytes.

如下:

1
2
3
4
5
6
type S struct {
	s1 struct{}
	s2 struct{}
}
var s S
fmt.Printf("width=%d\n", unsafe.Sizeof(s)) // prints 0

空结构体可以做什么?

根据 Go 的正交性原则,空结构体是一个与其他结构体类型一样的结构体类型。所有普通结构体中的惯用法都同样适用于空结构体。

声明的空结构类型的数组,同样也是不消耗存储空间的。

1
2
var a [100]struct{}
fmt.Printf("width=%d\n", unsafe.Sizeof(a)) // prints 0

slice 由于底层数据结构由 len(int)、cap(int)、array(unsafe.Pointer) 构成,所以声明为空结构体类型的切片,其底层的存储元素的数组结构也是不消耗内存的

1
2
var s = make([]struct{}, 100)
fmt.Printf("width=%d\n", unsafe.Sizeof(a)) // prints 0

Note:声明空结构体类型的切片,len、cap 等依然适用。

同时,空结构类型亦是可寻址的

1
2
3
var s struct{}
var a = &s
fmt.Printf("addr=%v", &s)

更有趣的是,声明两个空结构体类型的变量,其地址也是一样的。

1
2
3
var s1, s2 struct{}
b2 := &s1 == &s2
fmt.Printf("s1=s2=%v, s1_addr=%p, s2_addr=%p\n", b2, &s1, &s2)

对于空结构体类型的数组,以上属性也是成立

1
2
3
4
a := make([]struct{}, 10)
b := make([]struct{}, 20)
fmt.Println(&a == &b)       // false, a and b are different slices
fmt.Println(&a[0] == &b[0]) // true, their backing arrays are the same

为什么会这样?如果你仔细想想,空结构体类型没有包含任务字段,因此不持有任何数据。如果空结构体类型不持有任何数据,那么就没有办法说两个空结构体的值不相等。

1
2
3
4
s1 := struct{}{}  // not the zero value, a real new struct{} instance
s2 := struct{}{}

fmt.Println(s1 == s2) // true

空结构体作为方法接收者

像普通结构体一样,空结构体也可以作为方法的接收者使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type S struct{}

func(s *S) addr() string {
    return fmt.Sprintf("%p", s)
}

func main() {
    s := &S{}
    fmt.Printf("%v\n", s.addr()) // print, 0x5a8da8
}

可以看到,在这篇文章的示例中,所有零大小的值的地址都是 0x5a8da8。这个值,也会随着 Go 版本的变化而改变。

And More

虽然整篇文章都在讨论语言层面的那些晦涩难懂的知识点,但是也空结构体也有一个很重要的用途,那就是用于在 goroutines 之间传递信号。

Reading more about channels on here

See also