Go基础|第8章:array / slice
Synopsis: Go 语言有 3 种数据结构可以让用户管理集合数据:数组(array)、切片(slice)和映射(map),切片类型是在 Go 的数组类型之上所构建的抽象形式(切片会引用其底层数组的一个区间),因此要了解切片,我们必须首先了解数组
1. Array
Go 语言的 数组(array)
是一个 长度固定 的数据类型,用于存储一段 具有相同的类型 的元素的 连续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是结构体类型
数组中的每个元素包含相同的类型,它们占用的内存是连续分配的,因此可以用固定速度 O(1)
来索引数组中的任意数据,速度非常快
1.1 声明与初始化
类型 [n]T
表示拥有 n
个 T
类型的值的数组,数组的长度是其类型的一部分,因此数组不能改变大小
只声明数组时,数组内每个元素都是对应类型的零值,比如上面的 a[0] 此时等于 0
// 2. 通过数组字面量(array literal)来声明并初始化数组 a := [5]int{10, 20, 30, 40, 50} // 3. 使用 ... 代替数组的长度,由初始化的元素个数决定 a := [...]int{10, 20, 30, 40, 50} // 4. 声明数组,但只初始化其中一部分值,其余的使用对应类型的零值 a := [5]int{2: 20, 4: 40} // [0 0 20 0 40]
1.2 访问或修改数组的元素值
数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值:
1.3 迭代数组
package main import ( "fmt" ) func main() { a := [5]int{10, 20, 30, 40, 50} for index, value := range a { fmt.Printf("Index: %d Value: %d\n", index, value) } }
输出如下:
1.4 传递数组
在 Go 语言里,数组是一个值。如果数组中的元素本来就是一个指针的话,复制数组时,只会复制指针的值,而不会复制指针所指向的值:
package main import ( "fmt" ) func main() { // 1. 声明包含 3 个元素的指向 int 的指针数组 var a1 [3]*int // 2. 声明另一个包含 3 个元素的指向 int 的指针数组 a2 := [3]*int{new(int), new(int), new(int)} *a2[0] = 10 *a2[1] = 20 *a2[2] = 30 // 3. 将 a2 复制给 a1 a1 = a2 fmt.Println(a1) // [0x414020 0x414024 0x414028] fmt.Println(a2) // [0x414020 0x414024 0x414028] }
在函数间传递数组是一个开销很大的操作,如果只传入指向数组的指针,效率就会高很多。假设有个 arr 包,它下面有个 arr.go 文件:
然后再在 arr 包下创建 arr_test.go 文件:
package arr_test import ( "madmalls.com/go-utils/arr" "testing" ) func BenchmarkFoo(b *testing.B) { var a [1000000]int for n := 0; n < b.N; n++ { arr.Foo(&a) // 传入数组的指针 } }
此时,执行基准测试(0.398 ns/op):
D:\MyCode\go-utils\arr>go test -bench=. . goos: windows goarch: amd64 pkg: madmalls.com/go-utils/arr BenchmarkFoo-4 1000000000 0.398 ns/op PASS ok madmalls.com/go-utils/arr 3.884s
如果把函数 Foo() 的形参改为数组:
再次执行基准测试(1184240 ns/op):
D:\MyCode\go-utils\arr>go test -bench=. . goos: windows goarch: amd64 pkg: madmalls.com/go-utils/arr BenchmarkFoo-4 943 1184240 ns/op PASS ok madmalls.com/go-utils/arr 1.792s
2. Slice
数组不能改变大小,有些不灵活,因此你在 Go 代码中不会经常看到它们。但是,切片无处不在
切片(slice)
是引用类型,它由 3 个字段组成,分别是指向 底层数组 的指针、切片能访问的元素的个数(长度,length)和切片允许增长到的元素个数(容量,capacity):
从切片所指向的底层数组的起始元素开始计算 ,从左往右直到数组末尾的元素个数就是切片的容量
2.1 声明与初始化
切片的类型为 []T
,其中 T
是切片元素的类型,与数组类型不同的是,切片类型没有指定长度
(1) 数组的切片
package main import ( "fmt" ) func main() { // 1. 声明并初始化一个数组 a := [5]int{10, 20, 30, 40, 50} // 2. 创建一个切片,指向底层数组中的第 2 个和第 3 个元素 s := a[1:3] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [20 30] len=2 cap=4 }
可以通过 slice(动词)现有的数组来创建一个 slice(名词):
a[low:high]
: 通过指定起始位置和结束位置来创建切片,它是左闭右开
区间,包括 low 但不包括 high- 也可以忽略起始位置或结束位置,而使用其默认值。low 的默认值为零,hight 的默认值为数组的长度
通过 slice := array[low:high]
创建的切片,它的长度为 high-low
,它的容量为 len(array)-low
s = a[:2] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20] len=2 cap=5 s = a[2:] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [30 40 50] len=3 cap=3 s = a[:] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30 40 50] len=5 cap=5
当 s := a[1:4]
时,切片的长度为 3,容量为 4:
如果切片超过了数组的边界则会报错,比如 s := a[1:6]
会报 invalid slice index 6 (out of bounds for 5-element array)
(2) 切片字面量
通过 slice literal 来声明并初始化切片:
s := []int{10, 20, 30, 40, 50} // 会自动创建底层数组 [5]int{10, 20, 30, 40, 50} fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30 40 50] len=5 cap=5
(3) make() 函数创建切片
可以使用内建函数 make()
来创建切片,它接收三个参数:切片类型、长度、可选的容量
如果只指定长度,那么切片的容量和长度相等:
s := make([]int, 5) // 相当于 []int{0, 0, 0, 0, 0} fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [0 0 0 0 0] len=5 cap=5
如果同时指定长度和容量:
s := make([]int, 3, 5) // 相当于 new([5]int)[:3] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [0 0 0] len=3 cap=5
make([]int, 3, 5)
表示分配一个具有 5 个 int 类型的数组空间,接着创建一个长度为 3、容量为 5 并指向该数组中前 3 个元素的切片结构
(4) 切片的切片
还可以通过 slice(动词)现有的旧切片来创建一个新 slice(名词),新切片不会复制旧切片的数据,而是创建一个指向同一个底层数组的新切片值(新切片指针所指的元素不同):
s[low:high]
: 通过指定起始位置和结束位置来创建切片,它是左闭右开
区间,包括 low 但不包括 high- 也可以忽略起始位置或结束位置,而使用其默认值。low 和 high 是基于旧切片的索引而不是它的底层数组,所以 low 的默认值为零,high 的默认值为旧切片的长度
- high 不受旧切片的长度限制,但受旧切片的容量限制,意味着你可以用新切片扩展旧切片的长度。如果 high 超过旧切片的容量范围,则会引发 panic 错误;同样地,无法将 low 设置为小于 0 的数来访问底层数组中的前面的元素
通过 newSlice := oldSlice[low:high]
创建的切片,它的长度为 high-low
,它的容量为 cap(oldSlice)-low
注意: Go 没有负索引
package main import ( "fmt" ) func main() { // 1. 声明并初始化一个切片 s := []int{10, 20, 30, 40, 50} fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30 40 50] len=5 cap=5 // 2. 创建切片的切片 s2 := s[1:3] // low 和 high 是基于切片 [10 20 30 40 50] 的索引 fmt.Printf("%v len=%d cap=%d\n", s2, len(s2), cap(s2)) // [20 30] len=2 cap=4 // s2[:] // low默认为0、high默认为旧切片的长度,所以 s2[:] 等价于 s2[0:2], 即 [20 30] len=2 cap=4 // 3. 扩展切片 s2 的长度 s3 := s2[:cap(s2)] // high 最大可设置为旧切片 s2 的容量 4 fmt.Printf("%v len=%d cap=%d\n", s3, len(s3), cap(s3)) // [20 30 40 50] len=4 cap=4 //s4 := s2[:5] // panic: runtime error: slice bounds out of range [:5] with capacity 4 //s5 := s2[-1:cap(s2)] // 想访问底层数组第 1 个元素值 10 ? 抱歉会报错:invalid slice index -1 (index must be non-negative) }
(5) nil 切片和空切片
package main import ( "fmt" ) func main() { // 1. 创建 nil 切片 var s1 []int fmt.Printf("%#v len=%d cap=%d\n", s1, len(s1), cap(s1)) // []int(nil) len=0 cap=0 // 2. 通过 slice literal 创建空切片 s2 := []int{} fmt.Printf("%#v len=%d cap=%d\n", s2, len(s2), cap(s2)) // []int{} len=0 cap=0 // 3. 通过 make() 创建空切片 s3 := make([]int, 0) fmt.Printf("%#v len=%d cap=%d\n", s3, len(s3), cap(s3)) // []int{} len=0 cap=0 }
不管是使用 nil 切片还是空切片,对其调用 append()
、len()
和 cap()
内置函数的效果都是一样的
2.2 访问或修改切片的元素值
切片并不存储任何数据,它只是描述了底层数组中的一个区间。更改切片的元素会修改其底层数组中对应的元素,当一个切片修改了底层数组的共享部分时,与它共享同一个底层数组的其它切片就会看到这些修改:
package main import ( "fmt" ) func main() { s1 := []int{10, 20, 30, 40, 50} fmt.Printf("%v len=%d cap=%d\n", s1, len(s1), cap(s1)) // [10 20 30 40 50] len=5 cap=5 s2 := s1[1:3] fmt.Printf("%v len=%d cap=%d\n", s2, len(s2), cap(s2)) // [20 30] len=2 cap=4 s2[0] = 8 fmt.Printf("%v len=%d cap=%d\n", s1, len(s1), cap(s1)) // [10 8 30 40 50] len=5 cap=5 fmt.Printf("%v len=%d cap=%d\n", s2, len(s2), cap(s2)) // [8 30] len=2 cap=4 }
2.3 append() 函数扩容切片
可以通过内建函数 append()
来增加切片的容量,append()
函数的返回值是一个包含修改结果的新切片。函数 append()
总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量:
package main import ( "fmt" ) func main() { a := [5]int{10, 20, 30, 40, 50} s := a[1:3] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [20 30] len=2 cap=4 fmt.Println(a) // [10 20 30 40 50] s = append(s, 60) fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [20 30 60] len=3 cap=4 fmt.Println(a) // [10 20 30 60 50] s = append(s, 70, 80) fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // 切片容量翻倍了,[20 30 60 70 80] len=5 cap=8 fmt.Println(a) // 数组a没变 [10 20 30 60 50],因为切片指向了新的底层数组 }
如果切片的底层数组没有足够的可用容量时,append(s, 70, 80)
会创建一个新的底层数组,先将原切片所引用的值复制到新数组里,再追加新的值:
把切片 append 到另一个切片时,要使用
...
符号先将切片扩展开(解包)
package main import ( "fmt" ) func main() { s1 := []int{10, 20, 30} s2 := []int{40, 50} s3 := append(s1, s2...) // 等价于 append(s1, s2[0], s2[1]) fmt.Printf("%v len=%d cap=%d\n", s3, len(s3), cap(s3)) // [10 20 30 40 50] len=5 cap=8 }
2.4 copy() 函数复制切片
内置函数 copy()
将元素从源切片 src 所指向的数组复制到目标切片 dst 所指向的数组中:
复制的元素个数是 len(dst) 和 len(src) 的最小值:
package main import ( "fmt" ) func main() { var s = make([]int, 3) n := copy(s, []int{10, 20, 30, 40, 50}) fmt.Printf("n=%d\n", n) // n = 3 fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30] len=3 cap=3 // 相当于给切片 []int{10, 20, 30, 40, 50} 扩充了一倍容量 var s2 = make([]int, 10) n2 := copy(s2, []int{10, 20, 30, 40, 50}) fmt.Printf("n=%d\n", n2) // n = 5 fmt.Printf("%v len=%d cap=%d\n", s2, len(s2), cap(s2)) // [10 20 30 40 50 0 0 0 0 0] len=10 cap=10 }
最佳实践:
由于对切片重新 slice 并不会复制底层数组,所以完整的底层数组还是会保留在内存中。比如 FindDigits()
函数将文件加载到内存中并在文件中搜索第一组连续的数字,并将它们作为新切片返回:
var digitRegexp = regexp.MustCompile("[0-9]+") func FindDigits(filename string) []byte { b, _ := ioutil.ReadFile(filename) return digitRegexp.Find(b) }
但是返回的新切片还是引用了原来的包含整个文件的底层数组,所以底层数组不会被 GC 垃圾回收。要解决此问题,可以在函数返回之前将你想要的数据复制到新的切片中即可:
func FindDigits(filename string) []byte { b, _ := ioutil.ReadFile(filename) b = digitRegexp.Find(b) c := make([]byte, len(b)) copy(c, b) return c }
或者在内部使用 append()
:
func FindDigits(filename string) []byte { b, _ := ioutil.ReadFile(filename) b = digitRegexp.Find(b) //c := make([]byte, 0) var c []byte return append(c, b...) }
2.5 切片迭代
可以用 for range
来迭代切片里的元素:
package main import ( "fmt" ) func main() { s := []int{10, 20, 30, 40, 50} fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30 40 50] len=5 cap=5 for index, value := range s { fmt.Printf("Index: %d Value: %d\n", index, value) } }
输出如下:
2.6 传递切片
如果要将切片传递给函数,并想在函数内扩大切片的容量时,要么让函数值返回新切片:
package main import "fmt" func Foo(s []int, i int) []int { fmt.Printf("[Foo] old addr: %p\n", s) s = append(s, i) fmt.Printf("[Foo] new addr: %p\n", s) return s } func main() { s := make([]int, 0) // empty slice fmt.Printf("[main] addr: %p\n", s) s = Foo(s, 10) s = Foo(s, 20) s = Foo(s, 30) fmt.Printf("Result: %v", s) } /* Output: D:\MyCode\go-utils>go run transmit_slice.go [main] addr: 0x58bcf8 [Foo] old addr: 0x58bcf8 [Foo] new addr: 0xc0000120c0 [Foo] old addr: 0xc0000120c0 [Foo] new addr: 0xc0000120d0 [Foo] old addr: 0xc0000120d0 [Foo] new addr: 0xc00000c400 Result: [10 20 30] */
要么给函数传入切片的指针:
package main import "fmt" func Foo(s *[]int, i int) { fmt.Printf("[Foo] old addr: %p\n", s) *s = append(*s, i) fmt.Printf("[Foo] new addr: %p\n", s) } func main() { s := make([]int, 0) // empty slice fmt.Printf("[main] addr: %p\n", &s) Foo(&s, 10) Foo(&s, 20) Foo(&s, 30) fmt.Printf("Result: %v", s) } /* Output: D:\MyCode\go-utils>go run transmit_slice.go [main] addr: 0xc0000044a0 [Foo] old addr: 0xc0000044a0 [Foo] new addr: 0xc0000044a0 [Foo] old addr: 0xc0000044a0 [Foo] new addr: 0xc0000044a0 [Foo] old addr: 0xc0000044a0 [Foo] new addr: 0xc0000044a0 Result: [10 20 30] */
2.7 删除切片的元素
(1) 删除切片的最后一个元素
可以使用索引 len(s)-1
来访问切片 s 的最后一个元素(Go 不能像 Python 中列表中那样使用负索引),为了防止内存泄露,需要在创建不包含最后一个元素的切片前,先重置最后一个元素的值为对应类型的零值:
package main import "fmt" func main() { s := []int{10, 20, 30, 40, 50} a := s[len(s)-1] fmt.Println("Last element:", a) // Last element: 50 // Watch out for memory leaks s[len(s)-1] = 0 // Erase element (with zero value) s = s[:len(s)-1] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30 40] len=4 cap=5 }
(2) 删除切片的中间元素
假设要删除切片 s 中索引为 i 的元素,先将最后一个元素的值复制到 s[i],然后再参照章节 1 那样移除切片最后一个元素即可:
package main import "fmt" // Remove the element at index i from slice s. func remove(s []int, i int) []int { s[i] = s[len(s)-1] // Copy last element to index i // Watch out for memory leaks s[len(s)-1] = 0 // Erase element (with zero value) s = s[:len(s)-1] return s } func main() { s := []int{10, 20, 30, 40, 50} s = remove(s, 2) fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 50 40] len=4 cap=5 }
上面算法的时间复杂度为 O(1)
。还有一个时间复杂度为 O(n)
的算法,通过 copy(s[i:], s[i+1:])
将 s[i+1] 左移一位(会拷贝 len(s)-1-i
个元素),然后再删除最后一个元素:
package main import "fmt" // Remove the element at index i from slice s. func remove(s []int, i int) []int { copy(s[i:], s[i+1:]) // Shift s[i+1:] left one index // Watch out for memory leaks s[len(s)-1] = 0 // Erase element (with zero value) s = s[:len(s)-1] return s } func main() { s := []int{10, 20, 30, 40, 50} s = remove(s, 2) fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 40 50] len=4 cap=5 }
2.8 清空切片
(1) 会释放内存
只需要简单地将切片重新赋值为 nil
即可:
package main import "fmt" func main() { s := []int{10, 20, 30, 40, 50} fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30 40 50] len=5 cap=5 s = nil fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [] len=0 cap=0 }
上述 s = nil
将会释放切片引用的底层数组,后续会被 GC 垃圾回收(假设没有其它变量引用此底层数组)
(2) 会保持内存
如果想保持切片所引用的底层数组,可以对旧切片重新切片(长度为 0):
package main import "fmt" func main() { s := []int{10, 20, 30, 40, 50} fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30 40 50] len=5 cap=5 s = s[:0] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [] len=0 cap=5 s = s[:3] fmt.Printf("%v len=%d cap=%d\n", s, len(s), cap(s)) // [10 20 30] len=3 cap=5 }
由于 s = s[:0]
产生的新切片的 容量 等于旧切片的容量 5 减去切片时 low 的默认值 0,即新切片的容量为 5,所以不会释放切片引用的底层数组。后续继续切片 s = s[:3]
时,会扩展切片的长度,从而又可以看到底层数组前面的值了
0 条评论
评论者的用户名
评论时间暂时还没有评论.