理解go中空结构体的应用和实现原理

yudotyang · · 1538 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

大家好,我是「Go学堂」的渔夫子,欢迎关注Go学堂,学习更多实战应用案例。 原文地址:https://mp.weixin.qq.com/s/h8vhy8IJKnA8aNbTlCoQtg 在实际项目或开源程序中,相信大家都见过将一个空结构体作为map值的场景: ``` // CanSkipFuncs will skip valid if RequiredFirst is true and the struct field's value is empty var CanSkipFuncs = map[string]struct{}{ "Email": {}, "IP": {}, "Mobile": {}, "Tel": {}, "Phone": {}, "ZipCode": {}, } ``` 或将一个空结构体写入到通道中的使用: ``` w.ch <- struct{}{} ``` 那为什么要这样使用空结构体呢?今天就跟大家一起来学习下空结构体的应用以及底层原理 **01 什么空结构体** 首先来看看空结构体是什么。空结构体也是结构体类型,具有结构体的一切特性。但该结构体中没有任何字段组合。所以,**该空结构体类型的变量占用的空间为0**。 我们通过unsafe.Sizeof函数来验证一下。unsafe.Sizeof函数的作用是返回一个数据类型所占的空间大小。我们验证一下: ```golang var s struct{} fmt.Println(unsafe.Sizeof(s)) // prints 0 ``` 我们看到打印的结果是0,表明struct{}的类型占用的空间是0。 我们还可以通过reflect的类型来验证。 ```golang var s struct{} typ := reflect.TypeOf(s) fmt.Println(typ.Size()) // 0 ``` 我们看到,通过映射变量s的类型,输出空类型的空间大小也是0。 **02 空结构体类型变量的地址** 我们知道,在编程语言中,变量的作用就是在内存中,标记和存储数据的。也就是说每个变量会对应着一块内存空间,既然是内存空间,那就应该有对应的内存地址。那空结构体类型变量的地址是什么呢?我们通过如下代码来看下: ``` package main import ( "fmt" "unsafe" ) 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 } ``` 我们发现,所有空结构体类型的变量地址都是一样的。 那这是为什么呢? 在底层实现中,这和一个很重要的 zerobase 变量有关(在runtime里多次使用到了这个变量),而zerobase 变量是一个 uintptr 的全局变量,占用8个字节。在go源码src/runtime/malloc.go中有如下定义: ``` // base address for all 0-byte allocations var zerobase uintptr ``` 只要你将struct{} 赋值给一个或者多个变量,它都返回这个 zerobase 的地址,这点我们上面已经证实过这一点了。 在golang中大量的地方使用到了这个 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) } ... } ``` **** **03 空结构体的应用场景** 一般我们用在用户不关注值内容的情况下,只是作为一个信号或一个占位符来使用。 - 基于map实现集合功能。 - 与channel组合使用,实现一个信号 基于map实现集合功能就是我们开头提到的。使用空结构体**不占用存储空间外,还有一个语义上的原因**。例如: ``` var CanSkipFuncs = map[string]bool{ "Email": true, "IP": true, "Mobile": true, "Tel": false, "Phone": false, "ZipCode": false, } ``` 我们这里将空结构体类型更换成布尔类型。首先,声明下,CanSkipFuncs集合代表的是所有要跳过的函数。所以这里的值设置成true还是false是没有任何影响的。 那么如果另一位同学在查看或review代码的时候,很有可能带来疑惑。对于值所表达的意图就有所担心怀疑,提高了理解代码的门槛。心里会想如果值为true 的话,会执行一个逻辑,为false的话会执行另一个逻辑。而相比使用一个空结构体strcut{}来理解起来容易提高心智,别人一看空结构体struct{}就知道要表达的意思是不需要关心值是什么,只需要关心键值即可。 我们再来看下和channel组合使用的例子。在etcd项目中,就有通过往channel中写入一个空结构体作为信号的,源码位于/etcd/server/auth/simple_token.go中,如下: ```golang func (tm *simpleTokenTTLKeeper) stop() { select { case tm.stopc <- struct{}{}: case <-tm.donec: } <-tm.donec } ``` 还有一种是基于缓冲channel实现并发限速。如下: ``` var limit = make(chan struct{}, 3) func main() { // ………… for _, w := range work { go func() { limit <- struct{}{} w() <-limit }() } // ………… } ``` **** **04 总结** 空结构体是一种不包含任何字段的结构体类型,不仅具有结构体类型的一切属性,而且该结构体类型占用的空间为0。 **参考链接:** <https://blog.haohtml.com/archives/20339> <https://ijayer.github.io/post/tech/code/golang/20200419_emtpy_struct_in_go/>

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1538 次点击  ∙  3 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
1 回复  |  直到 2022-01-18 10:48:31
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传