大家好,我是建宇。在Go语言中,有一个特殊的类型,经常被刚接触Go的朋友问到,也不一定理解。就是Go中空结构体(struct)的使用。经常看到有人使用:ch:=make(chanstruct{})来使用没有任何其他类型的结构。非常普遍,这不是偶然现象,背后一定有某种原因。今天炸鱼的文章就带你了解为什么这么用,知其所以然。让我们一起愉快地开启熏鱼之路吧。为什么要使用它?说白了就是想节省空间。然而,新的问题又出现了,为什么用其他类型就不行呢?这就涉及到Go语言中“宽度”的概念,它描述了一个类型的实例占用存储空间的字节数。宽度是类型的属性。Go中的每个值都有一个类型,值的宽度由其类型定义,并且始终是8位的倍数。在Go语言中,我们可以使用unsafe.Sizeof方法来获取://Sizeof接受任何类型的表达式x并返回假设变量v的字节大小//就好像v是通过varv=x声明的一样。//该大小不包括x可能引用的任何内存。//例如,如果x是一个切片,则Sizeof返回切片//描述符的大小,而不是切片引用的内存的大小。//Sizeof的返回值是一个Go常量。funcSizeof(xArbitraryType)uintptr这个方法可以得到value的宽度,自然就可以知道其类型对应的宽度。我们来看看Go语言中几种常见类型的宽度:Println(unsafe.Sizeof(a),unsafe.Sizeof(b),unsafe.Sizeof(c),unsafe.Sizeof(d),unsafe.Sizeof(e),unsafe.Sizeof(f),)}输出结果:816112248可以发现我们列出的这几个类型只是声明,我们什么都没做,还是占据了一定的宽度。如果我们的场景只是一个占位符,我们该怎么办,系统中的开销就这么浪费了?空结构在各种系统中如此频繁出现的原因之一是它们需要一个占位符。恰好Go空结构的宽度是特殊的。如下:funcmain(){varsstruct{}fmt.Println(unsafe.Sizeof(s))}输出结果:0一个空结构体的宽度很直接就是0,即使是变形处理:typeSstruct{Astruct{}Bstruct{}}funcmain(){varsSfmt.Println(unsafe.Sizeof(s))}最终输出结果也是0,完美满足了人们对占位符的基本诉求,即占位坑位,刚好满足基本的输入输出。但是这时候问题又来了,为什么只有空结构有这种特殊处理,其他类型没有呢?这是Go编译器在内存分配期间所做的优化项//所有0字节分配的基地址varzerobaseuintptrfuncmallocgc(sizeuintptr,typ*_type,needzerobool)unsafe.Pointer{...ifsize==0{returnunsafe.Pointer(&zerobase)}}当发现size为0时,会直接返回变量zerobase的引用,即所有0字节的基地址,不占用任何宽度。所以,空结构的广泛使用就是Go开发者利用这个小小的优化来达到占位符的目的。使用场景在了解了为什么使用空结构作为占位符之后,我们来仔细看看它们的真实使用场景。主要分为三部分:实现方法receiver。实现集合类型。实施空渠道。在业务场景中实现方法接收者,我们需要将方法进行组合,将它们表示为一个“组”,方便后续的扩展和维护。但是如果我们使用:typeTstringfunc(s*T)Call()就显得有点不友好了,因为作为字符串类型,会占用一定的空间。在这种情况下,我们将使用一个空结构,这也方便以后为该类型添加公共字段。如下:typeTstruct{}func(s*T)Call(){fmt.Println("Thebrainisfriedfish")}funcmain(){varsTs.Call()}在这个场景中,使用空结构最适合多维考虑,易于扩展,节省空间,最有条理。另外,你会发现,在日常的发展中,你已经下意识地做到了这一点。你可以理解为设计模式与日常生活相结合的另类案例。集合类型的实现在Go语言的标准库中并没有提供集合(Set)的相关实现,所以我们一般在代码中为了方便使用map来代替。但是有一个问题,就是使用集合类型只需要使用键(key),不需要使用值(value)。这是空结构打架的场景:typeSetmap[string]struct{}func(sSet)Append(kstring){s[k]=struct{}{}}func(sSet)Remove(kstring){delete(s,k)}func(sSet)Exist(kstring)bool{_,ok:=s[k]returnok}funcmain(){set:=Set{}set.Append("Friedfish")set.Append("咸鱼")set.Append("清蒸鱼")set.Remove("炸鱼")fmt.Println(set.Exist("炸鱼"))}空结构作为占位符不会增加不必要的内存开销,解决起来也很方便。空通道的实现在Go通道的使用场景中,经常会遇到通知通道,它不需要发送任何数据,只是用来协调Goroutine的运行,流转各种状态或者控制并发。如下:funcmain(){ch:=make(chanstruct{})gofunc(){time.Sleep(1*time.Second)close(ch)}()fmt.Println("大脑好像in...")<-chfmt.Println("Friedfish!")}输出结果:大脑好像在...friedfish!程序会先输出“大脑好像进入了……”然后睡了一会儿再输出“炸鱼!”达到间歇控制通道的效果。由于通道使用空结构,因此没有额外的内存开销。小结在今天的文章中,我介绍了Go语言中几种常见类型的宽度,并根据开头的问题“空结构”进行了分析。最后,分析了业界最常见的三种代码模式,并进入了真实场景。不知道大家之前是否有过类似这篇文章的疑惑呢?欢迎大家在评论区留言交流:)有什么问题欢迎在评论区反馈交流。最好的关系是相互成就。您的好评是创作炸鱼最大的动力。感谢您的支持。文章持续更新中,可微信搜索【脑补炸鱼】阅读,本文已收录在GitHubgithub.com/eddycjy/blog,欢迎Star提醒。
