从零开始学习 Go 语言的切片

2020-07-09 00:00:00 函数 数组 指针 底层 切片

这篇文章受到了我与同事讨论使用 切片(slice)作为 栈(stack)的一次聊天的启发。后来话题聊到了 Go 语言中的切片是如何工作的。我认为这些信息对别人也有用,所以就把它记录了下来。

数组

任何关于 Go 语言切片的讨论都要从另一个数据结构也就是 数组(array)开始。Go 的数组有两个特性:

  1. 数组的长度是固定的;[5]int 是由 5 个 int 构成的数组,和 [3]int 不同。
  2. 数组是值类型。看下面这个示例:
    package main import "fmt" func main() { var a [5]int b := a b[2] = 7 fmt.Println(a, b) // prints [0 0 0 0 0] [0 0 7 0 0] }
    语句 b := a 定义了一个类型是 [5]int 的新变量 b,然后把 a 中的内容 复制b中。改变 ba 中的内容没有影响,因为 ab 是相互独立的值。1

切片

Go 语言的切片和数组的主要有如下两个区别:

  1. 切片没有一个固定的长度。切片的长度不是它类型定义的一部分,而是由切片内部自己维护的。我们可以使用内置的 len 函数知道它的长度。2
  2. 将一个切片赋值给另一个切片时 不会 对切片内容进行复制操作。这是因为切片没有直接持有其内部数据,而是保留了一个指向 底层数组 3 的指针。数据都保留在底层数组里。

基于第二个特性,两个切片可以享有共同的底层数组。看下面的示例:

  1. 对切片取切片
    package main import "fmt" func main() { var a = []int{1,2,3,4,5} b := a[2:] b[0] = 0 fmt.Println(a, b) // prints [1 2 0 4 5] [0 4 5] }
    在这个例子里,ab 享有共同的底层数组 —— 尽管 b 在数组里的起始偏移量不同,两者的长度也不同。通过 b 修改底层数组的值也会导致 a 里的值的改变。
  2. 将切片传进函数
    package main import "fmt" func negate(s []int) { for i := range s { s[i] = -s[i] } } func main() { var a = []int{1, 2, 3, 4, 5} negate(a) fmt.Println(a) // prints [-1 -2 -3 -4 -5] }
    在这个例子里,a 作为形参 s 的实参传进了 negate 函数,这个函数遍历 s 内的元素并改变其符号。尽管 nagate 没有返回值,且没有访问到 main 函数里的 a。但是当将之传进 negate 函数内时,a 里面的值却被改变了。

大多数程序员都能直观地了解 Go 语言切片的底层数组是如何工作的,因为它与其它语言中类似数组的工作方式类似。比如下面就是使用 Python 重写的这一小节的个示例:

Python 2.7.10 (default, Feb  7 2017, 00:08:15)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> a = [1,2,3,4,5]
>>> b = a
>>> b[2] = 0
>>> a
[1, 2, 0, 4, 5]

相关文章