Go中结构体的性能 | 青训营笔记

98 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第二篇笔记

前言:

Go语言中的结构体可以说是十分强大的数据结构,可以用来实现oop的思想,而Go中的结构体使用也是十分的讲究,接下来我们一探究竟。

空结构体及其作用

在Go中,一个有趣的现象是空结构体竟然不占用内存。我们可以通过unsafe.Sizeof()来获取一个数据需要占用的字节数。

fmt.Println(unsafe.Sizeof(struct{}{}))

注意:struct{}表示struct类型。struct{}{}表示struct类型的值。

打印的结果是0,说明了空结构体确实不占用内存。

那么有哪些作用呢?

  • 节省资源。
  • 仅仅作为占位符。

实现Set

在Go中的是没有Set这种数据结构的,因此我们可以借助map来实现Set。

如何定义好呢?

  • 在使用map作为集合使用时,可以将 值类型定义为空结构体,使其作为占位符使用。

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{}{}
}

func (s Set) Delete(key string) {
	delete(s, key)
}

func main() {
	s := make(Set)
	s.Add("Tom")
	s.Add("Sam")
	fmt.Println(s.Has("Tom"))
	fmt.Println(s.Has("Jack"))
}

数据通道占位符

如果子协程不需要发送任何数据,可以使用空结构体代替。

只有方法的结构体

如果结构体只包含方法而没有字段,实际上可以用任何的数据结构替代。 例如可以用 type Person bool 替代 type Person struct{},但是会浪费额外的内存。


结构体的内存对齐

在Go中,会有多种数据类型,例如int8,int16,int32,int64等等。每一种类型占用的字节数不一样,我们来看下面的一个结构体:

type S1 struct{
    num1 int
    num2 int
}

type S2 struct {
    num1 int8 
    num2 int32
    
}

func main(){
    fmt.Println(unsafe.Sizeof(S1{}))
    fmt.Println(unsafe.Sizeof(S2{}))
}

打印的结果分别时16和8 第一个16是因为我的电脑是在64位机器上,int默认占8个字节,所以总共是16个字节。 第二个打印8是因为内存对齐。

那么接下来我们就来聊聊Go中的结构体是如何实现内存对齐的。

CPU在访问内存的时候实际上是不会一个一个字节去读取和写入内存。相反CPU读取内存是一块一块读取的,块的大小就分为2、4、6、8、16等。上述对于结构体S2的访问,读取num1的时候,首先会以块为2大小进行访问,然后读取num2的时候,以块为4大小进行访问和写入,也就是4的整数倍来访问和写入,也就是在4开始的位置写入num2。

在Go中,我们可以通过unsafe.Alignof来表示结构体的对齐倍数(可以理解为最大块的倍数)

fmt.Println(unsafe.Alignof(S1{}))
fmt.Println(unsafe.Alignof(S2{}))

分别打印了8和4

理解了内存对齐,接下来我们再来看一个例子:

type test1 struct{
    x int8
    y int16
    z int32
}
type test2 struct{
    x int8
    y int32
    z int16
}

上述两个结构体的内存大小竟然不一样,分别是8和12,聪明的你应该知道是为什么了吧!

所以,在声明结构体的时候可以合理的安排字段的位置来减少占用的内存。

需要注意的是如果结构体中包含空结构体,那么它是不占用内存的,也就是不需要内存对齐,但如果是在最后一个字段,那么需要额外的内存保证安全性。