Golang学习笔记-复合类型
Posted Zheng"Rui
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang学习笔记-复合类型相关的知识,希望对你有一定的参考价值。
在之前学习了go语言的基本数据类型,现在讨论go的复合数据类型,包括:数组,切片(slice),字典(map)和结构体。
数组和结构体是聚合类型,它们的值由许多元素或成员字段的值组成。数组是同构的元素组成-每个数组中元素都是相同的类型。结构体是异构的元素组成。数组和结构体都有固定的内存大小。相比之下,切片和字典是动态的数据结构,它们将根据需要动态增长。
数组
数组是一个由固定长度的特定元素类型组成的序列,数组可以由零个或者多个元素组成,因为数组的长度是固定的,所以一般不直接使用。
数组的每个元素都可以通过其下标进行访问,使用内置函数len可以求出数组的长度。
默认情况下,数组的每个元素都会被初始化成相应的零值。当然,也可以使用字面值进行初始化。
在数组字面值中,如果在数组长度位置出现了三个点…说明这个数组的长度根据初始化的个数来确定(这里只能使用 := 这种形式的声明)
数组的长度是数组类型的一个组成部分,因此[3]int和[4]int不是一个类型的数组,数组的长度必须是常量表达式,因为数组的长度是在编译时期确定的。
数组,切片,字典,结构体的字面值写法都很相似。可以指定一个索引一个对应值的形式来进行初始化。
如果一个数组的元素类型是可以比较的,那么数组类型也是可以比较的,可以使用= = 或者!=来进行数组比较,当两个数组的元素全都相同时,认为两个数组是相等的(当然数组类型需要相等)
当我们调用一个函数的时候,函数的每一个参数都将会被赋值给函数内部的参数变量,所以参数变量接受的是一个复制的副本,并不是原始调用的变量。因为这个机制,所以函数参数传递大的数组效率是低效的,并且在函数内部,对于数组的改变,其实改变的是副本,并不是修改原始的数据,在这个方面,go对待数组的方式和其他很多编程语言不同,其他编程语言可能会隐式的将数组作为引用或指针对象进行传递,当然我们在go中可以显式的传入一个指针,这样在函数内部通过指针对数组进行修改就能够直接反馈到调用者。
虽然通过数组指针的方式进行传参是高效的,但是数组依然是僵化的类型,它不允许改变数组的长度,所以在SHA256这样需要处理特定长度的数组的特例之外,一般使用slice来替代数组。
切片
slice代表变长的序列,切片中保存的也是相同类型的元素,但是没有固定的长度。
一个切片是一个轻量级的数据结构。在切片的底层引用了一个数组对象,一个切片由三个部分组成:指针,长度,容量。其中指针指向第一个切片元素引用的数组元素的地址,这里的第一个切片元素并不一定是第一个数组元素。长度对应了切片中数据元素的个数,长度不能超过容量,容量一般是从切片开始到底层数据的结尾位置。
多个切片之间可以共享底层的数据,并且切片引用的数组部分可以重叠。
和数组不同,两个切片之间不能使用==来判断是否相等,切片可以和nil判断是否为空。但是需要注意,一个nil值的切片并没有底层数据,一个nil值的切片的长度和容量都是0,但是一个长度和容量都是0的切片不一定是nil,比如[]int或者make([]int,3)[3:]都是空切片,但是不等于nil,应该使用len来判断一个切片是否为空。
append函数
内置的append函数,用于向slice中添加元素,append的大致流程是,首先传递参数传递的是一个切片和一个对应的元素,首先判断当前slice中的容量剩余是否能够存放的下要添加的数据,如果能够添加,则直接在末尾添加,如果不够,则重新开辟一块足够大的空间,为了避免多次空间开辟,这里一般使用两倍的空间大小去开辟,然后进行原来的数据拷贝,之后将要添加的数据进行添加,然后返回新切片。所以append之后,返回的切片不一定就是原来传入的切片,这也就是为什么需要在append之后,对原切片进行更新的原因。(当然,go中的实现肯定比这个流程更加复杂,只是大致介绍这个流程)
需要注意,在更新slice的操作不仅在append中是必要的,实际上,在任何有可能导致长度,容量,或者底层数组发生变化的情况下,都需要进行slice更新,因为slice对底层数组的访问时间接的,但是,在函数进行传参的时候,对与本身的指针,长度,容量都是直接传参的,要在函数中更新这些信息,就必须要在外部对slice进行更新。
slice内存技巧
可以先实现一个返回不包含空字符串的切片的功能函数。
然后使用这个函数:
这样做虽然功能可以实现,但是缺点是改变了原来的切片
可以使用 s = nonempty(s)进行更新,当然也可以用append来实nonempty。
map
map是一个哈希表,是一个无序的key/value集合,其中所有的key都是不同的,然后根据key,在常数时间复杂度内进行检索value。
在go中一个map对应一个哈希表的引用,map类型可以写成map[K]V,其中key和value都具有相同的数据类型,但是key和value之间可以有不同的数据类型。
可以通过make来生成一个map类型的变量,也可以之间通过字面值语法进行创建。
mp := make(map[string]string) || mp := map[string]string“hello”:”hello”
一种创建空字典的方法是 mp := map[string]string
map中的元素,可以通过对应的key下标进行访问。
通过内置的delete函数可以删除元素。所有这些操作都是安全的,即使map中没有这些数据。
map中的元素并不是一个变量,因此不能进行取地址&操作。原因是随着map元素的增加,可能需要扩容操作,导致之前的地址失效。
可以使用for key,value := range mp的形式对字典进行遍历。对map的每一次遍历结果都可能是不同的,这样做可以限制程序不依赖于具体的哈希实现。
如果需要以同样的顺序进行遍历,需要显示的对key进行排序,记录下key,然后进行遍历。
map类型的零值是nil,也就是没有引用任何哈希表
对于map的大多数操作:例如查找,删除,len和range循环都可以在一个nil值的map上安全的进行,但是对一个nil值的map进行添加则会产生异常。即向map存放数据之前,必须先创建map。
通过key下标可以对这个数据进行访问,如果map中有这个key下标的元素,则返回对应的value,如果没有,则会创建这个元素,value是对应类型的零值。
可以使用 v,ok := mp[k] 其中v 返回对应的value,ok是一个布尔类型的值,用来表示该key对应的value是否存在。
和slice一样,map也不可以直接进行比较,要判断两个map是否相同,只能通过循环依次比较。
go中没有实现set集合,但是可以通过map来表示集合,例如可以实现一个map[string]bool,这种不关心value的map,可以看做是字符串的集合。
map中的key必须是可以比较的类型,如果有的时候我们想使用slice作为key,该怎么办呢?可以先创建的一个转化函数,将slice转化成可以比较的string,然后定义一个key是string的map,这样就可以实现不知是slice,还包括其他不能比较的类型,成为key的方法。
这里的可以比较,指的是可以使用两个等于号来判断两个key是否相等。因为map要求key不重复。
结构体
结构体是一种聚合类型,可以由零个或者多个不同类型的值聚合而成。每一个值被称为结构体的成员。
一个命名为S的结构体不能够在包含它本身。但是可以包含*S。
结构体字面值
结构体的值可以用结构体的字面值来表示,结构体字面值可以指定每一个成员的值。
有两种形式的结构体字面值语法:
1.是以结构体出现的顺序,进行字面值赋值,为每一个成员赋值。不够结构体成员的细微改动就可能导致此赋值语句无法编译。
2.更常见的是使用结构体的成员名和值进行对应,去进行初始化。如果有的成员被忽略,则被初始化为其对应的零值。
这两种写法的形式不能混和使用。
结构体可以作为函数的参数和返回值。考虑效率,较大的结构体,通常采用指针的形式进行传递。
如果要在函数内部改变一个结构体的话,那么使用指针传递就是必须的了,因为在go中所有的函数传参都是值传递。
使用 pp := &Point1,2这样的语法,可以生成一个结构体,并记录他的地址,可以直接在表达式中使用。
结构体比较
如果一个结构体中所有元素都是可以比较的,那么这个结构体就是可以比较的。也就是两个结构体可以使用= =或者!=进行比较。==运算符将会比较两个结构体中的每个成员,也就是两个结构体是否相等取决于其所有成员是否都相等。
可比较的结构体类型,可以用于map中的key
结构体嵌入和匿名成员
go语言中提供了结构体嵌入机制,使得一个命名的结构体包含另一个匿名的结构体成员,从而实现使用简单的点运算符x.f来访问匿名成员链中嵌套的x.a.b.f
go中的一个特性是让我们只声明一个成员的数据类型,而不指定成员的名字,这类成员就是匿名成员。匿名成员的类型必须是一个命名的类型或者是一个指向命名的类型的指针。
得益于这个特性,使得我们在访问匿名结构体的成员的时候,只需要访问叶子属性,而不需要给出完整路径。
当然,通过完整路径访问仍然是可以的,此时成员的名字就是它的类型名。
但是,可惜的是在字面值语法中,并没有这种简短的表示方法。
因为匿名成员也有一个隐式的名字,(即成员的类型),所以不能存在两个相同类型的匿名成员,这会导致名字冲突。
同时,因为匿名成员的名字由其类型隐式决定,所以仍然存在可见性的规则约束,也就是如果该类型的第一个字母是大写的,则该匿名成员对包外部是可见的。
扩展:到目前为止,学习到的匿名成员特性只是对访问嵌套成员的点运算符提供了简单的语法糖,但其实,匿名成员并不要求一定是结构体类型,任何类型都可以作为结构体的匿名成员。而嵌入一个没有任何子成员的匿名成员的目的就是为了嵌入该类型的方法集,简短的点运算符语法可以用来选择匿名成员嵌套的成员,也可以用来访问匿名成员的方法。实际上,外层的结构体不仅获得了匿名成员类型的全部成员,而且还获得了该类型导出的所有方法。这个机制可以用来将一个个有简单行为的对象组合成有复杂行为的对象。组合是go中面向对象编程的核心。
以上是关于Golang学习笔记-复合类型的主要内容,如果未能解决你的问题,请参考以下文章