作者:陈明勇

个人网站:chenmingyong.cn

文章继续更新,假如本文能让您有所收获,欢迎重视本号。

微信阅读可搜《Go 技能干货》。这篇文章已被收录于 GitHub github.com/chenmingyon…,欢迎咱们 Star 催更并继续重视。


Go Version → 1.20.4

前言

Go 言语中,有一种特别的用法可能让许多人感到困惑,那便是空结构体 struct{}。在本文中,我将对 Go 空结构体进行详解,预备好了吗?预备一杯你最喜欢的饮料或茶,跟着本文一探求竟吧。

什么是空结构体

不包括任何字段的结构体,便是空结构体。它有以下两种界说办法:

  • 匿名空结构体

    var e sruct{}
    
  • 命名空结构体

    type EmptyStruct struct{}
    var e EmptyStruct
    

空结构体的特色

空结构体主要有以下几个特色:

  • 零内存占用
  • 地址相同
  • 无状况

零内存占用

空结构体不占用任何内存空间,这使有空结构体在内存优化方面非常有用,咱们来通过比如看看是否真的是零内存占用:

package main
import (
   "fmt"
   "unsafe"
)
func main() {
   var a int
   var b string
   var e struct{}
   fmt.Println(unsafe.Sizeof(a)) // 4
   fmt.Println(unsafe.Sizeof(b)) // 8
   fmt.Println(unsafe.Sizeof(e)) // 0
}

通过打印成果比照可知,空结构体内存占用为 0

地址相同

无论创立多少个空结构体,它们所指向的地址都相同的。

package main
import (
   "fmt"
)
func main() {
   var e struct{}
   var e2 struct{}
   fmt.Printf("%p\n", &e)  // 0x90b418
   fmt.Printf("%p\n", &e2) // 0x90b418
   fmt.Println(&e == &e2)    // true
}

无状况

因为空结构体不包括任何字段,因而它不能有状况。这使有空结构体在表明无状况的目标或状况时非常有用。

为什么是零内存和地址相同

要理解为什么空结构体在内存上是零巨细(零内存)而且多个空结构体的地址是相同的,需求深入研究 Go 的源码。

/go/src/runtime/malloc.go

// base address for all 0-byte allocations
var zerobase uintptr
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    
    if size == 0 {
       return unsafe.Pointer(&zerobase)
    }
    

依据 malloc.go 源码的部分内容,当要分配的目标巨细 size0 时,会回来指向 zerobase 的指针。zerobase 是一个用于分配零字节目标的基准地址,它不占用任何实践的内存空间。

空结构体的运用场景

空结构体主要有以下三种运用场景:

  • 完结 Set 调集类型
  • 用于通道信号
  • 作为办法接收器

完结 Set 调集类型

Go 言语中,虽然没有内置 Set 调集类型,可是咱们能够运用 map 类型来完结一个 Set 调集。因为 mapkey 具有唯一性,咱们能够将元素存储为 key,而 value 没有实践作用,为了节省内存,咱们能够运用空结构体作为 value 的值。

package main
import "fmt"
type Set[K comparable] map[K]struct{}
func (s Set[K]) Add(val K) {
   s[val] = struct{}{}
}
func (s Set[K]) Remove(val K) {
   delete(s, val)
}
func (s Set[K]) Contains(val K) bool {
   _, ok := s[val]
   return ok
}
func main() {
   set := Set[string]{}
   set.Add("陈明勇")
   fmt.Println(set.Contains("陈明勇")) // true
   set.Remove("陈明勇")
   fmt.Println(set.Contains("陈明勇")) // false
}

用于通道信号

空结构体常用于 Goroutine 之间的信号传递,尤其是不关心通道中传递的具体数据,只需求一个触发信号时。例如,咱们能够运用空结构体通道来告诉一个 Goroutine 停止作业:

package main
import (  
   "fmt"  
   "time"  
)  
func main() {  
   quit := make(chan struct{})  
   go func() {  
      // 模仿作业  
      fmt.Println("作业中...")  
      time.Sleep(3 * time.Second)  
      // 封闭退出信号  
      close(quit)
   }()  
   // 堵塞,等候退出信号被封闭  
   <-quit  
   fmt.Println("已收到退出信号,退出中...")  
}

在这个比如中,创立了一个通道 quit,并在一个单独的 Goroutine 中模仿执行作业。在完结作业后,封闭了 quit 通道,表明退出信号。主函数在 <-quit 处堵塞,直到收到退出信号,然后打印一条音讯并退出程序。

因为通道运用的类型是空结构体,因而不会带来额定的内存开支。

Go 规范库中,context 包中的 Context 接口的 Done() 办法回来一个通道信号,用于告诉相关操作的完结状况。这个通道信号的回来值便是运用了空结构体。

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

作为办法接收器

有时候咱们需求创立一组办法集的完结(一般来说是完结一个接口),但并不需求在这个完结中存储任何数据,这种状况下,咱们能够运用空结构体来完结:

type Person interface {
   SayHello()
   Sleep()
}
type CMY struct{}
func (c CMY) SayHello() {
   fmt.Println("你好,我叫陈明勇。")
}
func (c CMY) Sleep() {
   fmt.Println("陈明勇睡觉中...")
}

这个比如界说了一个接口 Person 和一个结构体 CMY ,并为 CMY 完结了 Person 接口,界说了一组办法(SayHelloSleep)。

因为 CMY 结构体为空结构体,因而不会带来额定的内存开支。

小结

在本文中,首要介绍了 Go 言语 空结构体 的概念和界说办法,它有两种界说办法;

随后对 空结构体 的特色进行介绍,包括其零内存和多个变量地址相同的特性;

接着进一步深入源码,探求了为什么空结构体在 Go 言语中是零内存且多变量地址相同,原因是当要分配的目标巨细size0时,会回来指向zerobase的指针;

最后列举了空结构体的三个运用场景,通过这些代码示例,展示了空结构体在实践应用中的一些常见用途。

你还知道 空结构体 的其他运用场景吗?欢迎评论区留言讨论。

引荐内容

一文掌握 Go 并发形式 Context 上下文

Go 办法接收器:挑选值接收器仍是指针接收器?

Go 言语中没有枚举类型,可是咱们能够这样做