Go语言之结构体与方法

注意!!!结构体如果需要导出那么首字母应大写,字段名也要大写,如果多个字母采用驼峰表示法切记!!!!

什么是结构体?

结构体是一系列具有指定数据类型的数据字段,它能够让您通过单个变量引用一系列相关的值,通过使用结构体,可在单个变量中存储众多类型不同的数据字段,存储在结构体中的值可以轻松的访问和修改,这提供了一种灵活的数据结构创建方式,通过使用结构体,可提高模块化程度,能够创建并传递复杂的数据结构

Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型。

Go 语言中的类型可以被实例化,使用new 或“&”构造的类型实例的类型是类型的指针。

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:

  • 字段拥有自己的类型和值。
  • 字段名必须唯一。
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

一、定义结构体

Go 语言的关键字type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过type 定义为自定义类型后,使结构体更便于使用。

结构体的定义格式如下:

type 类型名 struct {        //注意:类型名首字母大写,多个字母采用驼峰表示法
字段l 字段l的类型           //字段名首字母大写
字段2 字段2的类型
.......
}
  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • struct{}:表示结构体类型, type 类型名struct{}可以理解为将struct{}结构体定义为类型名的类型。
  • 字段1 、字段2……:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段l 类型、字段2 类型……:表示结构体字段的类型。

例如:声明一个结构体,如下:

type test struct{
    x int
    y  int
}
#定义test类型的结构体
#同类型的变量也可以写在一行 上面可以写成x, y int 注意中间用逗号分割

例如:下面声明并创建一个简单的结构体,如图:

上图中,注意赋值后的成员变量后用逗号分隔。最后一个数据字段所在的行也必须以逗号结尾

代码指定结果如下:

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)

二、实例化结构体:

结构体的定义只是一种内存布局的描述,只有当结构体实例化时, 才会真正地分配内存。因此必须在定义结构体并实例化后才能使用结构体的字段。

实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。

Go 语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。

1、基本的实例化形式

结构体本身是一种类型,可以像整型、字符串等类型一样,以var 的方式声明结构体即可完成实例化。

var abc test

  • 其中abc为结构体的实例,可以自定义
  • test表示结构体的类型,在声明结构体时指定

通过var abc test创建一个结构体实例,并且将各个数据字段设置为相应数据类型的零值,如图:

从上图中可以看出,实例化结构体后,成员变量的值都是零值(默认值),字符串类型默认为为空字符串,整数默认值为0

下面的例子表示创建了结构体并实例化后,通过”点表示法”给字段赋值,如图:

注意:结构体在主函数外定义,但是实例化操作需要在主函数(main)内部进行

结构体数据字段的值是可变的,这意味这可动态修改它们,然而,一旦结构体被声明或者实例被创建,就不能在修改其字段的数据类型了,否则将引发编译错误

2、创建指针类型的结构体

Go 语言中,还可以使用new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

使用new 的格式如下:

ins : = new (T)   //为其分配内存

说明:

  • T 为类型,可以是结构体、整型、字符串等。
  • ins: T 类型被实例化后保存到ins 变量中, ins 的类型为*T, 属于指针。

Go 语言让我们可以像访问普通结构体一样使用”.”访问结构体指针的成员。

例如:下面例子通过new实例化结构体并赋值,如图:

经过new 实例化的结构体实例在成员赋值上与基本实例化的写法一致(通过点表示法)。

代码执行结果如下:

3、取结构体的地址实例化

在Go 语言中,对结构体进行“&”取地址操作时,视为对该类型进行一次new 的实例化操作。取地址格式如下:

ins : = &T {}

说明:

  • T 表示结构体类型。
  • ins 为结构体的实例,类型为*T ,是指针类型。

下图表示通过&实例化结构体,如图:

代码执行结果如下:

从结果可以看出,cmd.c打印的为内存地址,如果打印值改成*cmd.c,那么结果将是100 ,通过*将获取本身的值而不是内存地址

初始化结构体的成员变量

