3

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

 2 years ago
source link: https://studygolang.com/articles/35407
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

yudotyang · 2天之前 · 188 次点击 · 预计阅读时间 4 分钟 · 大约8小时之前 开始浏览    

大家好,我是「Go学堂」的渔夫子,欢迎关注Go学堂,学习更多实战应用案例。

原文地址:https://mp.weixin.qq.com/s/h8vhy8IJKnA8aNbTlCoQtg

在实际项目或开源程序中,相信大家都见过将一个空结构体作为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{}{}

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

01 什么空结构体

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

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

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

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)
    }
    ...
}

03 空结构体的应用场景

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

  • 基于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
        }()
    }
    // …………
}

04 总结

空结构体是一种不包含任何字段的结构体类型,不仅具有结构体类型的一切属性,而且该结构体类型占用的空间为0。

参考链接:

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

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


有疑问加站长微信联系(非本文作者))

280

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK