一、数组
《快学 Go 语言》第 4 课 —— 低调的数组
Go 语言里面的数组其实很不常用,这是因为数组是定长的静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不能相互转换相互赋值,用起来多有不方便之处。
切片是动态的数组,是可以扩充内容增加长度的数组。当长度不变时,它用起来就和普通数组一样。当长度不同时,它们也属于相同的类型,之间可以相互赋值。这就决定了数组的应用领域都广泛地被切片取代了。
1.只声明的话,全部是零值
func main() {
var a [9]int
fmt.Println(a)
}
------------
[0 0 0 0 0 0 0 0 0]
三种声明方式,给初值方式是一样的
var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
c := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
//[0,10,20,0,0]
array := [5]int{1:10,2:20}
2.下标访问
func main() {
var squares [9]int
for i := 0; i < len(squares); i++ {
squares[i] = (i + 1) * (i + 1)
}
fmt.Println(squares)
}
--------------------
[1 4 9 16 25 36 49 64 81]
3.数组赋值
同样的子元素类型并且是同样长度的数组才可以相互赋值,否则就是不同的数组类型,不能赋值。数组的赋值本质上是一种浅拷贝操作,赋值的两个数组变量的值不会共享。
func main() {
var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
var b [9]int
b = a
a[0] = 12345
fmt.Println(a)
fmt.Println(b)
}
--------------------------
[12345 2 3 4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9]
4.range关键字来遍历
func main() {
var a = [5]int{1,2,3,4,5}
for index := range a {
fmt.Println(index, a[index])
}
for index, value := range a {
fmt.Println(index, value)
}
}
------------
0 1
1 2
2 3
3 4
4 5
0 1
1 2
2 3
3 4
4 5
每次循环迭代, range 产生一对值;索引以及在该索引处的元素值。如果不需要索引怎么办,range 的语法要求, 要处理元素, 必须处理索引。一种思路是把索引赋值给一个临时变量,如 temp , 然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。Go语言中这种情况的解决方法是用 空标识符 (blank identifier),即 _ (也就是下划线)。空标识符可用于任何语法需要变量名但程序逻辑不需要的时候, 例如, 在循环里,丢弃不需要的循环索引, 保留元素值。
for _,value := range s1{
fmt.Println(value)
}
注意:range创建了每个元素的副本,而不是直接返回对该元素的引用。range总是会从切片头部开始迭代。
5.函数间传递数组
在函数间传递变量时,总是以值的方式传递。如果变量是个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。在这方面,go语言对待数组的方式和其它很多编程语言不同,其它语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。
有一种更好且更有效的方法来处理这个操作,就是只传入指向数组的指针,只需要复制8个字节的数据,这样危险在于,共享了内存,会改变原始值。
二、切片
上图中一个切片变量包含三个域,分别是底层数组的指针、切片的长度 length 和切片的容量 capacity。切片支持 append 操作可以将新的内容追加到底层数组,也就是填充上面的灰色格子。如果格子满了,切片就需要扩容,底层的数组就会更换。
形象一点说,切片变量是底层数组的视图,底层数组是卧室,切片变量是卧室的窗户。通过窗户我们可以看见底层数组的一部分或全部。一个卧室可以有多个窗户,不同的窗户能看到卧室的不同部分。
1.切片的创建有多种方式,我们先看切片最通用的创建方法,那就是内置的 make 函数
var s1 []int = make([]int, 5, 8)
var s2 []int = make([]int, 8) // 满容切片
make 函数创建切片,需要提供三个参数,分别是切片的类型、切片的长度和容量。其中第三个参数是可选的,如果不提供第三个参数,那么长度和容量相等,也就是说切片的满容的。
使用 make 函数创建的切片内容是「零值切片」,也就是内部数组的元素都是零值。Go 语言还提供了另一个种创建切片的语法,允许我们给它赋初值。使用这种方式创建的切片是满容的。
func main() {
var s []int = []int{1,2,3,4,5} // 满容的
fmt.Println(s, len(s), cap(s))
}
---------
[1 2 3 4 5] 5 5
这种写法,和数组的定义非常相似,注意区别就在方括号里是否写了长度。
var i1 [5]int = [5]int{1,2,3,4,5}
var i2 []int = []int{1,2,3,4,5}
i1 = append(i1, 6)
i2 = append(i2,7)
编译不通过,i1用不了append方法,参数必须是slice
2.切片的赋值
切片的赋值是一次浅拷贝操作,拷贝的是切片变量的三个域,你可以将切片变量看成长度为 3 的 int 型数组,数组的赋值就是浅拷贝。拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
func main() {
var s1 = make([]int, 5, 8)
// 切片的访问和数组差不多
for i := 0; i < len(s1); i++ {
s1[i] = i + 1
}
var s2 = s1
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
// 尝试修改切片内容
s2[0] = 255
fmt.Println(s1)
fmt.Println(s2)
}
--------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5]
[255 2 3 4 5]
3.append追加
相对于数组,切片可以增加长度。当append返回时,会返回一个包含修改结果的新切片。函数append总会增加新切片的长度,而容量有可能改变,也有可能不改变,这取决于被操作切片的可用容量。
slice := []int{10,20,30,40,50}
newSlice := slice[1:3]
newSlice = append(newSlice,60);
因为newSlice在底层数组里还有额外容量可用,append操作将可用元素合并到切片的长度,并对其进行赋值。由于和原始的slice共享同一个底层数组,slice中索引为3的元素值也被改动了。
如果切片的底层数组没有足够可用容量,append函数会创建一个新的底层数组,将被引用的现有值复制到新数组里,再追加新值。
slice := []int{10,20,30,40}
newSlice = append(slice,50)
函数append会自动处理底层数组的容量增长,在切片容量小于1000个元素时,总是会成倍地增加容量。超过1000时,每次增加25%。
append可以在一次调用传递多个追加值,如果使用...运算符,可以将一个切片所有元素追加到另一个切片里
s1 := []int{1,2}
s2 := []int{3,4}
append(s1,s2...);
4.切割
func main() {
var s1 = []int{1,2,3,4,5,6,7}
// start_index 和 end_index,不包含 end_index
// [start_index, end_index)
var s2 = s1[2:5]
s2[0] = 0
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
}
------------
[1 2 0 4 5 6 7] 7 7
[0 4 5] 3 5
我们注意到子切片的内部数据指针指向了数组的中间位置,而不再是数组的开头了。子切片容量的大小是从中间的位置开始直到切片末尾的长度,母子切片依旧共享底层数组。
子切片语法上要提供起始和结束位置,这两个位置都可选的,不提供起始位置,默认就是从母切片的初始位置开始(不是底层数组的初始位置),不提供结束位置,默认就结束到母切片尾部(是长度线,不是容量线)。使用过 Python 的同学可能会问,切片支持负数的位置么,答案是不支持,下标不可以是负数。
对数组进行切割可以转换成切片,切片将原数组作为内部底层数组。也就是说修改了原数组会影响到新切片,对切片的修改也会影响到原数组。
func main() {
var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var b = a[2:6]
fmt.Println(b)
a[4] = 100
fmt.Println(b)
}
-------
[3 4 5 6]
[3 4 100 6]
5.切割的第三个参数
source := []string{"apple","orange","plum","banana","grape"};
slice := source[2:3]//plum1个元素,到结尾,3个容量
slice := source[2:3:4]//也是一个1元素,不过有4-2=2个容量
使用第3个参数将长度和容量保持一致后,再使用append操作就会创建新的底层数组,从而和原底层数组分离,这样就不用担心影响到其他切片中的数据。
6.内置 copy 函数 func copy(dst, src []T) int
copy 函数不会因为原切片和目标切片的长度问题而额外分配底层数组的内存,它只负责拷贝数组的内容,从原切片拷贝到目标切片,拷贝的量是原切片和目标切片长度的较小值 —— min(len(src), len(dst)),函数返回的是拷贝的实际长度。
func main() {
var s = make([]int, 5, 8)
for i:=0;i<len(s);i++ {
s[i] = i+1
}
fmt.Println(s)
var d = make([]int, 2, 6)
var n = copy(d, s)
fmt.Println(n, d)
}
-----------
[1 2 3 4 5]
2 [1 2]
7.range遍历
需要强调的是,range创建了每个元素的副本,而不是直接返回该元素的引用
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
value, &value, &slice[index])
}
Output:
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
因为迭代返回的变量是一个迭代过程中根据切片依次赋值的新变量,所以 value 的地址总是相同的。要想获取每个元素的地址,可以使用切片变量和索引值。
8.没有push,pop这些功能
参考[译]Go Slice 秘籍
Pop
x, a = a[len(a)-1], a[:len(a)-1]
Push
a = append(a, x)
Shift
x, a := a[0], a[1:]
Unshift
a = append([]T{x}, a...)
要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
参照上面的copy解释,slice[i+1:]的长度必定是小于slice[i:]的,所以不会出现copy过程中丢失自己想留的数据。然后copy不改变原有底层数组的len和cap,只是把数据往前覆盖了一个元素,所以必须使用return slice[:len(slice)-1]才是想要的结果。
9.可变参数
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[] int的切片。sum可以接收任意数量的int型参数:
fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"
10.清空
参考 展示不同方式清空 slice 的效果
package main
import (
"fmt"
)
func dump(letters []string) {
fmt.Printf("addr = %p\n", letters)
fmt.Println("letters = ", letters)
fmt.Println(cap(letters))
fmt.Println(len(letters))
for i := range letters {
fmt.Println(i, letters[i])
}
}
func main() {
fmt.Println("=== 基础数据 ==========")
letters := []string{"a", "b", "c", "d"}
dump(letters)
fmt.Println("=== ====== ==========")
fmt.Println("=== \"原地\"清空 ===")
fmt.Println("=== 效果:")
fmt.Println("=== 1.直接在原 slice 上操作,故无 GC 行为")
fmt.Println("=== 2.清空后 cap 值和之前相同,len 值清零")
letters = letters[:0]
dump(letters)
fmt.Println("=== 添加元素效果:基于原 slice 操作,故再未超 cap 前无需内存分配")
letters = append(letters, "e")
dump(letters)
fmt.Println("=== ====== ==========")
fmt.Println("=== 基于 nil 清空 ===")
fmt.Println("=== 效果:")
fmt.Println("=== 1.类似 C 语言中赋值空指针,原内容会被 GC 处理")
fmt.Println("=== 2.清空后 cap 值清零,len 值清零")
letters = nil
dump(letters)
fmt.Println("=== 添加元素效果:类似从无到有创建 slice")
letters = append(letters, "e")
dump(letters)
}
运行结果
=== 基础数据 ==========
addr = 0xc420070080
letters = [a b c d]
4
4
0 a
1 b
2 c
3 d
=== ====== ==========
=== "原地"清空 ===
=== 效果:
=== 1.直接在原 slice 上操作,故无 GC 行为
=== 2.清空后 cap 值和之前相同,len 值清零
addr = 0xc420070080
letters = []
4
0
=== 添加元素效果:基于原 slice 操作,故再未超 cap 前无需内存分配
addr = 0xc420070080
letters = [e]
4
1
0 e
=== ====== ==========
=== 基于 nil 清空 ===
=== 效果:
=== 1.类似 C 语言中赋值空指针,原内容会被 GC 处理
=== 2.清空后 cap 值清零,len 值清零
addr = 0x0
letters = []
0
0
=== 添加元素效果:类似从无到有创建 slice
addr = 0xc42007c270
letters = [e]
1
1
0 e