Go语言--空结构体struct{}解析

た 入场券 2022-03-20 15:44 509阅读 0赞

简介

有c/c++学习经历的会发现go的struct语法和c/c++很类型,但是golang的struct{}很有意思。

  1. 做控制而非数据信息:chan struct{}
  2. 实现set:map[string]struct{}

解析

结构体是没有位段的结构体,以下是空结构体的一些例子:

  1. type Q struct{}
  2. var q struct{}

但是如果一个就结构体没有位段,不包含任何数据,那么他的用处是什么?我们能够利用空结构体完成什么任务?

背景

在深入研究空结构体之前,我想先简短的介绍一下关于结构体宽度的知识。

术语宽度来自于gc编译器,但是他的词源可以追溯到几十年以前。

宽度描述了存储一个数据类型实例需要占用的字节数,由于进程的内存空间是一维的,我更倾向于将宽度理解为Size(这个词实在不知道怎么翻译了,请谅解)。

宽度是数据类型的一个属性。Go程序中所有的实例都是一种数据类型,一个实例的宽度是由他的数据类型决定的,通常是8bit的整数倍。

我们可以通过unsafe.Sizeof()函数获取任何实例的宽度:

  1. var s string
  2. var c complex128
  3. fmt.Println(unsafe.Sizeof(s)) // prints 8
  4. fmt.Println(unsafe.Sizeof(c)) // prints 16

数组的宽度是他元素宽度的整数倍。

  1. var a [3]uint32
  2. fmt.Println(unsafe.Sizeof(a)) // prints 12

结构体提供了定义组合类型的灵活方式,组合类型的宽度是字段宽度的和,然后再加上填充宽度。

  1. type S struct {
  2. a uint16
  3. b uint32
  4. }
  5. var s S
  6. fmt.Println(unsafe.Sizeof(s)) // prints 8, not 6

空结构体

现在我们清楚的认识到空结构体的宽度是0,他占用了0字节的内存空间。

  1. var s struct{}
  2. fmt.Println(unsafe.Sizeof(s)) // prints 0

由于空结构体占用0字节,那么空结构体也不需要填充字节。所以空结构体组成的组合数据类型也不会占用内存空间。

  1. type S struct {
  2. A struct{}
  3. B struct{}
  4. }
  5. var s S
  6. fmt.Println(unsafe.Sizeof(s)) // prints 0

空结构体作用

由于Go的正交性,空结构体可以像其他结构体一样正常使用。正常结构体拥有的属性,空结构体一样具有。

你可以定义一个空结构体组成的数组,当然这个切片不占用内存空间。

  1. var x [1000000000]struct{}
  2. fmt.Println(unsafe.Sizeof(x)) // prints 0

空结构体组成的切片的宽度只是他的头部数据的长度,就像上例展示的那样,切片元素不占用内存空间。

  1. var x = make([]struct{}, 1000000000)
  2. fmt.Println(unsafe.Sizeof(x)) // prints 12 in the playground

当然切片的内置子切片、长度和容量等属性依旧可以工作。

  1. ar x = make([]struct{}, 100)
  2. var y = x[:50]
  3. fmt.Println(len(y), cap(y)) // prints 50 100

你甚至可以寻址一个空结构体,空结构体是可寻址的,就像其他类型的实例一样。

  1. var a struct{}
  2. var b = &a

有意思的是两个空结构体的地址可以相等。(go 1.12 版本,不相等)

  1. var a, b struct{}
  2. fmt.Println(&a == &b) // false

空结构体的元素也具有一样的属性。

  1. a := make([]struct{}, 10)
  2. b := make([]struct{}, 20)
  3. fmt.Println(&a == &b) // false, a and b are different slices
  4. fmt.Println(&a[0] == &b[0]) // true, their backing arrays are the same

为什么会这样?因为空结构体不包含位段,所以不存储数据。如果空结构体不包含数据,那么就没有办法说两个空结构体的值不相等,所以空结构体的值就这样相等了。

  1. a := struct{}{} // not the zero value, a real new struct{} instance
  2. b := struct{}{}
  3. fmt.Println(a == b) // true

空结构体作为接收者

现在让我们展示一下空结构体如何像其他结构体工作,空结构体可以作为方法的接收者。

  1. type S struct{}
  2. func (s *S) addr() { fmt.Printf("%p\n", s) }
  3. func main() {
  4. var a, b S
  5. a.addr() // 0x1beeb0
  6. b.addr() // 0x1beeb0
  7. }

