Golang -- 结构体

结构体定义与内存布局

1.1定义、引言

Golang中的结构体是一种复合数据类型,用于将不同类型的数据组合在一起。
例如一个学生的学号、姓名、性别

type Student struct{
  id int
  name,sex string
}

我们可以通过unsafe.Sizeof(x) 来确定一个变量占用的内存字节数
对于64位机器,占用内存大小

类型字节数
bool1
intN,uintN,floatNN/8((eg:int32是4个字节)
int,uint8
指针8
map,func8
string16
切片24
	fmt.Println(unsafe.Sizeof(int(0)))           //8
	fmt.Println(unsafe.Sizeof(uintptr(1)))       //8
	fmt.Println(unsafe.Sizeof([]int{}))          //24
	fmt.Println(unsafe.Sizeof(map[string]int{})) //8
	fmt.Println(unsafe.Sizeof(string("")))       //16
	fmt.Println(unsafe.Sizeof(bool(true)))       //1

计算一个结构体占用内存的大小

type Example struct {
	a bool
	b int
	c string
}

func main() {
	fmt.Println(unsafe.Sizeof(Example{})) //32
}

将每个字段的字节数相加,得到 1 + 8 + 16 = 25 个字节
但是最终编译器输出32字节
这表明计算 结构体占用内存大小 并不是仅仅将结构体中每个字段的字节数相加得到的
而一定遵循某种规律

实际上,这是因为 在内存布局方面,Golang存在内存对齐机制。内存对齐是为了提高CPU访问内存的效率,确保数据存储在核实的内存地址上。


1.2什么是内存对齐

要在计算机中访问一个变量,需要访问它的内存地址
从理论上讲似乎对任何类型的变量的访问可以从任何地址开始
但是实际情况是:在访问特定类型变量的时候 通常在特定的内存地址访问,这就需要对这些数据在内存中存放的位置进行限制
各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个地排列,这就是对齐


1.3为什么需要内存对齐

  1. 有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因为不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可移植性了(移植性就是可以跨平台编译)
  2. CPU访问内存时,并不是逐个字节进行访问,而是以字长为单位访问(例如以4为字长,那就是访问地址为0、4、8…的位置),例如64位的CPU字长是8字节,32位的CPU字长是4字节。
    如果变量的地址没有对齐,可能需要多次访问才能完整读取到变量内容,而对齐后可能就只需要一次内存访问,因此内存对齐可以减少CPU访问内存的次数

1.4内存对齐规则

64位机器最大对齐数为8,32位机器最大对齐数是4

对齐的规则:

  1. 占用空间的地址从0开始
  2. 变量占用内存空间是m,取对齐数 k 为m与最大对齐数中的较小值,那么该变量的起始地址是k的倍数,向后数m个就是该变量占用的内存空间
  3. 对每个成员进行对齐后,占用的内存空间必须是最大对齐数的整数倍,如果不是,那么需要在后面填充空白字节以达到整数倍

还是上面的 Example 结构体,实际上它在内存中是这样存储的

  1. 对于a(bool),bool是1字节,小于最大对齐数,所以bool对齐数是1,起始地址是0,占用一个空间
  2. 对于b(int),int是8字节,等于最大对齐数,所以 int 对齐数是8,起始地址要从8的倍数开始,向下找,找到8,从8开始向下数8个
  3. 对于c(string),string是16字节,大于最大对齐数,所以string对齐数是8,起始地址要从8的倍数开始,向下找到16,从16开始向下数16个,到31
  4. 32是8的整数倍,所以占用内存字节数是32
    在这里插入图片描述

1.5对齐数函数unsafe.Alignof()

unsafe.Alignof(x)的返回值是m,当变量进行内存对齐时,需要保证分配到x的内存地址能够整除m。实际上这里的m就是上面提到的对齐数
对于64位的机器

	fmt.Println(unsafe.Alignof(bool(true))) //1-----min(1,8)
	fmt.Println(unsafe.Alignof(int32(0)))   //4-----min(4,8)
	fmt.Println(unsafe.Alignof(int64(0)))   //8-----min(8,8)
	fmt.Println(unsafe.Alignof(string(""))) //8-----min(16,8)

匿名字段与嵌入类型

2.1匿名结构体

匿名结构体就是不用给结构体起名字,直接在函数中定义,只能调用一次
可以在定义时直接赋值,也可以先定义后赋值

func main() {
//在定义时直接赋值
	person := struct {
		name string
		city string
		age  int
	}{name: "小张", city: "江苏", age: 18}
	fmt.Println(person)

//先定义后赋值
	person2 := struct {
		name string
		city string
		age  int
	}{}
	person2.name = "小李"
	person2.city = "山东"
	person.age = 19

	fmt.Println(person2)
}

2.2匿名字段

匿名字段是指结构体字段中没有名字 只有类型的成员

对匿名字段赋值时,必须按照顺序,并且结构体中的字段必须是唯一的
如果要得到成员值,因为成员没有名字,所以直接访问字段就可以

// 结构体中的成员在声明中 没有名字只有类型 称为匿名字段
// 字段是唯一的
type person struct {
	string
	int
}

func main() {
	p1 := person{
		"哈哈",  //注意逗号
		17,
	}
	fmt.Println(p1)
	fmt.Println(p1.string, p1.int)
}

2.3嵌套结构体

嵌套结构体就是结构体B作为结构体A中的成员

如果结构体A匿名嵌套结构体B
要访问结构体B有两种方法
1.通过嵌套的匿名结构体字段访问其内部的字段
2.直接访问匿名结构体中的字段
对于第二种,在查询字段时,首先在结构体person中查询字段,如果没能查找到,再去匿名结构体address中查找

// 描述人的家庭住址
type address struct {
	province, city string
}

// 人的基本信息
type person struct {
	name, gender string
	age          int
	address      //匿名嵌套另外一个结构体
}

func main() {
	p1 := person{
		name:   "小王子",
		gender: "nan",
		age:    18,
		address: address{
			province: "河北",
			city:     "衡水",
		},
	}
	fmt.Println(p1) //{小王子 nan 18 {河北 衡水}}
	//通过嵌套的匿名结构体字段访问其内部的字段
	fmt.Println(p1.address.province) //河北
	//直接访问匿名结构体中的字段
	fmt.Println(p1.province) //河北
}

  • 字段冲突
    如果嵌套了多个结构体,并且多个结构体里面都有一个相同的字段
type address struct {
	province   string
	city       string
	updateTime string
}

type email struct {
	Addr       string
	updateTime string
}

type person struct {
	name    string
	age     int
	address //匿名嵌套address
	email   //匿名嵌套email
}

结构体person中嵌套了两个结构体address和email,这两个结构体都包含字段updateTime

定义一个person结构体类型的变量

   p1 := person{
		name: "小王子",
		age:  18,
		address: address{
			province:   "河北",
			city:       "衡水",
			updateTime: "0505",
		},
		email: email{
			Addr:       "21231313@qq.com",
			updateTime: "0504",
		},
	}

如果直接访问匿名结构体中的字段,编译器会报错
在这里插入图片描述

所以如果想访问重复的字段,必须通过嵌套的匿名结构体字段访问其内部的字段
否则并不知道要找的是哪一个结构体中的字段,所以必须说明是哪个结构体中的字段

   p1.address.updateTime = "0505"
   p1.email.updateTime = "0504"

2.4结构体继承

通俗来讲就是结构体A中嵌套了结构体B,结构体B满足性质x,那么结构体A也满足性质x

比如下面这两个结构体

// 动物
type Animal struct {
	Name string
}

// 小狗
type Dog struct {
	Feet   int //有feet条腿
	Animal     //匿名嵌套Animal结构体
}

// 给Animal加方法
func (a Animal) Speak() {
	fmt.Printf("%s会说话\n", a.Name)
}

// 给Dog加方法
func (d Dog) Wang() {
	fmt.Printf("%s会汪汪叫\n", d.Name) //或d.Animal.Name
}

func main() {
	d1 := Dog{
		Feet: 4,
		Animal: Animal{
			Name: "平安",
		},
	}
	d1.Wang()
	d1.Animal.Speak()
	d1.Speak()
}

结构体Dog中匿名嵌套了结构体Animal,Speak是Animal的方法,Wang是Dog的方法,对于语句

	d1.Wang()
	d1.Animal.Speak()
	d1.Speak()

都成立,表明结构体Dog继承了Animal的方法

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值