结构体在实例化时可以直接对成员变量进行初始化。初始化有两种形式: 一种是字段“键值对”形式及多个值的列表形式,键值对形式的初始化适合选择性填充字段较多的结构体;多个值的列表形式适合填充字段较少的结构体。

一、使用“键值对”初始化结构体

结构体可以使用“ 键值对” ( Key value pair )初始化字段, 每个“键” ( Key )对应结构体中的一个字段。键的“值”( Value )对应字段需要初始化的值。

键值对的填充是可变的,不需要初始化的字段可以不填入初始化列表中。

结构体实例化后字段的默认值是字段类型的默认值,例如: 数值为0 ,字符串为空字符串,布尔为false ,指针为nil 等。

1、键值对初始化结构体的书写格式:

ins :=结构体类型名{
    字段1 : 字段1 的值,
    字段2 : 字段2 的值,
    ........
}
  • 结构体类型: 定义结构体时的类型名称。
  • 字段1 、字段2 :结构体的成员字段名。结构体类型名的字段初始化列表中,字段名只能出现一次。
  • 字段1 的值、字段2 的值: 结构体成员字段的初始值。

注意:键值之间以分号”:” 分隔;键值对之间以逗号“,”分隔。

2、使用键值对填充结构体的例子:

下面例子中描述了家里的人物关联。正如儿歌里唱的: “爸爸的爸爸是爷爷”,人物之间可以使用多级的child 来描述和建立关联。使用键值对形式填充结构体的代码如下:

  • 定义People 结构体
  • name 表示结构体的成员变量以及类型
  • child表示类型为指针类型,类型为*People
  • relation 由Peopl e 类型取地址后, 形成类型为*People 的实例。
  • child 在初始化时, 需要*People类型的值。使用取地址初始化一个People

结构体成员中只能包含结构体的指针类型, 包含非指针类型会引起编译错误。

二、使用多个值的列表初始化结构体

Go 语言可以在“ 键值对” 初始化的基础上忽略“ 键“,可以通过多个值的列表初始化结构体的字段(出于可维护性考虑,不推荐这样做)

1、多个值列表初始化结构体的书写格式:

ins := 结构体类型名{
    字段l 的值,
    字段2 的值,
    ......
}

使用这种格式初始化时, 需要注意:

  • 必须初始化结构体的所有字段
  • 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致
  • 键值对与值列表的初始化形式不能混用

2、多个值列表初始化结构体的例子:

注意:上图中实例化结构体后,直接赋值即可,成员变量会自动匹配出来,注意每个值后面的逗号,最后一个值也要加逗号

代码执行结果如下:

三、初始化匿名结构体

匿名结构体没有类型名称, 无须通过type 关键字定义就可以直接使用

1、使用短变量方式定义匿名结构体

匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成。结构体定义时没有结构体类型名,只有字段和类型定义。键值对初始化部分由可选的多个键值对组成,如下格式所示:

ins := struct {
    字段1 字段类型l     //匿名结构体字段定义
    字段2 字段类型2
    .......
}{
    初始化字段1 : 字段l 的值,//字段值初始化,注意后面逗号
    初始化字段2 : 字段2 的值,
    ......
}
  • 字段1、字段2 ……:结构体定义的字段名
  • 初始化字段1 、初始化字段2 ……:结构体初始化时的字段名,可选择性地对字段初始化
  • 字段类型1 、字段类型2 ……: 结构体定义字段的类型
  • 字段1的值、字段2 的值……:结构体初始化字段的初始值。

键值对初始化部分是可选的,不初始化成员时,匿名结构体的格式变为:

ins := struct {
     字段1 字段类型1
     字段2 字段类型2
     ....
}{ }

2、使用var关键字来定义匿名结构体

使用这种方式定义匿名结构体,需要先声明表示结构体的变量,再通过该变量为结构体中的成员赋值

3、使用匿名结构体的例子

在本例中, 使用匿名结构体的方式定义和初始化一个消息结构,这个消息结构具有消息标识部分(ID)和数据部分( data )。打印消息内容的printMsg() 函数在接收匿名结构体时需要在参数上重新定义匿名结构体,代码如下:

代码执行结果如下:

说明:

  • 定义printMsg()函数,参数为msg ,类型为*struct { id int data string },因为类型没有使用type 定义,所以需要在用到的地方每次进行定义。
  • 对匿名结构体进行实例化,同时初始化成员
  • 定义匿名结构体的字段
  • 给匿名结构体字段赋予初始值
  • 将msg 传入printMsgType()函数中进行函数调用。

匿名结构体的类型名是结构体包含字段成员的详细描述。医名结构体在使用时需要重新定义, 造成大量重复的代码, 因此开发中较少使用。

3、匿名成员

在定义结构体时,只定义数据类型,没有定义名称的成员是匿名成员。在实例化结构体时,可以通过成员的数据类型访问该成员,如图:

注:在结构体中,匿名成员的数据类型只能是int、string、float或bool等基本数据类型,不能是数组、切片或集合等复合数据类型

四、比较结构体:

对结构体进行比较,要先看他们的类型和值是否相同,对于类型相同的结构体,可使用相等性运算符来比较,判断两个结构体是否相等,可使用==,判断他们是否不相等,可使用!=,如图:

上图中,实例化后的a和b类型和成员变量的值都是相同,因此比价结果将返回true,如图:

现在将实例化后的b变量的成员变量weight改为200kg,如图:

代码执行结果为false,如下:

五、结构体嵌套

1、下面例子将gong包中的three结构体嵌套到One中,如图:

上面One中添加了一个字段QT,字段的类型为three,即为three结构体,此时的QT首字母需要大写,three可以小写,因为要先访问QT在访问three

定义main函数,导入gong包,实例化结构体One,如图:

上图中实例化结构体One后的变量给结构体three中的Gender赋值,需要先通过点运算符定位QT,然后在通过点运算符给字段Gender赋值

运行结果如下:

上面的方法稍微有些麻烦,因为还要通过QT来进行赋值和获取值,其实可以直接将three内嵌,不用指定字段名,直接用类型就行,如图:

在实例化后,直接通过字段Gender来赋值和打印值即可,如图:

注意:也可以通过one.Three.Gender来调用,此时gong包中的three结构体就要写成Three,首字母大写才可以导出

构造函数:结构体和类型的一系列初始化操作的函数封装

Go 语言的类型或结构体没有构造函数的功能。结构体的初始化过程可以使用函数封装实现。

因为结构体(struct)是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型

1、带参数的构造函数,如下:

使用结构体描述猫的特性,那么根据猫的颜色和名字可以有不同种类的猫。那么不同的颜色和名字就是结构体的字段,同时可以使用颜色和名字构造不同种类的猫的实例, 这个过程可以参考下面的代码:

上图中的cat实例化方式为取结构体地址实例化,返回类型为指针类型,通过星号(*)来取实际的值

代码执行结果如下:

2、不带参数的构造函数,结构体成员值在函数内定义,如下:

3、带有父子关系的结构体的构造和初始化一一模拟父级构造调用

黑猫是一种猫, 猫是黑猫的一种泛称。同时描述这两种概念时, 就是派生,黑猫派生自猫的种类。使用结构体描述猫和黑猫的关系时,将猫(cat)的结构体嵌入到黑猫(blackcat)中,表示黑猫拥有猫的特性,然后再使用两个不同的构造函数分别构造出黑猫和猫两个结构体实例, 参考下面的代码:

说明:

  • BlackCat 结构中嵌入了Cat 结构体。BlackCat 拥有Cat 的所有成员,实例化后可以自由访问Cat 的所有成员。
  • NewCat()函数定义了Cat 的构造过程, 使用名字作为参数,填充Cat 结构体
  • NewBlackCat()使用color 作为参数,构造返回BlackCat 指针
  • 实例化BlackCat 结构,此时Cat 也同时被实例化。
  • 填充BlackCat 中嵌入的Cat 颜色属性。BlackCat 没有任何成员,所有的成员都来自于Cat

这个例子中, Cat 结构体类似于面向对象中的“基类”。BlackCat 嵌入Cat 结构体,类似于面向对象中的“派生” 。实例化时, BlackCat 中的Cat 也会一并被实例化