chan struct{}

在Go语言中,有一种特殊的struct{}类型的channel,它不能被写入任何数据,只有通过close()函数进行关闭操作,才能进行输出操作。struct{}类型的channel不占用任何内存!!!
定义:

  1. var sig = make(chan struct{})

使用空 struct 是对内存更友好的开发方式,在 go 源代码中针对 空struct 类数据内存申请部分,返回地址都是一个固定的地址。那么就避免了可能的内存滥用。

栗子:

  1. package main
  2. import "fmt"
  3. import "time"
  4. var strChan = make(chan string,3)
  5. func main(){
  6. syncChan1 := make(chan struct{},1) //接收同步变量
  7. syncChan2 := make(chan struct{},2) //主线程启动了两个goruntime线程,
  8. //等这两个goruntime线程结束后主线程才能结束
  9. //用于演示接受操作
  10. go func(){
  11. <- syncChan1 //表示可以开始接收数据了,否则等待
  12. fmt.Println("[receiver] Received a sync signal and wait a second...")
  13. time.Sleep(time.Second)
  14. for{
  15. if elem,ok := <-strChan;ok{
  16. fmt.Println("[receiver] Received:",elem)
  17. }else{
  18. break
  19. }
  20. }
  21. fmt.Println("[receiver] Stopped.")
  22. syncChan2 <- struct{}{}
  23. }()
  24. //用于演示发送操作
  25. go func(){
  26. for i,elem := range []string{"a","b","c","d"}{
  27. fmt.Println("[sender] Sent:",elem)
  28. strChan <- elem
  29. if (i+1)%3==0 {
  30. syncChan1 <- struct{}{}
  31. fmt.Println("[sender] Sent a sync signal. wait 1 secnd...")
  32. time.Sleep(time.Second)
  33. }
  34. }
  35. fmt.Println("[sender] wait 2 seconds...")
  36. time.Sleep(time.Second)
  37. close(strChan)
  38. syncChan2 <- struct{}{}
  39. }()
  40. //主线程等待发送线程和接收线程结束后再结束
  41. fmt.Println("[main] waiting...")
  42. <- syncChan2
  43. <- syncChan2
  44. fmt.Println("[main] stoped")
  45. }

运行结果:

  1. [main] waiting...
  2. [sender] Sent: a
  3. [sender] Sent: b
  4. [sender] Sent: c
  5. [sender] Sent a sync signal. wait 1 secnd...
  6. [receiver] Received a sync signal and wait a second...
  7. [receiver] Received: a
  8. [receiver] Received: b
  9. [receiver] Received: c
  10. [sender] Sent: d
  11. [sender] wait 2 seconds...
  12. [receiver] Received: d
  13. [receiver] Stopped.
  14. [main] stoped
  • struch{}代表不包含任何字段的结构体类型,也可称为空结构体类型。在go语言中,空结构体类型是不占用系统内存的,并且所有该类型的变量都拥有相同的内存地址。建议用于传递信号的通道都以struct{}作为元素类型,除非需要传递更多的信息
  • 发送方向通道发送的值会被复制,接收方接收到的总是该值得副本,而不是该值本身。经由通道传递的值最少会被复制一次,最多会被复制两次。例如,当向一个已空的通道发送值,且已有至少一个接收方因此等待时,该通道会绕过本身的缓冲队列,直接把这个值复制给最早等待的那个接收方,这种情况传递的值只复制一次;当从一个已满的通道接收值,且已有至少一个发送方因此等待时,该通道会把缓冲队列中最早进入的那个值复制给接收方,再把最早等待的发送方要发送的数据复制到那个值得原先位置上(通道的缓冲队列属于环形队列,这样做是没有问题的),这种情况传递的值复制两次。
  • 通道传递是复制传递的值。因此如果传递的是值类型,接收方对该值得修改不会影响发送方持有的值;如果传递的是引用类型,则发送方或者接收方对该对象的修改会影响双方所持有的对象

参考:https://dave.cheney.net/2014/03/25/the-empty-struct

End

发表评论

表情:
评论列表 (有 0 条评论,509人围观)

还没有评论,来说两句吧...

相关阅读

    相关 go语言结构

    Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。 类型别名和自定义类型 自定

    相关 GO语言结构

    当我第一次接触到C语言时,就对结构体投入了极大的兴趣,认为这个东西以后大有作为,后来接触Java、C++,面向对象编程中的对象进入我的视线,经过了这么多年的磨练,回过头来再看结