Go中所说的空结构体就是struct{},它是一种特殊的存在,可能你在项目中看到过,但并没有深入的了解它的应用场景,这里结合自己平时项目中的经验,介绍一下空结构体(struct{})的一些应用场景。

Go语言中的空结构体(struct{})是一种零内存占用的特殊类型,其所有实例可能共享同一内存地址(zerobase),它不包含任何字段,但却有很多应用场景。

紧接上文,这里就来验证一下,空结构体的内存地址是否相同,以及内存占用大小:

package main

import (
    "fmt"
    "unsafe"
)

type Empty struct{}

func main() {
    var e Empty
    var s struct{}

    fmt.Printf("e addr: %p, size: %d\n", &e, unsafe.Sizeof(e)) // output: e addr: 0x5800a0, size: 0
    fmt.Printf("s addr: %p, size: %d\n", &s, unsafe.Sizeof(s)) // output: s addr: 0x5800a0, size: 0
}

可以看出,空结构体占用内存的字节数是0,所以说它不占内存空间。

再者,两个空结构体的内存地址是相同的,但是不是所有空结构体的内存地址都是相同的呢?不一定

Go语言规范中有这么一段话:

A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

意思就是说,如果结构体或数组类型不包含大小大于零的字段(或元素),则其大小为零。两个不同的零大小变量可能在内存中具有相同的地址。

注意,这里说是可能,也就是说也可能不相同,如果发生了内存逃逸,那么它们的地址就不一定相同。

下面就列举一些空结构体的一些应用实践。

标记或计数

空结构体可以用来标记一个元素是否存在,或者记录出现次数,或者去重等。

func ReDupUint64(ui64s []uint64) []uint64 {
    var rets []uint64
    m := make(map[uint64]struct{})
    for _, ui64 := range ui64s {
        if _, ok := m[ui64]; ok {
            continue
        }
        rets = append(rets, ui64)
        m[ui64] = struct{}{}
    }

    return rets
}

这是一个用来标记切片中已经存在元素并去重的用法,因为空结构体不占用空间,用来作为map的值来占位非常合适。

实现轻量级集合(Set)

我们都知道,在Go语言中没有集合(Set)类型,但我们使用map来自定义一个集合,map的 key 是集合的元素,能保证唯一,map的 value 就使用空结构体(struct{})来占位(map[string]struct{}),避免使用bool存储,从而达到节省内存的作用。

type Set map[string]struct{}

func (s Set) Has(key string) bool {
    _, ok := s[key]
    return ok
}

func (s Set) Add(key string) {
    s[key] = struct{}{}
}

通道信号通知

在并发编程中,通过空结构体(struct{})通道传递信号,因为仅仅是传递信号而已,不需要关注数据内容,使用空结构体占位也能起到节省内存的作用。

done := make(chan struct{})
go func() {
    // 执行任务
    done <- struct{}{} // 发送完成信号
}()
<-done // 阻塞等待信号

无状态方法接收器

当结构体不需要定义任何字段,用它来实现接口时,只需要定义接收器即可。

type Logger struct{} // 空结构体

func (l Logger) Log(msg string) {
    fmt.Println(msg) // 不依赖实例状态
}

// 调用时零内存开销
logger := Logger{}
logger.Log("info")

优化结构体内存布局

在之前的文章中,有说到空结构体作为结构体字段置于末尾时会产生内存对齐填充,那么我们可以前置空结构体字段,不占用空间。

type A struct {
    _ struct{} // 零大小
    n int64    // 偏移量0
}

最后

空结构体在 Go 语言中,看似不起眼的东西,但它的用途不可小觑。

在项目中,合理且巧妙的应用空结构体,从而达到内存空间的零占用,但也不要过度用空结构体而降低了代码的可读性。