5 复合数据类型

[TOC]

复合数据类型

Go基本复合数据类型有指针、数组、slice、map、struct、chan、interface。

  • 其中指针本身和数组、struct是值类型,slice、map、chan、interface是引用类型。

指针

指针本质上是内存地址,一般为内存中存储变量值的起始位置。

  • 指针变量就是存储内存地址的变量。

  • 指针是对内存数据的一种引用。

  • 指针代表一个变量的地址和类型,如果变量类型是T,那指向它的指针就是一个指向T的指针类型。

指针的优势

  • 大量数据作为函数参数传递时,传递数据的指针最为高效,不需要对数据进行值拷贝,速度更快、内存占用更低。

声明指针

指针声明类型为*T,Go支持多级指针**T,通过&获取变量的地址。

  • *T出现再“=”左边表示指针声明,*T出现在“=”右边表示取指针指向的值。

  • Go为实现自动垃圾回收禁止了指针运算,在C和C++中指针运算问题较多。

  • Go编译器通过“栈逃逸”机制将函数中的局部变量分配在堆上

栈逃逸机制

在函数栈之外共享一个值时,它将被分配在堆上。逃逸分析算法的工作是找到这些情况,并在程序中确保对任何值的访问是准确、一致和高效的。

  • 编译器在编译阶段确立一个变量要放堆上还是栈上要看是否有在*其它地方(非局部)*被引用,有被引用那么它一定分配到堆上,否则分配到栈上。没有被外部引用,但对象太大导致无法存放在栈区上,依然有可能分配到堆上。

  • 如果变量都分配到堆上会出现垃圾回收(GC)的压力不断增大,申请、分配、回收内存的系统开销增大,动态分配内存会产生一定量的内存碎片。频繁申请并分配堆内存是有一定 “代价” 的,会影响应用程序运行的效率,间接影响到整体系统,因此要按需分配最大限度地灵活利用资源,所以编译器需要进行逃逸分析确定变量放在堆上还是栈上。

查看逃逸分析过程

通过编译器命令,可以看到详细的逃逸分析过程。指令集 -gcflags 用于将标识参数传递给 Go 编译器。

  • -m 会打印出逃逸分析的优化策略,最多总共可以用 4 个 -m,但是信息量较大,一般使用 1 个。

  • -l 禁用函数内联,禁用掉 inline 可以更方便观察逃逸情况,减少干扰。

  • go build -gcflags '-m -l' main.go

也可以通过反编译命令查看详细的逃逸分析过程。

  • go tool compile -S main.go

  • 可以通过 go tool compile -help 查看所有允许传递给编译器的标识参数。

逃逸案例

使用 go build -gcflags '-m -l' main.go 查看逃逸分析,这些命令适用于 linux 或 unix 环境。

查看分析结果,a 分配在堆上。

new 创建指针

new 内置函数创建指针

  • new 内置函数可以声明一个指针类型并初始化为对应类型的零值。

  • new 会开辟一块内存空间,并将这块空间的内存地址返回。

  • Go是强类型语言,不同类型之间不能直接进行赋值操作,指针也是具有明确类型的。

new内置函数创建指针是开辟了内存空间返回给变量的,var a *int64 只是声明

  • 因此 a 的值是nil(野指针),没有可用的内存空间,如果进行赋值会导致空指针异常。

  • b 是有内存空间的,可以进行赋值。

new和make的区别

make 和 new 都是用来分配内存的內置函数,且在堆上分配内存。

  • make 既分配内存,也初始化内存。new 只是将内存清零,并没有初始化内存。

  • make内置函数只能对map、slice、channel进行内存空间分配,make返回的是引用类型本身。

  • new 可以分配任意类型的内存,返回的是指向类型的指针。

指针内存示意图

指针内存示意图

指针内存示意图

unsafe 包

不同类型指针不允许直接相互赋值,但是Go提供了unsafe包可以实现这种操作。

数组

Go中有指定长度的是数组,没有指定长度的是切片。

  • 数组创建完成就是固定长度,不可追加元素。

  • 数组长度是数组类型的组成部分,[2]int 和 [3]int是不同类型的。

  • 数组定义后不初始化会是数组类型的默认值。

  • 固定长度初始化。

  • 指定数组索引初始化,索引从0开始,最后索引是len(arr) - 1。

  • 省略数组长度...初始化数组 自动推算长度。

  • 数组是值类型,数组赋值或作为形参都是值拷贝。

多维数组

多维数组是数组中装数组。

  • 多维数组定义只有第一维可以使用[...]推导长度。

  • 多维数组访问使用索引层层确定访问的位置,ar[0][0] 访问第0组0位置元素。

  • 二维数组

  • 三维数组,修改最后一个元素

slice 切片

切片是引用类型,可变长度的数组。

  • 切片本质是一个结构体,内部维持一个数组array指针、长度len、容量cap。

  • 内置函数len(切片)获取长度,cap(切片)获取容量。

  • 定义、创建切片

slice追加元素和扩容

切片通过append() 追加元素,如果容量不够会自动扩容,每次扩容是原容量2倍,但是最高为1024扩容。

  • 切片扩容是它内部的array数组扩容,容量不够开辟一个空间更大的数组,将数组的数据拷贝到新数组再把array指针指向新的内存空间就完成扩容。

引用类型传递

引用类型传递,在函数中发生内容改变会反应到所有切片引用中。

复制切片 copy()

切片删除元素

切片没有提供删除元素的函数,因此删除切片的元素实际是放弃旧数组创建一个没有要被删除的元素的数组,再将 array 指向新数组。

数组转切片

  • 数组转切片,arr[起始:结束],arr[起始:],arr[:结束]

map 映射

map是引用类型,存储无序、不可重复(键重复覆盖)的键值对。

  • map是基于键来存储值,map能够基于键快速检索数据,键就像索引一样,指向与该键关联的值。

  • delete(键) 删除指定键值。

  • 引用类型传递,在函数中发生内容改变会反应到所有map引用中。

map内部实现

map是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素。

  • map是无序的集合,没有办法预测键值对被返回的顺序,即便使用同样的顺序保存键值对,每次迭代map的时候顺序也可能不一样。

  • map无序的原因是map的实现使用了hash散列表。

struct 结构体

结构体可以由不同类型的元素组合,它是值类型,赋值或者作为形参都会进行值拷贝传递。

  • 结构体中的类型可以是任意类型。

  • 结构体的存储空间是连续的,按照声明的顺序存放。

  • 匿名结构体

  • 自定义结构体,结构体继承准确说是组合

  • 初始化

  • new() 初始化结构体,new()返回的是一个指针

  • 结构体之间进行转换

细节

  • 会进行值拷贝,p1会拷贝给p2, var p1 Person; var p2 Person = p1

  • 因为值拷贝 p1与p2它们互不干扰,p1.age = 112; p2.Age = 223;

  • 结构体作为函数形参也会进行值拷贝。

  • 结构体在内存中是一片连续的空间。

struct 结构体 Tag

  • tag可以通过反射机制获取,常用于序列化和反序列化。

  • struct每个字段上都可以写多个tag使用空格间隔,也可以使用"_"忽略

  • json.Marshal() json序列化读取tag为json的字段,将Name字段序列化为name。

  • 零值忽略 omitempty,Class字段是零值则不会被json序列化。

Last updated