空结构体 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