理解Go中空结构体的应用和实现原理

开发 前端
空结构体是一种不包含任何字段的结构体类型,不仅具有结构体类型的一切属性,而且该结构体类型占用的空间为0。常被用于map的集合或和通道配合使用发送信号使用的场景。

在实际项目或开源程序中,相信大家都见过将一个空结构体作为map值的场景:

// CanSkipFuncs will skip valid if RequiredFirst is true and the struct field's value is empty
var CanSkipFuncs = map[string]struct{}{
    "Email":   {},
    "IP":      {},
    "Mobile":  {},
    "Tel":     {},
    "Phone":   {},
    "ZipCode": {},
}

或将一个空结构体写入到通道中的使用:

w.ch <- struct{}{}

那为什么要这样使用空结构体呢?今天就跟大家一起来学习下空结构体的应用以及底层原理。

1 什么空结构体

首先来看看空结构体是什么。空结构体也是结构体类型,具有结构体的一切特性。但该结构体中没有任何字段组合。所以,该空结构体类型的变量占用的空间为0。

我们通过unsafe.Sizeof函数来验证一下。unsafe.Sizeof函数的作用是返回一个数据类型所占的空间大小。我们验证一下:

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

我们看到打印的结果是0,表明struct{}的类型占用的空间是0。

我们还可以通过reflect的类型来验证。

var s struct{}
typ := reflect.TypeOf(s)
fmt.Println(typ.Size()) // 0

我们看到,通过映射变量s的类型,输出空类型的空间大小也是0。

2 空结构体类型变量的地址

我们知道,在编程语言中,变量的作用就是在内存中,标记和存储数据的。也就是说每个变量会对应着一块内存空间,既然是内存空间,那就应该有对应的内存地址。那空结构体类型变量的地址是什么呢?我们通过如下代码来看下:

package main
 
import (
    "fmt"
    "unsafe"
)
 
type emptyStruct struct{}
 
func main() {
    a := struct{}{}
    b := struct{}{}
 
    c := emptyStruct{}
 
    fmt.Println(a)
    fmt.Printf("%pn", &a) //0x116be80
    fmt.Printf("%pn", &b) //0x116be80
    fmt.Printf("%pn", &c) //0x116be80
 
    fmt.Println(a == b) //true
}

我们发现,所有空结构体类型的变量地址都是一样的。那这是为什么呢?

在底层实现中,这和一个很重要的 zerobase 变量有关(在runtime里多次使用到了这个变量),而zerobase 变量是一个 uintptr 的全局变量,占用8个字节。在go源码src/runtime/malloc.go中有如下定义:

// base address for all 0-byte allocations
var zerobase uintptr

只要你将struct{} 赋值给一个或者多个变量,它都返回这个 zerobase 的地址,这点我们上面已经证实过这一点了。

在golang中大量的地方使用到了这个 zerobase 变量,只要分配的内存为0,就返回这个变量地址,在go源码src/runtime/malloc.go的mallocgc函数中定义如下:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    if gcphase == _GCmarktermination {
  throw("mallocgc called with gcphase == _GCmarktermination")
    }


    if size == 0 {
  return unsafe.Pointer(&zerobase)
    }
    ...
}

3 空结构体的应用场景

一般我们用在用户不关注值内容的情况下,只是作为一个信号或一个占位符来使用。

  • 基于map实现集合功能。
  • 与channel组合使用,实现一个信号

基于map实现集合功能就是我们开头提到的。使用空结构体不占用存储空间外,还有一个语义上的原因。例如:

var CanSkipFuncs = map[string]bool{
    "Email":   true,
    "IP":      true,
    "Mobile":  true,
    "Tel":     false,
    "Phone":   false,
    "ZipCode": false,
}

我们这里将空结构体类型更换成布尔类型。首先,声明下,CanSkipFuncs集合代表的是所有要跳过的函数。所以这里的值设置成true还是false是没有任何影响的。

那么当阅读或review代码的时候,很有可能带来疑惑,对于值所表达的意图就有所怀疑,增加了理解代码的难度。就会理解成当值为true时会执行一个分支,当值为false时会执行另一段逻辑。而相比使用一个空结构体strcut{}理解起来会更容易,一看空结构体struct{}就知道要表达的意思是不需要关心值是什么,只需要关心键值即可。

我们再来看下和channel组合使用的例子。在etcd项目中,就有通过往channel中写入一个空结构体作为信号的,源码位于/etcd/server/auth/simple_token.go中,如下:

func (tm *simpleTokenTTLKeeper) stop() {
    select {
    case tm.stopc <- struct{}{}:
    case <-tm.donec:
    }
    <-tm.donec
}

还有一种是基于缓冲channel实现并发限速。如下:

var limit = make(chan struct{}, 3)


func main() {
    // …………
    for _, w := range work {
        go func() {
            limit <- struct{}{}
            w()
            <-limit
        }()
    }
    // …………
}

4 总结

空结构体是一种不包含任何字段的结构体类型,不仅具有结构体类型的一切属性,而且该结构体类型占用的空间为0。常被用于map的集合或和通道配合使用发送信号使用的场景。

参考链接:

https://blog.haohtml.com/archives/20339

https://ijayer.github.io/post/tech/code/golang/20200419_emtpy_struct_in_go/

责任编辑:武晓燕 来源: 大话开源
相关推荐

2021-11-02 12:19:18

Go函数结构

2020-12-02 09:10:22

Go结构数据类型

2023-07-29 15:03:29

2021-04-20 09:00:48

Go 语言结构体type

2021-11-02 14:54:41

Go结构体标签

2024-08-14 18:18:47

2023-11-21 08:03:43

语言架构偏移量

2020-12-02 08:45:36

Go语言

2020-11-30 06:17:03

Go语言

2020-11-26 06:40:24

Go语言基础

2020-11-23 08:54:14

Go语言结构体

2022-10-24 00:48:58

Go语言errgroup

2021-12-20 07:59:07

Go语言结构体

2009-08-13 14:24:44

C#结构体构造函数

2020-08-10 15:24:05

Snowflake算法开源

2022-05-06 09:22:25

Go泛型

2022-07-06 08:30:36

vuereactvdom

2024-01-02 10:54:07

Rust结构体元组

2011-04-11 13:00:08

C++结构体枚举

2020-07-21 15:20:20

语言结构体共用体
点赞
收藏

51CTO技术栈公众号