掘金 后端 ( ) • 2024-04-07 09:28

写在文章开头

这篇文章会通过unsafe api获取go语言不同类型大小,深入底层分析go语言中如何完成空值内存分配和空值的应用场景。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

获取类型的大小

我们以整数类型为例,通过unsafe包的Sizeof函数方法即可得到当前整数类型字节数,关于这个函数学习过C语言的同学都知道C语言中的sizeof方法,而unsafeSizeof本质就是对该方法的一层封装:

 num := 1
 fmt.Println("num size:", unsafe.Sizeof(num))

对应输出结果如下,因为笔者使用的操作系统为64位,而整数类型和字长是和操作系统位数保持一致,所以num的字节数为8字节(64bit):

num size: 8

同理我们这里给出一个结构体并打印其大小:

type people struct {
 id   int
 name string
}

func main() {

 p := people{1, "xiaoming"}
 fmt.Println("people size:", unsafe.Sizeof(p))
}

输出结果为24

people size: 24

其原因是字符串变量name 底层有指向字符的指针和记录容量大小的int,二者都是8字节,再加上int的8字节总共24字节:

指针类型的长度

同理我们再查看不同的指针类型的长度:

func main() {
 var n = 1
 var f = 1.0
 fmt.Println("num ptr size:", unsafe.Sizeof(&n))
 fmt.Println("float ptr size:", unsafe.Sizeof(&f))
}

从输出结果来看,对于不同的类型,go语言对应指针大小都是8字节

num ptr size: 8
float ptr size: 8

原因也是因为笔者操作系统为64位的缘故,指针指向的地址永远保持在64位以内:

空基本类型和空结构体的大小

我们再来看一些有意思的,假如我们声明的基本类型没有赋值,那么这个变量占用的内存空间是多少呢?

func main() {
 var num int
 fmt.Println("num size:", unsafe.Sizeof(num))
}

还是8个字节,很明显go语言对于基本类型的内存分配如论是否赋值对应内存分配的大小都是固定的:

num size: 8

那要是一个空结构体呢?

type emptyObj struct {
}

func main() {
 var e emptyObj
 fmt.Println("emptyObj size:", unsafe.Sizeof(e))
}

输出结果为0,不难猜出因为自定义类型的原因,go语言在编译期会检查该类型是否有内置变量从而动态分配内存大小:

emptyObj size: 0

go语言空值更进一步的理解

那么问题来了,既然空结构体的大小为0,如果考虑到空结构体0大小的特点,我们是否可以认为如果这些空结构体存在地址时,它们的地址都是一样的呢?

对此我们给出下面这段代码:

type emptyObj struct {
}

func main() {

 e := emptyObj{}
 e2 := emptyObj{}
 fmt.Printf("%p\n", &e)
 fmt.Printf("%p\n", &e2)
}

从输出结果来看,两个不同的变量创建的空结构体指向的内存地址都是一样的:

0x10ca478
0x10ca478

关于这个地址值,我们可以在内存分配的工具包malloc.go看到这样一个变量,这就是go语言中对于空结构体都采用zerobase的指针地址,这样做是得所有空结构体都复用一个指针,确保所有的空结构体都能用同一块内存使用以节约宝贵的内存:

这一点我们可以在源码中得以印证:

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



func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
 if gcphase == _GCmarktermination {
  throw("mallocgc called with gcphase == _GCmarktermination")
 }
 //如果size为0则分配zerobase的指针地址
 if size == 0 {
  return unsafe.Pointer(&zerobase)
 }
}

空结构体的应用场景

go语言为了简而精,它没有做到像Java那样定义了各种数据结构,所以为了实现hashset这样的数据结构,我们只能自行实现,了解Java的Hashset的读者都知道,Hashset本质就是对hashMap的封装,用map的key作为set的value,将value全部设置为null。

同样的利用空结构体这一特点,我们通过go语言map创建一个stringkey,接口为value的变量,赋值时将需要记录的value作为mapkey,而value全部用空结构体让这些结构体都是用zerobase以避免没必要的内存空间占用:

 set := map[string]interface{}{}
 set["key1"] = struct{}{}
 set["key2"] = struct{}{}

同理go语言在进行协程并发工作时常用到channel发送信号,有时候我们只是为了发送信号,信号没有任何含义,为了节约宝贵的内存,我们同样可以使用空结构体作为channel以确保在可以发送信号的情况下节约内存空间:

func main() {
 //创建计时门闩
 var wg sync.WaitGroup
 wg.Add(4)

 //创建空结构体
 c := make(chan interface{})

 //创建协程发送空结构体信号
 go func() {
  for i := 0; i < 2; i++ {
   time.Sleep(3000)
   c <- struct{}{}
   wg.Done()
  }

 }()

 //协程2等待空结构体信号并按下倒计时门闩
 go func() {
  for i := 0; i < 2; i++ {
   <-c
   fmt.Printf("%p\r\n", &c)
   wg.Done()
  }

 }()

 //结束并输出结果
 wg.Wait()
 fmt.Println("结束")
}

可以看到所有的空结构体信号都是用到zerobase的指针的地址:

0xc0000ca018
0xc0000ca018
结束 

小结

以上便是笔者对于go语言空值的介绍,不难看出go语言为了压榨服务器的性能在内存分配方面也是做到极致!

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。