Go基础|第8章:array / slice

  • 原创
  • Madman
  • /
  • /
  • 0
  • 5868 次阅读

Golang.jpg

Synopsis: Go 语言有 3 种数据结构可以让用户管理集合数据:数组(array)、切片(slice)和映射(map),切片类型是在 Go 的数组类型之上所构建的抽象形式(切片会引用其底层数组的一个区间),因此要了解切片,我们必须首先了解数组

1. Array

Go 语言的 数组(array) 是一个 长度固定 的数据类型,用于存储一段 具有相同的类型 的元素的 连续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是结构体类型

array.jpg

数组中的每个元素包含相同的类型,它们占用的内存是连续分配的,因此可以用固定速度 O(1) 来索引数组中的任意数据,速度非常快

1.1 声明与初始化

类型 [n]T 表示拥有 nT 类型的值的数组,数组的长度是其类型的一部分,因此数组不能改变大小

// 1. 声明一个包含 5 个 int 类型元素的数组
var a [5]int  // [0 0 0 0 0]

只声明数组时,数组内每个元素都是对应类型的零值,比如上面的 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. 下标从 0 开始,获取第 2 个元素的值
fmt.Println(a[1])

// 2. 修改第 2 个元素的值
a[1] = 8

数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值:

var a [5]int
b := [5]int{10, 20, 30, 40, 50}
a = b  // 将 b 的值复制给 a,此时 a 和 b 都是 [10 20 30 40 50]

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)
  }
}

输出如下:

Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
Index: 4 Value: 50

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 文件:

package arr

// Foo 接受一个指向 100 万个整型值的数组的指针
func Foo(a *[1000000]int) *[1000000]int {
  return a
}

然后再在 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() 的形参改为数组:

package arr

func Foo(a [1000000]int) [1000000]int {
  return a
}

再次执行基准测试(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):

slice 01.jpg

从切片所指向的底层数组的起始元素开始计算 ,从左往右直到数组末尾的元素个数就是切片的容量

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 02.jpg

可以通过 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:

slice 03.jpg

如果切片超过了数组的边界则会报错,比如 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() 来创建切片,它接收三个参数:切片类型、长度、可选的容量

func make([]T, len, cap) []T

如果只指定长度,那么切片的容量和长度相等:

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],因为切片指向了新的底层数组
}

slice 04.jpg

如果切片的底层数组没有足够的可用容量时,append(s, 70, 80) 会创建一个新的底层数组,先将原切片所引用的值复制到新数组里,再追加新的值:

slice 05.jpg

把切片 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 所指向的数组中:

func copy(dst, src []Type) int

复制的元素个数是 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)
  }
}

输出如下:

Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
Index: 4 Value: 50

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] 时,会扩展切片的长度,从而又可以看到底层数组前面的值了

参考: https://blog.golang.org/go-slices-usage-and-internals

未经允许不得转载: LIFE & SHARE - 王颜公子 » Go基础|第8章:array / slice

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

0 条评论

暂时还没有评论.