写在文章开头
这篇文章会通过unsafe api
获取go语言
不同类型大小,深入底层分析go语言
中如何完成空值内存分配和空值的应用场景。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。
获取类型的大小
我们以整数类型为例,通过unsafe
包的Sizeof
函数方法即可得到当前整数类型字节数,关于这个函数学习过C语言
的同学都知道C语言
中的sizeof
方法,而unsafe
的Sizeof
本质就是对该方法的一层封装:
css
num := 1
fmt.Println("num size:", unsafe.Sizeof(num))
对应输出结果如下,因为笔者使用的操作系统为64位,而整数类型和字长是和操作系统位数保持一致,所以num
的字节数为8字节(64bit):
arduino
num size: 8
同理我们这里给出一个结构体并打印其大小:
go
type people struct {
id int
name string
}
func main() {
p := people{1, "xiaoming"}
fmt.Println("people size:", unsafe.Sizeof(p))
}
输出结果为24
:
arduino
people size: 24
其原因是字符串变量name
底层有指向字符的指针和记录容量大小的int,二者都是8字节,再加上int的8字节总共24字节:
指针类型的长度
同理我们再查看不同的指针类型的长度:
csharp
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字节
:
arduino
num ptr size: 8
float ptr size: 8
原因也是因为笔者操作系统为64位
的缘故,指针指向的地址永远保持在64位
以内:
空基本类型和空结构体的大小
我们再来看一些有意思的,假如我们声明的基本类型没有赋值,那么这个变量占用的内存空间是多少呢?
csharp
func main() {
var num int
fmt.Println("num size:", unsafe.Sizeof(num))
}
还是8个字节,很明显go语言对于基本类型的内存分配如论是否赋值对应内存分配的大小都是固定的:
arduino
num size: 8
那要是一个空结构体呢?
csharp
type emptyObj struct {
}
func main() {
var e emptyObj
fmt.Println("emptyObj size:", unsafe.Sizeof(e))
}
输出结果为0,不难猜出因为自定义类型的原因,go语言在编译期会检查该类型是否有内置变量从而动态分配内存大小:
arduino
emptyObj size: 0
go语言空值更进一步的理解
那么问题来了,既然空结构体的大小为0,如果考虑到空结构体0大小的特点,我们是否可以认为如果这些空结构体存在地址时,它们的地址都是一样的呢?
对此我们给出下面这段代码:
go
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
的指针地址,这样做是得所有空结构体都复用一个指针,确保所有的空结构体都能用同一块内存使用以节约宝贵的内存:
这一点我们可以在源码中得以印证:
csharp
// 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
创建一个string
为key
,接口为value
的变量,赋值时将需要记录的value
作为map
的key
,而value
全部用空结构体让这些结构体都是用zerobase
以避免没必要的内存空间占用:
c
set := map[string]interface{}{}
set["key1"] = struct{}{}
set["key2"] = struct{}{}
同理go语言
在进行协程并发工作时常用到channel
发送信号,有时候我们只是为了发送信号,信号没有任何含义,为了节约宝贵的内存,我们同样可以使用空结构体作为channel
以确保在可以发送信号的情况下节约内存空间:
go
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语言为了压榨服务器的性能在内存分配方面也是做到极致!
我是 sharkchili ,CSDN Java 领域博客专家 ,开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。