数组
在数组字面量中,如果省略号“…” 出现在数组长度的位置,那么数组的长度由初始化数组的元素个数决定。
q := [...]int{1,2,3}
fmt.Printf("%T\n", q) // [3]int
数组的长度是数组类型的一部分,所以 [3]int 和 [4]int 是两种不同的数组类型。数组的长度必须是常量表达式,也就是说,这个表达式的值在程序编译的时候就可以确定。
q := [3]int{1,2,3}
q = [4]int{1,2,3,4} //编译错误,不可以将 [4]int 赋值给 [3]int
如果一个数组的元素类型是可比较的,那么这个数组也是可比较的,这样我们可以直接使用 == 操作符来比较两个数组,比较的值结果是两边元素的值是否完全相同。使用 != 来比较两个数组是否不同。
a := [2]int{1,2}
b := [...]int{1,2}
c := [2]int{1,3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1,2}
fmt.Println(a == d) // 编译错误:无法比较 [2]int == [3]int
在 Go 语言中,函数传递参数中,把数组和其他的类型都看成值传递,而在其他语言中,数组是隐式地使用引用传递。当然,也可以显式地传递一个数组的指针给函数,这样在函数内部对数组的修改都会反映在原始数组的上面。
// 将数组的元素清零
func zero(ptr *[32]byte) {
for i := range ptr {
ptr[i] = 0
}
}
使用数组指针是高效的,同时允许被调函数修改调用方数组的元素,但是因为数组长度是固定的,则数组本身是不可变的,我们无法为数组添加或者删除元素。
slice
数组和 slice 是紧密关联的。 slice 是轻量级的数据结构,可以用来访问数组的部分或者全部的元素,而这个数组称为 slice 的底层数组。 slice 有三个属性:指针、长度和容量。
slice 包含了指向数组元素的指针,则将一个 slice 传递给函数的时候,可以在函数内部修改底层数组的元素。
// 就地反转一个整型 slice 中的元素
func reverse(s []int) {
for i, j := 0, len(s) - 1; i < j; i, j = i + 1, j - 1 {
s[i], s[j] = s[j], s[i]
}
}
将一个 slice 左移n 个元素的简单方法是连续调用 reverse 函数三次。第一次反转前 n 个元素,第二次反转剩下的元素,最后再对整个 slice 做一次反转(如果将slice 右移 n 个元素,则先做第三次调用)
a := []int{0, 1, 2, 3, 4}
// 向左移两个元素
reverse(a[:2])
reverse(a[2:])
reverse(a)
fmt.Println(a) // [2 3 4 0 1]
和数组不同的是, slice 无法做比较,因此不能用 == 来测试两个 slice 是否拥有相同的元素。标注库里面提供了高度优化的函数 bytes.Equal 来比较两个字节 slice ([]byte)。但是对于其他类型的 slice ,我们必须自己写函数来进行比较。
slice 不能用 ==来做比较,其原因只要有两点:1、和数组元素不同,slice 的原始是非直接的,有可能 slice 可以包含它自身;2、slice 的元素不是直接的,如果底层数组元素发生改变,同一个 slice 在不同时刻会拥有不同的元素。
slice 类型的零值是 nil。值为 nil 的slice 没有对应的底层数组。值为 nil 的 slice 长度和容量都是零,但是也有非 nil 的slice 长度和容量是零。
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
如果想检查一个 slice 是否为空,那么使用 len(s) == 0, 而不是 s == nil,因为 s != nil 的情况下,slice 也有可能为空。
append 函数对理解 slice 的工作原理很重要,下面看下为 []int slice 定义的方法 appendInt:
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
// slice 仍有增长空间,扩展 slice 内容
z = x[:zlen]
} else {
// slice 已无空间,为它分配一个新的底层数组
// 为了达到分摊线性复杂性,容量扩展一倍
zcap := zlen
if zcap < 2 * len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x)
}
z[len(x)] = y
return z
}
每一次 appendInt 调用都必须检查 slice 是否有足够的容量来存储新的元素。如果 slice 容量充足,则它会定义一个新的 slice(仍然引用原始底层数组),然后将新元素 y 复制到新的位置,并返回这个新的 slice 。输入 slice x 和函数返回 slice z 拥有相同的底层数组。
如果 slice 的容量不够容纳增长的元素, appendInt 函数必须创建一个拥有足够容量的新的底层数组来存储新元素,然后将元素从 slice x 复制到这个数组,再将新元素追加到数组后面。返回值 slice z 将和输入参数 slice x 引用不同的底层数组。
slice 可以用来实现栈。给定一个空的 slice 元素 stack,可以使用 append 向 slice 尾部追加值:
stack = append(stack, v) // push v
栈的顶部是最后一个元素:
top := stack[len(stack) - 1] // 栈顶
通过弹出最后一个元素来缩减栈:
stack = stack[:len(stack) - 1] // pop
为了从 slice 中间移除一个元素,并保留剩余元素的顺序,可以使用函数 copy 来将高位索引的元素向前移动来覆盖被移除元素所在位置:
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i + 1:])
return slice[:len(slice) - 1]
}
如果不需要维持 slice 中剩余元素的顺序,可以简单地将 slice 的最后一个元素赋值给被移除元素所在的索引位置:
func remove(slice []int, i int) []int {
slice[i] = slice[len(slice) - 1]
return slice[:len(slice) - 1]
}
map
散列表是一个拥有键值对元素的无序集合。在这个集合中,键的值是唯一的,键对用的值是可以通过键来获取、更新或移除。无论这个散列表有多大,这些操作基本上是通过常量时间的键比较就可以完成。
在 Go 语言中, map 是散列表的引用, map 的类型是 map[K]V,其中 K 和 V 是字典的键和值对应的数据类型。键的类型 K 必须是可以通过操作符 == 来进行比较的数据类型,所以 map 是可以检测某一个键是否已经存在。值类型 V 没有任何限制。
map 元素不是一个变量,不可以获取它的地址。
_ = &ages["bob"] // 编译错误,无法获取 map 元素的地址
我们无法获取 map 元素的地址的一个原因是 map 的增长可能会导致已有元素被重新散列到新的存储位置,这可能使得获取的地址无效。
var ages map[string]int
fmt.Println(ages == nil) // true
fmt.Println(len(ages) == 0) // true
大多数的 map 操作都可以安全地在 map 的零值 nil 上执行,包括查找元素,删除元素,获取 map 元素个数(len),执行 range 循环,因为这和空 map 的行为一致。但是向零值 map 中设置元素会导致错误:
ages["caro"] = 21 // 宕机:为零值 map 中的项赋值
设置元素前,必须初始化 map.
通过下标的方式访问 map 中的元素总是会有值的。如果键在 map 中,你将得到键对应的值;如果键不在 map 中,你将得到 map 值类型的零值。
结构体
结构体类型的值可以通过结构体字面量来设置,即通过结构体的成员变量来设置。
type Point struct {
X, Y int
}
p := Point{1, 2}
使用结构体字面量来设置结构体的值的方法也绕过不可导出变量无法在其他包中使用的规则。
package p
type T struct{a, b int}
package q
import "p"
var _ = p.T{a:1, b: 2} // 编译错误,无法引用a、b
var _ = p.T{1,2} // 编译错误,无法引用 a、b
由于通常结构体都通过指针的方式使用,因此可以使用一种简单的方式来创建、初始化一个struct 类型的变量并获取它的地址:
pp := &Point{1, 2}
// 这个等价于:
pp := new(Point)
*pp = Point{1,2}
如果结构体的所有成员变量都可以比较,那么这个结构体就是可比较的。两个结构体的比较可以使用 == 或者 != ,其中 == 操作符按照顺序比较两个结构体变量的成员变量。
和其他可比较的类型一样,可比较的结构体类型都可以作为 map 的键类型。
Go 允许我们定义不带名称的结构体成员,只需要指定类型即可;这种结构体成员称为匿名成员。这个结构体的类型必须是一个命名类型或者指向命名类型的指针。
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
var w Wheel
w.X = 8 // 等价于 w.Circle.Point.X = 8
w.Y = 8 // 等价于 w.Circle.Point.Y = 8
w.Radius = 5 // 等价于 w.Circle.Radius = 5
w.Spokes = 20
遗憾的是,结构体字面量并没有什么快捷方式来初始化结构体,下面的语句是无法通过编译的:
var w Wheel
w = Wheel{8,8,5,20} // 编译错误,未知成员变量
w = Wheel{X:8, Y:8, Radius:5, Spokes:20} // 编译错误,未知成员变量
结构体字面量必须遵循形状类型的定义,下面两种初始化方法是等价的。
var w Wheel
w = Wheel{Circle{Point{8,8},5},20}
w = Wheel{
Circle: Circle{
Point: Point{X:8, Y:8},
Radius:5,
},
Spokes:20,
}