当前位置: 首页 > 工具软件 > BStruct > 使用案例 >

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

梅耘豪
2023-12-01

简介

有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()函数获取任何实例的宽度:

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

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

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

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

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

空结构体

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

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

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

type S struct {
A struct{}
B struct{}
}
var s S
fmt.Println(unsafe.Sizeof(s)) // prints 0

空结构体作用

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

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

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

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

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

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

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

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

var a struct{}
var b = &a

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

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

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

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

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

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

空结构体作为接收者

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

type S struct{}
func (s *S) addr() { fmt.Printf("%p\n", s) }
func main() {
var a, b S
a.addr() // 0x1beeb0
b.addr() // 0x1beeb0
}

chan struct{}

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

var sig = make(chan struct{})

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

栗子:

package main

import "fmt"
import "time"

var strChan = make(chan string,3)

func main(){
    syncChan1 := make(chan struct{},1)  //接收同步变量  
    syncChan2 := make(chan struct{},2) //主线程启动了两个goruntime线程,
                                       //等这两个goruntime线程结束后主线程才能结束

    //用于演示接受操作
    go func(){
        <- syncChan1  //表示可以开始接收数据了,否则等待
        fmt.Println("[receiver] Received a sync signal and wait a second...")
        time.Sleep(time.Second)
        for{
            if elem,ok := <-strChan;ok{
                fmt.Println("[receiver] Received:",elem)
            }else{
                break
            }
        }
        fmt.Println("[receiver] Stopped.")
        syncChan2 <- struct{}{}
    }()

    //用于演示发送操作
    go func(){
        for i,elem := range []string{"a","b","c","d"}{
            fmt.Println("[sender] Sent:",elem)
            strChan <- elem
            if (i+1)%3==0 {
                syncChan1 <- struct{}{}
                fmt.Println("[sender] Sent a sync signal. wait 1 secnd...")
                time.Sleep(time.Second)
            }
        }
        fmt.Println("[sender] wait 2 seconds...")
        time.Sleep(time.Second)
        close(strChan)
        syncChan2 <- struct{}{}
    }()

    //主线程等待发送线程和接收线程结束后再结束
    fmt.Println("[main] waiting...")
    <- syncChan2
    <- syncChan2
    fmt.Println("[main] stoped")
}

运行结果:

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

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

End

 类似资料: