简介
有c/c++学习经历的会发现go的struct语法和c/c++很类型,但是golang的struct{}很有意思。
做控制而非数据信息: chan struct{}实现set: map[string]struct{}
解析
结构体是没有位段的结构体,以下是空结构体的一些例子:
type Q struct{}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
有疑问加站长微信联系(非本文作者)