写在开头
非原创,知识搬运工
往期回顾
- 基本数据类型
- slice/map/array
带着问题去阅读
- type有哪些用法
- 空结构体有什么意义
- 结构体如何自引用
- GO有无继承和重写
- 结构体内存对齐规则是什么
1.type的用法
type关键词的三种用法
- type 名字 interface {}
- type 名字 struct{}
- type 名字 别的类型
- type 别名 = 别的类型
其中需要注意的是第三点和第四点的区别
没加等号的是产生新的类型
type man struct{}
func (m *man) construct() {
fmt.Println("MAN!")
}
type human man
func main() {
human := new(human)
human.construct() //这里报错
}
因为是两种类型,human上没有该方法,改成下面就能通过
type human = man
//或者类型强制转换,前提是内存布局相似
( (man)human ).construct
type a=b 一般只用于处理兼容性,不需要太关注
2.空结构体
2.1空接口
interface {}
&A{} //对于普通接口的实现需要用结构体指针
对于普通接口的实现,需要使用结构体的指针
空接口不包含任何方法,因此任何结构体都实现了该接口,就好比JS和JAVA的Object,即继承树的根节点,因此空接口作为函数参数传入意味着可以是任何类型
func A(v interface{})
2.2空结构体的大小
var a struct{}
fmt.Println(unsafe.Sizeof(a)) //0
很奇怪,空指针按常理来说怎么都是个整数类型的指针,怎么会不占用空间呢?更奇怪的是,我们用dlv工具也打不上断点(var a struct{}),go tool compile -S main.go | grep "main.go:" 发现根本没有这行代码对应的汇编,直接被优化掉了(没有main.go:8)
仔细想想也是,既然不占用内存直接连寄存器存值都没必要
我们去之前分配内存的函数mallocgc看一下,发现这么一行代码
如果size=0那么就返回一个zerobase的地址
// base address for all 0-byte allocations
var zerobase uintptr
看这变量的注解恍然大悟,所有的零比特(这里空结构体)内存都是指向这个地址。
那么我们基于内存零开销就能做很多事情了,比如当作信号传入channel,与map结合实现set即值没有意义,只需要键
3.结构体初始化
需要注意GO没有构造函数(__construct),因此没法通过该方法进行初始化
- struct{}
- &struct{}
- new(struct)
new我们之前学习过会分配内存且所有bit置为0
4.结构体自引用
内部引用即结构体自己就是成员,典型的例子有二叉树的左右子根,只能使用指针
type Node struct{
left Node
right Node
}
这里报错无法通过,需要改成
type Node struct{
left *Node
right *Node
}
这里我们下面会提到,编译器需要确认结构体在内存空间中所占用的大小,不使用指针就会在陷入自循环状态,而指针在基础数据中提到过占用8个byte
5.结构体接收器和指针接收器
两者区别在于结构体接收器无法改变结构体的内部成员变量
//结构体接收器 失败
func (m man) a() {
a.age = 1
}
//方法接收器 成功
func (m *man) a() {
a.age = 1
}
6.继承?组合(嵌入字段)!
go没有继承的概念,把结构体a当作结构体b成员,结构体b也能调用a的方法,甚至只需要提供类型就可以(这叫做组合也叫嵌入字段)
type Person struct{}
func (p Person) say() {
fmt.Println("hi")
}
type Book struct{
Person
}
//下面这种写法比较罕见也没啥问题,但是大多都用上面的写法
type Book struct{
*Person
}
func main() {
var a = Book{}
a.say() // "hi"
}
组合可以是结构体组合也可以是接口组合
type Swiming interface {
Swim()
}
type Duck interface {
Swiming
}
6.1重写
go没有重写,会完全覆盖相同函数
7.内存布局
重点来了
内存大小与数组一样仅仅是简单的成员大小相加?不是的,但是结构体的内存是非常紧凑的,我们要知道一个前提,计算机读取数据是以字长为单位的(我以64位8字节的字长系统为例),那么我们就要确保我们要读取的变量刚好在这个字长的整数倍范围内,不能东一块西一块,不然不好拼接,以结构体T为例
type T struct{
a int8 // int类型是8字节 int8是1字节
b string //16字节
}
如果我们是紧挨着排列会怎么样
第一次按字长读取a能读到,b不完整只读取了一部分(7个字节还剩9字节),那剩下的部分要占1+1/8个字长(先读取8个字节,再读取最后一个字节),读完后还要切分拼接(前b的7个字节和a被一块读取),那效率多低(跨字长的排布会影响内存原子性和效率),如果我们需要把b的开头直接移到下一个字长,读b的时候连续读取2个字长不就效率更高了。其实a,b在字长这个区间上到底占哪一个(如何读取)是受对齐系数影响的
unsafe.Alignof()返回一个整数
//string 8
// int 8
// int8 1
变量的内存地址必须被对齐系数整除,string的对齐系数为8,必须在8的倍数上,这是为了内存对齐,那结构体的成员顺序不就影响了内存大小(结构体的对齐系数为成员最大的对齐系数,即上面这个结构体的对其系数为8)
type A struct {
a int8
b string
}
func main() {
var a A
fmt.Println(unsafe.Alignof(a)) //8
fmt.Println(unsafe.Sizeof(a)) //24
}
这是不是符合了我们的假设,a的读取确实是放在一个字长以内,那a和b的顺序换一下呢?也是一样的受最长对其系数影响
空结构体成员怎么办?
var a struct{}
fmt.Println(unsafe.Alignof(a)) //1
fmt.Println(unsafe.Sizeof(a)) //0
对齐系数是1,意味着可以随便放?
不是的,在结构体中意味着占位(前提是它位置是最后一个),如果前一个大于或等于一个字长则它需要单独占用一个字长,否则和前一个共用一个字长
type A struct{
a int8
b struct{}
c string
}
type B struct {
a int8
b string
c struct{}
}
type C struct {
b string
a int16
c struct{}
}
func main() {
var a A
var b B
var c C
fmt.Println(unsafe.Sizeof(a),unsafe.Alignof(a))
fmt.Println(unsafe.Sizeof(b),unsafe.Alignof(b))
fmt.Println(unsafe.Sizeof(c),unsafe.Alignof(c))
fmt.Println(unsafe.Sizeof(b.c) )
}
24 8
32 8
24 8
0
占了32个字节,意味着最后的空结构体要占位,占满一个字长
所以结构体的空间分布并不是紧挨着的,占用大小受成员的最大对齐系数影响
8.Tag
我们常能看到这样的结构体,成员后面跟字面量的描述
type A struct {
Name string `json:"name"`
Age int `json:age`
}
这是用于json的序列化和反序列化,json库依赖这些Tag的内容来完成json数据到结构体的映射
小结
所以在结构体中一定要注意字段的顺序,降低对空间的占用
参考
1.ref
2.Why is there no type inheritance?