总之, Go 语言中没有提供构造函数相关的特殊机制,用户根据自己的需求,将参数使用函数传递到结构体构造参数中即可完成构造函数的任务

3、通过构造函数来自定义成员变量的默认值,如图:

代码执行结果如下:

区分指针引用和值引用

数据值存储在计算机内存中,指针包含值的内存地址,这意味着使用指针可读写存储的值

复制结构体时,明确内存方面的差别很重要,将指向结构体的变量赋给另一个变量时,被称为赋值

下面的例子表示以值引用的方式复制结构体,如图:

从上图中可以看出,b变量是a变量的副本,通过b变量修改其中成员变量的值,对原始结构体实例a中的成员变量的值无影响,最后打印两个变量的内存地址,可以看到内存地址也是不同的

如果要修改原始结构体实例包含的值,必须要使用指针,指针是指向内存地址的引用,因此使用它操作的不是结构体的副本而是其本身,要获得指针,可在变量前加上和(&)号。

下面例子是对上面例子的修改,通过指针的方式复制结构体,如图:

从上图中可以看出,将指针赋值给b,通过b修改成员变量的值,会将a中的值一起修改,因为他们指向的是同一个内存地址,因此通过指针的方式复制结构体,会修改原结构体实例包含的值。

总结:如果要修改原结构体实例,就使用指针,如果要操作一个结构体,但不想修改原结构体实例,那么就使用值

方法

Go 语言中的方法( Method ) 是一种作用于特定类型变量的函数。这种特定类型变量叫做接收器( Receiver )

如果将特定类型理解为结构体或“类”时,接收器的概念就类似于其他语言中的this 或者self

在Go 语言中,接收器的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法

注意:在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在Go 语言中“方法”的概念与其他语言一致,只是Go 语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。

一、为结构体添加方法

本节中,将会使用背包作为“对象”,将物品放入背包的过程作为“方法”。

将背包及放入背包的物品中使用Go 语言的结构体和方法方式编写:为*Bag 创建一个方法,代码如下:

  • Insert(itmeid int)的写法与函数一致。( a *Bag) 表示接收器,即Insert 作用对象
  • append()方法表示向切片中添加元素,第一个元素表示切片,第二个元素表示要添加的值,注意等号前面接受值a.items表示当前切片

注意:上图中结构体通过new方式实例化,实例化后的类型为指针类型,因此上面的接收器类型为指针类型

每个方法只能有一个接收器,如图:

注意:结构体方法只能由创建的结构体实例化变量进行调用

二、接收器——-方法作用的目标

接收器的格式如下:

func (接收器变量  接收器类型)方法名(参数列表) (返回参数) {
    函数体
}
  • 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是self、this 之类的命名。例如, Socket 类型的接收器变量应该命名为s, Connector 类型的接收器变量应该命名为c 等。
  • 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:格式与函数定义一致。

根据接收器的类型可以分为指针接收器、非指针接收器,两种接收器在使用时会产生不同的效果。根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。

1、理解指针类型的接收器

指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的this 或者self。由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。

例如:首先设置结构体成员值,然后再取值,接收器类型为指针类型,如图:

上述代码执行后将打印结果为:199

注意:上图中,取值方法的返回参数设置为int,因此需要return来返回值,如果没有设置返回参数,就不需要return,方法作用于结构体,因此获取结构体值可直接使用t.value来获取

2、理解非指针类型的接收器(值类型)

当方法作用于非指针接收器时, Go 语言会在代码运行时将接收器的值复制一份。在非指针接收器的方法中可以获取接收器的成员值,但修改后无效

下面例子表示

上面的结构体abc为非指针类型,因此修改结构体的字段值后,原结构体值不会变,打印结果如下:

从上图可以看到,调用方法修改字段值后,原结构体的值不变,如果将abc改为*abc,如图:

3、指针和非指针接收器的使用

在计算机中,小对象由于值复制时的速度较快, 所以适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制, 只是传递指针。

4、下面的例子也可以更好的理解指针类型接收器和非指针接收器的区别:

