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}