1.空结构体介绍
首先来看看空结构体是什么;空结构体也是结构体类型,具有结构体的一切特性,但该结构体中没有任何字段组合。
type a struct {
}
func main() {
fmt.Println(a{}) //输出{}
}
type a struct {
}
func main() {
fmt.Println(reflect.TypeOf(a{})) //输出:main.a 注意:不能直接写a,必须得写a{}
fmt.Println(reflect.TypeOf(&a{})) //输出:*main.a
b := a{}
p1 := &b
p2 := *p1
fmt.Println(reflect.TypeOf(b)) //输出main.a
fmt.Println(reflect.TypeOf(p1)) //输出:*main.a
fmt.Println(reflect.TypeOf(p2)) //输出:main.a
c := &a{}
p3 := &c
p4 := *c
fmt.Println(reflect.TypeOf(c)) //输出:*main.a
fmt.Println(reflect.TypeOf(p3)) //输出:**main.a
fmt.Println(reflect.TypeOf(p4)) //输出:main.a
}
2. 空结构体变量的占用空间
2.1 unsafe.Sizeof函数验证
struct{}的类型占用的空间是0;我们通过unsafe.Sizeof函数来验证一下;unsafe.Sizeof函数的作用是返回一个数据类型所占的空间大小,我们验证一下:
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // 输出: 0
2.2 reflect的类型验证
我们看到,通过映射变量s的类型,输出空类型的空间大小也是0:
var s struct{}
typ := reflect.TypeOf(s)
fmt.Println(typ.Size()) //输出: 0
3.空结构体类型变量的地址
3.1所有空结构体类型的变量地址都是一样
我们知道,在编程语言中,变量的作用就是在内存中,标记和存储数据的。也就是说每个变量会对应着一块内存空间,既然是内存空间,那就应该有对应的内存地址。那空结构体类型变量的地址是什么呢?我们通过如下代码来看下:
type emptyStruct struct{}
func main() {
a := struct{}{}
b := struct{}{}
c := emptyStruct{}
fmt.Println(a)
fmt.Printf("%pn", &a) //0x116be80
fmt.Printf("%pn", &b) //0x116be80
fmt.Printf("%pn", &c) //0x116be80
fmt.Println(a == b) //true
}
发现所有空结构体类型的变量地址都是一样的
3.2通过底层查看原因
在golang中,只要分配的内存为0,就返回zerobase这个变量地址l;
只要你将struct{} 赋值给一个或者多个变量,它都返回这个 zerobase 的地址;
zerobase这个变量是一个 uintptr 的全局变量,占用8个字节,在go源码src/runtime/malloc.go中有定义(在runtime里多次使用到了zerobase这个变量);
zerobase返回为0与go源码src/runtime/malloc.go的mallocgc函数有关系,其定义如下:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if gcphase == _GCmarktermination {
throw("mallocgc called with gcphase == _GCmarktermination")
}
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}
4.空结构体的应用场景
4.1基于map实现集合功能
一般我们用在用户不关注值内容的情况下,只是作为一个占位符来使用;
使用空结构体不占用存储空间外,还有一个语义上的原因;
一看空结构体struct{}就知道要表达的意思是不需要关心值是什么,只需要关心键值即可
var CanSkipFuncs = map[string]struct{}{
"Email": {},
"IP": {},
"Mobile": {},
"Tel": {},
"Phone": {},
"ZipCode": {},
}
fmt.Println(CanSkipFuncs) //输出:map[Email:{} IP:{} Mobile:{} Phone:{} Tel:{} ZipCode:{}]
4.2与channel组合使用,实现一个信号
func (tm *simpleTokenTTLKeeper) stop() {
select {
case tm.stopc <- struct{}{}:
case <-tm.donec:
}
<-tm.donec
}
4.3与channel组合使用,基于缓冲channel实现并发限速
func (tm *simpleTokenTTLKeeper) stop() {
select {
case tm.stopc <- struct{}{}:
case <-tm.donec:
}
<-tm.donec
}