代码执行结果如下:

从上图结果可以看出,使用非指针类型的接收器(Bag),在调用方法test()后,并没有修改原结构体中成员变量的值, 只是将成员变量复制了一份然后赋值, 而使用指针类型的接收器(*Bag),在调用方法test1()后,实际修改了原结构体中的成员变量的值

方法作用域接收器,可以将接收器看成方法的参数

什么时候应该使用指针类型接收者?

  • 需要修改接收者中的值
  • 接收者是拷贝代价比较大的大对象
  • 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者

函数和方法的区别:

  • 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
  • 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
  • 函数是直接通过函数名调用即可,方法需要通过 变量名.方法()调用,例如上图中的a.test()
  • 同名的方法可以定义在不同的类型上(方法名可以重复,只要接收器不同),但是函数名不允许相同

三、为类型添加方法:

Go 语言可以对任何类型添加方法。给一种类型添加方法就像给结构体添加方法一样,因为结构体也是一种类型。

1、为基本类型添加方法

在Go 语言中, 使用type 关键字可以定义出新的自定义类型。之后就可以为自定义类型添加各种方法。我们习惯于使用面向过程的方式判断一个值是否为0 , 例如:

if v == 0 {
    v的值为0
}

如果将v 当做整型对象,那么判断v 值就可以增加一个lsZero() 方法,通过这个方法就可以判断v 值是否为0 ,例如:

  • 使用type Mylnt int 将int 定义为自定义的My Int 类型。
  • 为Myint 类型添加IsZero()方法。该方法使用了( m Myint )的非指针接收器,数值类型没有必要使用指针接收器
  • 为MyInt 类型添加Add()方法
  • 由于m 的类型是MyInt 类型,但其本身是int 类型,因此可以将m 从MyInt类型转换为int 类型再进行计算。
  • 调用b 的IsZero()方法。由于使用非指针接收器, b 的值会被复制进入IsZero()方法进行判断。
  • 调用b 的Add()方法。同样也是非指针接收器, 结果直接通过Add()方法返回

代码执行结果如下:

注意:结构体的方法不一定非要跟结构体在一个go文件中,可以在同一个包的不同文件中

四、使用事件系统实现事件的晌应和处理

Go 语言可以将类型的方法与普通函数视为一个概念,从而简化方法和函数混合作为回调类型时的复杂性。这个特性和C#中的代理( de legate )类似, 调用者无须关心谁来支持调用, 系统会自动处理是否调用普通函数或类型的方法。

1、方法和函数的统一调用

本节的例子将让一个结构体的方法(class .Do)的参数和一个普通函数(funcDo)的参数完全一致, 也就是方法与函数的签名一致,然后使用与它们签名一致的函数变量分别赋值方法与函数,接着调用它们,观察实际效果,如图:

这段代码能运行的基础在于: 无论是普通函数还是结构体的方法, 只要它们的签名一致,与它们签名一致的函数变量就可以保存普通函数或是结构体方法。

类型内嵌和结构体内嵌

结构体允许其成员字段在声明时没有字段名而只有类型,这种形式的字段被称为类型内嵌或匿名字段

类型内嵌的写法如下:

type Data struct {
    int
    float32
    bool
}
ins := &Data{
    int: 10,
    float32 : 3.14,
    bool : true
}
  • 定义结构体中的匿名字段,类型分别是整型、浮点、布尔。
  • 将实例化的Data 中的字段赋初值。

类型内嵌其实仍然拥有自己的字段名,只是字段名就是其类型本身而己,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

结构体实例化后,如果匿名的字段类型为结构体,那么可以直接访问匿名结构体里的所有成员,这种方式被称为结构体内嵌。

一、声明结构体内嵌

结构体类型内嵌比普通类型内嵌的概念复杂一些,下面通过一个实例来理解。

例如:计算机图形学中的颜色有两种类型,一种是包含红、绿、蓝三原色的基础颜色;另一种是在基础颜色之外增加透明度的颜色。透明度在颜色中叫Alpha ,范围为0 ~ 1 之间。0表示完全透明, 1表示不透明。使用传统的结构体字段的方法定义基础颜色和带有透明度颜色的过程代码如下:

