Go语言备忘录:基本数据结构
Posted SuriFuture的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言备忘录:基本数据结构相关的知识,希望对你有一定的参考价值。
本文内容是本人对Go语言的变量、常量、数组、切片、映射、结构体的备忘录,记录了关键的相关知识点,以供翻查。
文中如有错误的地方请大家指出,以免误导!转摘本文也请注明出处,多谢!
参考书籍《Go语言圣经》、《Go语言实战》、《Go语言学习笔记》等
一、变量
- 变量是一段或多段用来存储数据的内存;
- 变量总是有固定的数据类型,类型决定了所占内存的长度和存储格式;
- 编译后的代码使用变量的内存地址来访问数据,而不是变量名;
- 简短变量声明只能在函数内声明(局部变量),var声明方式则无限制(但一般用于声明未显式初始化的变量);
- 声明同一作用域中的同名变量时,将回退为赋值,即重用该变量(必须至少有一个新变量定义);
- 而声明不同作用域的同名变量则为重新定义(覆盖);
var q int var y = 453 var ( n,m = 134,"srf" n1,m1 int ) func f1() { n,m := 25,"sss" n,m1 := 34,"yyy" fmt.Println(n,m,m1) n = n+5 //赋值表达式中,首先计算右值 //“_”空标识符用来临时规避编译器对未使用变量和导入包的错误检查 if _,ok := add1(n);ok { fmt.Println(n) } } func add1(n int) (int, bool) { return n+1,true }
二、常量、枚举
- 常量是一个不可改变的值,它可以为字面量,或编译器能计算出结果的表达式。未使用的常量不会引起编译错误;
- 在常量组中如不指定类型和初始值,则与上一行非空常量右值相同;
- 常量会被编译器在预处理阶段直接展开,作为指令数据使用,所以无法取常量的地址;
const i = 5 const ( x byte = 1 x1 x2 //x1,x2均为1 s = "abs" s1 //s1=“abc” ) const ( _,_ int = iota,iota*3 //0,0*3 忽略值,并显式指定类型为int k1,k2 //1,1*3 l1,l2 //2,2*3 o1,o2 = 5,6 //中断iota自增 r1,r2 //5,6 同上一行 e1,e2 = iota,iota*3 //5,5*3 恢复iota自增,按行递增 ) //枚举 type color byte const ( blue color = iota red green ) func main() { t:= blue fmt.Println(t) //0 //fmt.Println(&i) //错误:无法对常量取地址 cannot take the address of i }
三、数组
- 数组是切片和映射的基础数据结构。数组是值类型,在赋值和传递数组时将拷贝整个数组。
- 数组是一个长度固定的数据类型,存储着一段具有相同数据类型元素的连续内存块。
- 因为数组占用的内存是连续分配的,所以对数组元素的查询、修改等操作速度很快。
- 声明数组的方式:
- var array1 [5]int
- array1 := [5]int{3,5,6,3,2}
- array1 := [...]int{3,4,7,8,1} //根据数组字面量中元素的个数来确定数组的长度
- array1 := [5]int{0:3,3:5,4:8} //只初始化指定索引的元素,其余元素保持零值
- array1 := [...]int{1,2,9:32}
- 数组元素的类型可以为任何内置类型,也可以是某种结构类型,也可以是指针类型。
- 数组变量的类型包括数组长度和元素的类型,只有两部分都相同的数组才可相互赋值。
- 多维数组:数组本身只有一个维度,只能通过组合多个数组来创建多维数组;内置函数len、cap均返回第一维度的长度
- var array [4][2]int
- array := [4][2]int{2:{20,21},3:{41,25}}
- array := [4][2]int{2:{1:21},3:{0:41}}
- array := [...][4]int{{2,3},{4,5}} //仅第一维度允许使用“...”
- array[2][1] = 10
- 在函数间传递数组:由于在函数间传递变量时,传递的总是变量的值的副本,因为数组是值类型,所以在赋值和传递数组变量时将拷贝整个数组!在定义函数时,对于较大的数据类型应该把参数设计为指针类型,这样在调用函数时,只需在栈上分配给每个指针8字节的内存,但这意味着会改变指针指向的值(共享的内存),其实大部分情况下应该使用切片类型,而不是数组。
- 注意:因为切片的底层数组可能会在堆上分配内存,对于小数组在栈上拷贝的消耗可能比make代价小;
四、切片slice
- 切片slice是引用类型,它内部通过指针引用一个底层数组,并设定相关属性将数据的读写操作限定在指定区域。
//切片本身是个只读对象,工作机制类似数组指针的一种包装 type slice struct{ array unsafe.Pointer len int //可读写的元素数量 cap int //所引用数组片段的真实长度 }
- 创建和初始化:
- slice1 := make( []string, 5 ) //创建一个长度、容量都为5的string类型的切片
- slice1 := make( []string, 3, 5 ) //创建一个长度为3,容量为5的string类型的切片
- slice2 := []string{ "red","blue","green" } //长度和容量均为3的切片
- slice2 := []int{ 99:1 } //长度和容量均为100,并初始化第100个元素为1
- 再次切片reslice:以开始和结束原切片的索引位置确定所引用的数组片段,不支持反向索引,实际范围是一个右半开区间
假设原切片slice容量为k,新切片newSlice为原切片的索引 i 元素位置开始,在原切片的容量范围内取值 - newSlice := slice[ i : j ] //长度为j-i,容量为k-i
- newSlice := slice[ i : j : n ] //限制新切片的容量为n-i(第三个参数n-1表示新切片可扩展到的最后一个可见的底层数组部分的元素索引,这样就达到了限制容量的目的,注意:n必须>=j)
- 新切片无法访问它所指向的底层数组的第一个元素之前的部分(第一个索引之前的部分)
- 例子:ss:=[]int{10,20,30,40,50} newss:=ss[2:4:5] //newss为[30,40],容量为3
- 新切片和旧切片指向同一个底层数组;
//利用reslice实现一个栈式结构(也可将stack定义为一个类型) var stack = make([]int,0,5) func push(x int) error { n:=len(stack) if n == cap(stack) { return errors.New("stack is full") } stack = stack[:n+1] //新的stack增加了一个可访问元素stack[n] stack[n]=x return nil } func pop() (int, error) { n:=len(stack) if n == 0 { return 0,errors.New("stack is empty") } x:=stack[n-1] stack = stack[:n-1] //新的stack减少了一个可访问元素stack[n-1] return x,nil } func main() { for i := 0; i < 7; i++ { fmt.Printf("push %d: %v,%v\n",i,push(i),stack) } for i := 0; i < 7; i++ { x,err:=pop() fmt.Printf("push %d: %v,%v\n",x,err,stack) } }
- 切片的长度可以按需自动增长或缩小:
- 动态增长是通过append()函数实现的
- 缩小则是通过对它再次切片来实现,通过再次切片获得的新切片将和原切片共享底层数组,它们的指针指向同一个底层数组。
- 由于切片只是引用了底层数组,底层数组的数据并不属于切片本身,所以一个切片只需要24字节的内存(在64位机器上):指针字段8字节、长度字段8字节、容量字段8字节。所以在函数之间直接传递切片是高效的,只需分配24字节的栈内存。
- nil切片和空切片:
- nil切片:只声明,但未初始化的切片,如var slice1 []int,nil切片可用来描述一个不存在的切片
- 空切片:长度和容量均为0的切片,创建空切片时未对底层数组元素分配任何内存,可用来表示空集合,如slice1 := make( []int, 0 ),slice2 := []int{}
- 对nil切片和空切片调用内置函数append、len、cap的效果一样
- append()函数:
slice = append(slice, elem1, elem2) //一次可追加多个值
slice = append(slice, anotherSlice...) //使用“...”将anotherSlice的所有元素追加到slice里 - 当slice还有可用的容量时,append()操作将可用的元素合并到切片的长度,并对其赋值,最后返回一个全新的切片(和旧切片共享同一个底层数组);
- 如果slice的容量不足时,append()操作会创建一个新的底层数组,并将被引用的旧值复制到新数组里,然后追加新的值;
- 原切片容量不足时,且小于1000,则新切片的容量为原容量的2倍,若大于1000,则容量的增长因子变为1.25;
- 由于容量不足时,append操作会返回一个具有自己独立的底层数组的新切片,即与原切片不共享同一底层数组,对新切片元素的修改将不会影响原切片的底层数组,技巧:在创建切片时设置长度等于容量,这样就可以强制在第一次append操作时创建新的底层数组,达到与原数组分离的目的,如newSlice := oldSlice[2:3:3]
- copy函数:在两个切片对象之间复制数据,允许指向同一个底层数组,允许目标区间重叠。最终所复制长度以较短的切片长度为准
- 切片的迭代如:for index, value := range slice{ .... },index为当前迭代到的索引位置,value是从slice的副本中取值,index和value变量的内存地址是不变的,只是指向的值在不断更新。
- len函数可返还切片的长度、cap函数可返还切片的容量
- 多维切片(类似交错数组):切片和数组一样,本身是一维的,可以组合多个切片来形成多维切片,如:slice := [][]int{ {12},{34,23} },slice[0]为{12},slice[1]为{34,23}
- 注意:如果切片长时间引用大数组中很小的片段,则应该复制出所需数据,新建独立切片,以便原大数组内存可被及时回收;
五、映射map
- 映射map:是一个存储键值对的无序集合,它能基于键快速检索数据,键就像索引一样,指向与该键关联的值;
- 映射是无序的,每次迭代它时顺序可能不一样,因为映射内部使用了散列表;
- 映射的散列表包含一组桶,每个桶里存储着一部分键值对;
- 映射内部使用了两个数组:
- 第一个数组:存储着用于选择桶的散列键的高八位值,该数组用于区分每个键值对要存在哪个桶里;
- 第二个数组:每个桶里都有一个字节数组,先依次存储了该桶里的所有键,之后存储了该桶的所有值;
- 在存储、删除、查找映射的键值对时,会把指定的键传给映射的散列函数,该函数把键转换为一个散列值,然后把该散列值与第一个数组比对来选择哪个桶,再到桶里的字节数组里查找对应的键和值;
- 创建和初始化映射:
- dict1 := make(map[string]int) //空映射,等同于dict1 := map[string]int{}
dict1 := make(map[string]int, 1000) //预先分配足够内存空间,有助于提升性能
dict2 := map[string]int{"srf":143,"robt":342} - 映射的键:只能是能用“==”做比较的类型,但不可以是切片、函数、以及包含切片的类型,因为他们具有引用语义。而映射的值则可以是任意类型;
- nil映射是只声明而未初始化的映射,无法直接使用,如var dict map[string]int。空映射则可以直接使用;
- map类型的零值是nil,也就是没有引用任何哈希表。在向map存数据前必须先创建map,即:引用一个哈希表。
- 如果要用map存储大量小对象,应该直接存储为值类型,而非指针类型,有助于减少需要扫描的对象数量,大幅缩短垃圾回收时间;
- 从映射中取值:
- value := dict2["srf"] //键存在时返回对应的值,不存在时返回类型的零值
- value, ok := dict2["srf"] //ok为键是否存在的布尔标志
if ok { ...... } - map中的元素并不是一个变量,我们不能对map的元素进行取址操作(因为map可能会在动态增长时重新分配内存),因此无法直接修改value成员,而应该通过临时变量来修改,或把值定义为指针类型:
m := users[int]user{ 1:{"srf",25} } //m[1].age +=1 //错误,无法设置值 u := m[1] u.age+=1 m[1] = u
- 遍历映射:
- for key := range dict2 { ..... } //只接收键
- for key, value := range dict2 { ...... } //同时接收键和值
- 遍历映射时,可以添加、删除成员
- 遍历映射的键值对时的顺序是随机,若要有序的获得映射的键值对,则需要先遍历出映射的键存到一个切片中,然后排序该切片,最后遍历该切片,按切片中元素的顺序去映射中取对应的值
- delete(dict2,"srf") 从映射中删除指定的键值对;
- 运行时会对映射的并发操作做出检测,对映射的操作只能同步进行(同一时刻只能有一个任务在操作映射),否则会导致进程崩溃。可用读写锁sync.RWMutex实现同步,避免读写操作同时进行:
func main() { var lock sync.RWMutex m:=make(map[string]int) go func() { for { lock.Lock() m["a"] += 1 lock.Unlock() //不能用defer time.Sleep(time.Microsecond) } }() go func() { for { lock.RLock() _ = m["b"] lock.RUnlock() time.Sleep(time.Microsecond) } }() select {} //阻止进程退出 }
- 在函数间传递映射与传递切片一样(无须再次取地址),传递的只是映射本身的副本,而不会复制映射所引用的所有底层数据结构,对该映射副本所做的修改将会反映到所有对这个映射的引用。
- 多维映射:即值为映射类型的映射。使用时应注意,作为值的映射也需要初始化后才能使用,如:
var m1 = make(map[int]map[string]string)
m1[13]= map[string]string{"srf":"yes"} - 判断两个map是否相等的函数:
func equal(x, y map[string]int) bool { if len(x) != len(y) { return false } for k, xv := range x { if yv, ok := y[k]; !ok || yv != xv { return false } } return true }
- 用map来表示字符串的集合set:
m:=make(map[string]bool) if !m["srf"] { m["srf"] = true }
六、结构体
- 结构体struct是一种复合类型,由多个不同类型的命名字段(field)系列打包而成;
- 字段名必须唯一,可用“_”补位,支持使用自身的指针类型成员(这可以让我们创建递归的数据结构,比如链表和树结构等);
type node struct{ _ int id int `账号` next *node }
- 结构体类型信息包括:字段名、字段标签、排列顺序,只有三者全部相同才可认为是同一类型;
- 可按顺序初始化全部字段,但建议使用命名方式初始化部分或全部字段(可忽视字段的定义顺序,便于结构体成员顺序的修改、扩充);
- 结构体的比较:只有当结构体的所有成员都是可比较的,那么该结构体才是可比较的
- 可直接定义一个匿名的结构体类型,并赋值给一个变量,或用作字段的类型(匿名结构体字段无法用字面量形式直接初始化,需要“.”语法来初始化其成员)
u := struct{ name string } type file struct{ name string attr struct{ owner int perm int } } f := file{name:"test.dat"} f.attr.owner = 1 f.attr.perm = 0755
- 空结构:struct{},长度为0,不分配内存,它和其它所有“长度”为0的对象通常都指向runtime.zerobase变量(即它们都指向同一个变量);空结构类型经常作为通道元素的类型,用于事件通知(优点是不占内存);
- 匿名字段(嵌入类型):即没有指定显式的名称,只有类型的字段:
- 编译器将隐式地以类型名作为字段名称(不包含包名);
- 外层的结构体不仅获得了匿名成员类型的所有成员,而且也获得了该类型全部的导出的方法;
- 可直接引用嵌入类型字段的成员,但在以字面量语法初始化时须显式初始化它的整个结构;
- 匿名字段的成员的数据类型必须是命名的类型或指向一个命名的类型的指针,不能是接口指针和多级指针;
- 不能将基础类型和其指针类型同时作为匿名字段
- 字段重名处理:优先使用外层字段(内层字段被遮蔽了,只能通过完全限定名来访问),对于多个相同层级的同名字段也必须通过完全限定名来访问,否则编译器无法确定目标;
- 字段标签(tag):用来对字段进行描述的元数据,它虽然不属于数据成员,但却是类型信息的组成部分;在运行期,可用反射来获取字段的标签信息,它常被用作格式检验(如JSON)、数据库关系映射等;标准库reflect.StructTag提供了分/解析标签的功能;
type user struct{ name string `昵称` sex byte `性别` } func main(){ u:=user{"TOM",1} v:=reflect.ValueOf(u) t:=v.Type() for i,n:=0,t.NumField();i<n;i++{ fmt.Printf("%s: %v\n", t.Field(i).Tag, v.Field(i)) } }
- 不管结构体有多少个字段,它的内存总是一次性分配的,各字段在相邻的地址空间按定义顺序排列(包含嵌入字段的所有 成员)。对于引用类型、字符串、指针,结构内存中只包含其基本(头部)数据。
- 结构体在分配内存时,会进行内存对齐处理(根据所有字段中最长的基础类型宽度为标准),唯一例外是编译器把空结构类型字段作为最后一个字段时的长度视为1来做对齐处理(避免越界)。
- 内存对齐与硬件平台、以及访问效率有关(CPU在访问自然对齐的数据时需要的读周期更少,还可避免拼接数据)
以上是关于Go语言备忘录:基本数据结构的主要内容,如果未能解决你的问题,请参考以下文章
Go语言实战 (14) Gin+gRPC 微服务实现备忘录 (上) | 用户模块
Go语言实战 (15) Gin+gRPC 微服务实现备忘录 (下) | 备忘录模块
Go语言实战 (10) go-micro微服务 实现简单备忘录 (上) | 用户模块