go的数据结构详解。
空结构体
空结构体是0字节
空结构体地址都是相同(不被包含在其他结构体中)
空结构体 可以节约内存
空结构体一般用在 map做为值、 channel 做数据通信用
1//runtime.malloc.go
2// base address for all 0-byte allocations
3var zerobase uintptr
string 类型
本质是个结构体
16个字节
1fmt.Println(unsafe.Sizeof("中国人")) //16
2fmt.Println(unsafe.Sizeof("中国人mao")) //16
1// runtime/strings.go
2type stringStruct struct {
3 str unsafe.Pointer //指向 字节数组
4 len int //长度 , 字节数 ,字节数组的长度
5}
for …range 可以转为 Rune类型的数组
字符串是底层数组的引用
字符串底层数组的地址
1// 字符串底层数组的地址
2func stringptr(s string) uintptr {
3 return (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
4}
字符串转byte数组避免内存copy
1// 字符串转byte数组,优化方案,fasthttp 使用这种方案。
2func String2bytes(s string) []byte{
3 sh :=(*reflect.StringHeader)(unsafe.Pointer(&s))
4 bh :=reflect.SliceHeader{
5 Data: sh.Data,
6 Len: sh.Len,
7 Cap: sh.Len,
8 }
9 return *(*[]byte)(unsafe.Pointer(&bh))
10}
slice
对数组的引用
1var slice []int
2//unsafe.Sizeof功能:计算⼀一个数据在内存中占的字节⼤大⼩小
3size := unsafe.Sizeof(slice)
4fmt.Println(slice)
5//⽆无论切⽚片是否有数据 计算结果都为:24
1//path:Go SDK/src/runtime/slice.go
2type slice struct {
3 array unsafe.Pointer //指向数组
4 len int
5 cap int //数组的长度
6}
切⽚的源码
go build -gcflags -S main.go. 查看编译过程
- 字面量方式创建切片
1var slice = []int{1,2,3}
2//会先创建一个arr:=[3]int{1,2,3}的数组
3//然后 runtime.newobject slice{arr,3,3}
4 0x0021 00033 (/Users/mac/code/crgo/test.go:7) LEAQ type.[3]int(SB), AX
5 0x0028 00040 (/Users/mac/code/crgo/test.go:7) MOVQ AX, (SP)
6 0x002c 00044 (/Users/mac/code/crgo/test.go:7) PCDATA $1, $0
7 0x002c 00044 (/Users/mac/code/crgo/test.go:7) CALL runtime.newobject(SB)
- make 方式创建切片
1var slice []int = make([]int, 10, 20)
会被编译器翻译为 runtime.makeslice,并执行如下函数:
1
2func makeslice(et *_type, len, cap int) slice {
3 maxElements := maxSliceCap(et.size) //判断len是否满⾜足条件
4if len < 0 || uintptr(len) > maxElements {
5 panic(errorString("makeslice: len out of range"))
6 }
7//判断cap是否满⾜足条件
8if cap < len || uintptr(cap) > maxElements {
9 panic(errorString("makeslice: cap out of range"))
10 }
11//根据cap⼤⼩,通过mallocgc创建内存空间
12p := mallocgc(et.size*uintptr(cap), et, true) //将地址作为其中⼀一个结构体成员返回
13return slice{p, len, cap}
14}
- 基于新建内存空间创建
1 var slice *[]int = new([]int)
会编译为:
1 func newobject(typ *_type) unsafe.Pointer { //创建内存空间 并返回指针
2 return mallocgc(typ.size, typ, true)
3}
append
不扩容时,只调整len (编译器负责)
扩容时,编译时转为调用runtime.growslice() growslice
- 如果期望容量大于当前容量2倍,就会使用期望容量
- 如果当前切片的长度小于1024,将容量翻倍
- 当切片长度大于1024,每次增加25%
- 切片扩容时,并发不安全,注意切片并发要加锁
接口
鸭子类型
维基百科里的定义
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
翻译过来就是:如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。
Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过通过接口的方式完美支持鸭子类型。
鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了 鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。
值接受者和指针接受者的区别
1package main
2
3import "fmt"
4
5type Person struct {
6 age int
7}
8
9func (p Person) howOld() int {
10 return p.age
11}
12
13func (p *Person) growUp() {
14 p.age += 1
15}
16
17func main() {
18 // qcrao 是值类型
19 qcrao := Person{age: 18}
20
21 // 值类型 调用接收者也是值类型的方法
22 fmt.Println(qcrao.howOld())
23
24 // 值类型 调用接收者是指针类型的方法
25 qcrao.growUp()
26 fmt.Println(qcrao.howOld())
27
28 // ----------------------
29
30 // stefno 是指针类型
31 stefno := &Person{age: 100}
32
33 // 指针类型 调用接收者是值类型的方法
34 fmt.Println(stefno.howOld())
35
36 // 指针类型 调用接收者也是指针类型的方法
37 stefno.growUp()
38 fmt.Println(stefno.howOld())
39}
- | 值接收者方法 | 指针接收者方法 |
---|---|---|
值类型调用者 | 方法会使用调用者的一个副本,类似于“传值” | 使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp() |
指针类型调用者 | 指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld() |
实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针 |
- 指针对象可以调用 指针方法,也可以调用值方法 (底层是解引用调用)
- 值对象可以调用值方法,也可以调用指针方法(底层是通过取地址 &xx{}去调用)
结构体和指针实现接口
1package main
2
3import "fmt"
4
5type coder interface {
6 code()
7 debug()
8}
9
10type Gopher struct {
11 language string
12}
13
14func (p Gopher) code() {
15 fmt.Printf("I am coding %s language\n", p.language)
16}
17
18/*
19底层会加上这个方法 。
20func (p *Gopher) code() {
21 fmt.Printf("I am coding %s language\n", p.language)
22}
23*/
24
25func (p *Gopher) debug() {
26 fmt.Printf("I am debuging %s language\n", p.language)
27}
28
29func main() {
30 var c coder = &Gopher{"Go"} //这个OK
31 c.code()
32 c.debug()
33}
34
35func main1() {
36 var c coder = Gopher{"Go"} //报错。
37 c.code()
38 c.debug() //这个方法他没实现调
39}
当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法。(底层编译器会做)
如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。
实现接口类型的对象的时候,指针对象能实现值方法(那个值方法,底层会生成一个指针方法), 值对象不能实现指针方法。
接口值的底层表示 iface
- 接口数据使用runtime.iface表示
- iface记录了数据的地址
- iface中也记录了接口类型信息和实现的方法
1type iface struct {
2 tab *itab // 它表示接口的类型以及赋给这个接口的实体类型。
3 data unsafe.Pointer //data 则指向接口具体的值,一般而言是一个指向堆内存的指针。
4}
5
6// layout of Itab known to compilers
7// allocated in non-garbage-collected memory
8// Needs to be in sync with
9// ../cmd/compile/internal/gc/reflect.go:/^func.dumptabs.
10type itab struct {
11 inter *interfacetype
12 _type *_type
13 hash uint32 // copy of _type.hash. Used for type switches.
14 _ [4]byte
15 fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. 动态
16}
空接口
1type eface struct {
2 _type *_type
3 data unsafe.Pointer
4}
- 可以承载任何数据
- 最大的作用作为函数任意类型的入参。
- 函数调用时, 生成一个空接口eface ,再传进去。
nil
1// nil is a predeclared identifier representing the zero value for a
2// pointer, channel, func, interface, map, or slice type.
3var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
- nil是空,并不一定是空指针
- nil是6种类型的零值或空值 (pointer, channel, func, interface, map, or slice type)
- 每种类型的nil 是不同的,无法比较
- 空接口的零值是nil, 但是有类型就不是nil
内存对齐
cpu64位系统, 8字节读取, cpu32位系统4字节进行读取。
内存对齐:提高内存的操作效率,有利于内存原子性
对齐系数
- 为了 方便内存对齐,go 提供了对齐函数 unsafe.Alignof()
- 对齐系数的含义:变量的内存地址必须被对齐系数整除
- 如果对齐系数为4,表示变量内存地址必须是4的倍数
- 基本类型考虑对齐系数
1package main
2
3import (
4 "fmt"
5 "unsafe"
6)
7
8func main() {
9 fmt.Printf("bool size is %d ,Alignof对齐系数 is %d\n", unsafe.Sizeof(true), unsafe.Alignof(true))
10 fmt.Printf("int8 size is %d ,Alignof对齐系数 is %d\n", unsafe.Sizeof(int8(0)), unsafe.Alignof(int8(0)))
11 fmt.Printf("int16 size is %d ,Alignof对齐系数 is %d\n", unsafe.Sizeof(int16(0)), unsafe.Alignof(int16(0)))
12 fmt.Printf("int32 size is %d ,Alignof对齐系数 is %d\n", unsafe.Sizeof(int32(0)), unsafe.Alignof(int32(0)))
13 fmt.Printf("int64 size is %d ,Alignof对齐系数 is %d\n", unsafe.Sizeof(int64(0)), unsafe.Alignof(int64(0)))
14}
15
16/*
17bool size is 1 ,Alignof对齐系数 is 1
18int8 size is 1 ,Alignof对齐系数 is 1
19int16 size is 2 ,Alignof对齐系数 is 2
20int32 size is 4 ,Alignof对齐系数 is 4
21int64 size is 8 ,Alignof对齐系数 is 8
22*/
对齐系数,如下图所示
bool 对齐系数是1
int16 对齐系数是 2 内存地址必须是2的倍数
结构体内存对齐
结构体的内存顺序严格按代码编写顺序从上到下进行编排
结构体对齐分为内部对齐和 结构体之间的对齐(可能需要外部填充对齐)
内部对齐:考虑成员大小和成员对齐系数
结构体长度填充:考虑成员对齐系数 和 系统字长
结构体内部对齐
结构体内部对齐指的是 结构体成员的相对位置(偏移量)
每个成员的偏移量是自身大小与其对齐系数较小值的倍数
1type Demo stuct {
2 a bool //bool 大小1 对齐系数 1
3 b string // 大小16,对齐系数 8
4 c int16 // 大小2, 对齐系数 2
5}
结构体长度填充
结构体长度填充 指的是结构体通过增加长度,对齐系数字长
结构体长度是最大成员长度与系统字长较小的整数倍
(系统字长 64位系统 就是64位(8字节), 上面Demo struct 最大成员长度是string 16字节,较小就是8字节, 结构体长度是 要求是 8字节的整数倍)
可以尝试通过调整成员顺序,节约空间
1type Demo stuct {
2 a bool //bool 大小1 对齐系数 1
3 c int16 // 大小2, 对齐系数 2
4 b string // 大小16,对齐系数 8
5}
结构体自身对齐系数
结构体的起始位置必须是对齐系数的倍数
- string 的对齐系数是8
- demo 结构体,对齐系数是多少 = 8
- 结构体的对齐系数是其成员最大对齐系数
上面例子demo 这个结构体 的 起始位置 必须是 8 的倍数
空结构体在结构体中的内存对齐情况
空结构体单独出现时,地址为zerobase
空结构体出现在结构体中时,地址跟随前一个变量 (前一个变量地址+变量长度)
空结构体出现在结构体末尾的时,需要补齐字长, 需要填充对齐
1type Demo stuct {
2 a bool //bool 大小1 对齐系数 1
3 c int16 // 大小2, 对齐系数 2
4 b string // 大小16,对齐系数 8
5 d struct{}
6}