代码执行结果如下:

上图中实例化后需要通过Basic 结构才能设置R 、G 、B 分量,虽然合理但是写法很复杂。使用Go 语言的结构体内嵌写法重新调整代码如下:

上图中,将BasicColor结构体嵌入到Color结构体中,BasicColor没有名字只有字段类型,下面直接通过c.R、c.G、c.B设置成员值即可

二、结构内嵌特性

Go 语言的结构体内嵌有如下特性:

1、内嵌的结构体可以直接访问其成员变量(外层结构体可直接访问内嵌结构体的成员变量)

嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如, ins.a.b.c 的访问可以简化为ins.c

2、内嵌结构体的字段名是它的类型名

内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名。

一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。

3、初始化结构体内嵌

结构体内嵌初始化时,将结构体内嵌的类型作为字段名像普通结构体一样进行初始化,如图:

4、初始化内嵌匿名结构体

在前面描述车辆和引擎的例子中,有时考虑编写代码的便利性,会将结构体直接定义在嵌入的结构体中。也就是说,结构体的定义不会被外部引用到。在初始化这个被嵌入的结构体时,就需要再次声明结构才能赋予数据,如图:

代码执行结果如下:

  • Engine 结构体被直接定义在Car 的结构体中。这种嵌入的写法就是将原来的结构体类型转换为struct{ … }

5、成员名字冲突:

嵌入结构体内部可能拥有相同的成员名,成员重名时会发生什么?如图:

上图中,通过c.A.a进行赋值,然后打印即可正常输出,如果直接通过c.a进行赋值,将会报错,因此编译器无法决定将值赋值给A中的值还是B中的值

在使用内嵌结构体时, Go 语言的编译器会非常智能地提醒我们可能发生的歧义和错误。

结构体struct内存

struct所占的内存空间不一定等于各个字段加起来的空间之和,甚至有时候把字段的顺序调整一下,所占的空间就会不同

1、字段顺序不同,所占用内存空间大小不同,通过unsafe.Sizeof方法可以获取内存大小,如图:

运行结果如下:

从上图看出,结构体abc占用16个字节,顺序变化后,所占用的字节数变为24,性能受到影响

内存对齐:

不同类型的变量占用内存大小是不一样的,但是cpu每次读取的内存长度是固定的,例如64位的CPU,每次读取8个字节,即64位的数据,32位的CPU,每次读取4个字节,即332位数据,为了提高CPU读写效率,编译器会将各种类型的数据放在合适的位置,并占用合适的长度,而不是按照顺序一个接一个存放,这就是内存对齐,每种类型的对齐值就是它的对齐边界

如果没有内存对齐,那么结构体中各个字段在内存中是紧密排列的,例如结构体def内存布局如下:

从上图看出,a占用1个字节,b占用8个字节,但是第一列已经满了,因此会有一个字节的数据排列到第二列中,如果要获取b字段的数据,CPU需要读取两次才能获取到完整的b数据,此时影响了程序的性能,为了减少CPU获取的时间,Go编译器会把结构体做数据对齐,如图:

从上图看出,一共占用24个字节,其中灰色的11个字节是为了对齐而填充的,不存储任何数据,通过对齐的方式确实提高了CPU的读写效率,但是又浪费了很多内存空间,更好的办法就是调整struct字段的顺序,既可以保证对齐保证效率又不浪费空间

调整顺序后的结构体为abc,abc的内存布局如图:

从上图看出,重新排序后,只占用了16个字节,虽然也浪费了3个字节的空间,但是整体上比之前上了8个字节,性能得到提高

总结:

  • 结构体是占用一块连续的内存,一个结构体变量的大小是由结构体中的字段决定
  • unsafe.Sizeof(x) 返回了变量x的内存占用大小
  • 两个结构体,即使包含变量类型的数量相同,但是位置不同,占用的内存大小也不同,由此引出了内存对齐
  • 对结构体字段的重新排列会让结构体更节省内

标签