go程序设计语言第四章-组合类型
本章主要介绍组合类型(composite type)中的array、slice、map、struct
。
Slice
slice的组成
A slice has three components: a pointer, a length, and a capacity.
slice由三个部分组成:指针,长度,容量
slice之间比较
Unlike arrays, slices are not comparable, so we cannot use == to test whether two slices contain the same elements.
与数组不同的是,slice并不能使用==来比较两个slice是否包含相同的元素。
标准库中有bytes.Equal来比较[]byte, 其他的slice类型比较需要我们自己实现。
package bytes
// Equal reports whether a and b
// are the same length and contain the same bytes.
// A nil argument is equivalent to an empty slice.
func Equal(a, b []byte) bool {
// Neither cmd/compile nor gccgo allocates for these string conversions.
return string(a) == string(b)
}
比如我们自己实现两个[]string的比较,先比较深度,再逐一比较各个元素:
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
slice为什么不能比较
为什么slice不采用上面这种先深度相等判断然后逐一比较元素的方法,来进行两个slice的比较呢?
有两个方面原因:
1:与array不同, slice的元素是间接(indirect)的,因此一个slice可能会包含自身
:
s := []interface{}{1, nil}
s[1] = s
fmt.Println(s) // fatal error: stack overflow
这样的s执行fmt.Println(s)时会报错stack overflow,其原因是Println函数是递归打印的,s的元素类型是interface{},打印时要获取其动态类型和动态值。
而s[1]的动态类型是[]interface{},其动态值是指向s底层数组的指针。
就是说s[1]又指向它自己,又要获取其动态类型和动态值?这个过程一直持续,导致最后栈溢出。
【这就涉及到了将具体类型赋值给空接口时,接口里保存的是什么?比如array、slice、string、int、map、chan、interface】
s包含了它自身,只要不用fmt.Println(s)导致迭代栈溢出,其元素是可以打印的:
s2 := s[1].([]interface{})
fmt.Println(s2[0]) // 1
s3 := s2[1].([]interface{})
fmt.Println(s3[0]) // 1
相对应的array,不能包含它自己:
s := [2]interface{}{1, nil}
s[1] = s
fmt.Println(s) // [1 [1 <nil>]]
可以看到在赋值s[1] = s时,是将当前s的每个元素组成array赋给了s[1]这个interface{},是值复制。
2: 因为slice element是indirect, 因此一个固定的slice value在不同时刻可能会有不同的元素
,
因为slice value仅有三个部分(指针、长度、容量),当底层数组改变时,slice的元素也会改变。
而hash table如go中的map只是对它的key进行shallow copy浅复制,它要求在hash table的声明周期内
每个key保持不变。因此slice不能成为map的key。
而对于引用类型如ptr和chan而言,等号 == 操作符test reference identity只是判断是否引用相同的对象。
而如果slice也采用这种 语义,它也就可以解决map的key问题,但它就和同为容器类的array的等号操作符含义不同了,会令人感到困惑,因此干脆禁用slice的比较操作。
总结:slice为什么不能比较?
1是如果采用类似array的逐个元素比较的这种”deep“ equality,由于slice可能会包含它自己,
导致这种比较会比较复杂也不易于理解。
2是如果采用了”deep“ equality方式而使得两个slice可以比较,那按道理他们就可以作为map的key。
但map的key只是被浅复制,要求在声明周期内不可变,那么”deep“ equality就会使得即使两个slice可比较,
也不能作为map的key。而如果采用了类似其他引用方式的shallow queqlity方式,即比较引用对应的对象是否是同一个,这会使得slice与array的==操作符产生不同的含义。
因此,slice不能比较,也不能作为map的key值
。
slice的零值为nil,代表没有底层数组,它的len和cap也都为0.
同时也有非nil的slice,但len和cap也为0,例如[]int{}或者make([]int,3)[3:].
和其他有nil值的类型一样,一个nil类型的slice可以简写为[]int(nil)。
一般情况下,除非特别说明,go函数对待0-length的slice应该是相同的行为,而不管它是不是nil或非nil。
slice扩容
append函数为slice增加元素,根据slice的cap来确定是否扩容,如果发生扩容,则返回的新slice和旧slice指向的不是同一个地址。通常我们返回的结果需要赋值给原来的slice,这样原slice的地址就不再使用了。
slice作为函数参数
当slice作为函数参数时,什么时候传slice,什么时候传slice的指针?
go的函数参数传递都是值转递,只是由于slice本身是引用类型,因此函数内部拷贝的参数和形参指向的是同一个地址。
但如果函数函数对slice进行了扩容,则可能导致内部slice指向新地址,则函数外的slice并不能感知到。
通俗的讲,函数内部slice的len和cap的变化,都不能改变函数外slice的len和cap,如果要同步改变,则需要用slice指针。
func mytest(s []int) {
s = append(s, 3)
fmt.Println(s)
}
func main() {
s := make([]int, 3, 5)
s[0] = 1
s[1] = 2
fmt.Println(s) // [1 2 0]
mytest(s) // [1 2 0 3]
fmt.Println("after func mytest")
fmt.Println(s) // [1 2 0] 这里已经将s[3]改为3,但原s看不到
s = s[:4]
fmt.Println(s) // [1 2 0 3]
}
Map
一个无序的字典,key必须是能使用==操作符的可比较的类型
,如整数、字符串等。
- map的零值为nil,代表没有hash表
- map的元素不是一个变量,不能取地址(不能取地址的一个原因是,map的增长将会导致已存储的元素rehash到另外的位置,因此地址会发生变化)。
- map不能直接比较,需要自己实现比较方法
- 如果想用slice作为map的key,可以利用一个函数将slice转为string,再作为key,这种方法可以试用于所有不可比较的类型。
- 可以利用map的key值不重复的特点,构建一个set,且将其value用bool或struct{}表示
_ = &ages["bob"] // compile error
func mapEqual(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
}
Struct
结构体的每个元素都是一个变量,因此可以取地址。
成员的顺序很重要,不同的顺序定义出的结构体不同
。
A named struct type S can’t declare a field of the same type S: an aggregate value cannot contain itself。(An analogous restriction applies to arrays.)
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。
(该限制同样适用于数组。)
But S may declare a field of the pointer type S, which lets us create recursive data structures like linked lists and trees.
但是S类型的结构体可以包含S指针类型的成员,这可以让我们创建递归的数据结构
// 一个排序算法
func Sort(values []int) {
var root *tree
for _, v := range values {
root = add(root, v)
}
appendValues(values[:0], root)
}
func appendValues(values []int, t *tree) []int {
if t != nil {
values = appendValues(values, t.left)
values = append(values, t.value)
values = appendValues(values, t.right)
}
return values
}
func add(t *tree, value int) *tree {
if t == nil {
t = new(tree)
t.value = value
}
if value < t.value {
t.left = add(t.left, value)
} else {
t.right = add(t.right, value)
}
return t
}
如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。
有些Go语言程序员用map来模拟set数据结构时,用它来代替map中布尔类型的value,
只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所以我们通常会避免这样的用法。
函数返回一个结构体,则不能使用.形式调用结构体的元素,返回结构体指针则可以。
如果结构体的所有成员都是可比较的,则结构体是可比较的。比较时按顺序比较各个字段
。
如果结构体可比较,则可以作为map的key值。
匿名成员: 当成员只有类型,不指定名字时为匿名成员。使用点号操作符可以直接抵达最底层的叶子名称。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术