Go语言核心36讲 07 | 数组和切片

加韭猫(Garfield Li) / 2024-01-22 / 原文

从本篇文章开始,我们正式进入了模块2的学习。在这之前,我们已经聊了很多的Go语言和编程方面的基础知识,相信你已经对Go语言的开发环境配置、常用源码文件写法,以及程序实体(尤其是变量)及其相关的各种概念和编程技巧(比如类型推断、变量重声明、可重名变量、类型断言、类型转换、别名类型和潜在类型等)都有了一定的理解。

它们都是我认为的Go语言编程基础中比较重要的部分,同时也是后续文章的基石。如果你在后面的学习过程中感觉有些吃力,那可能是基础仍未牢固,可以再回去复习一下。


我们这次主要讨论Go语言的数组(array)类型和切片(slice)类型。数组和切片有时候会让初学者感到困惑。

它们的共同点是都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素)。

不过,它们最重要的不同是:数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。

数组的长度在声明它的时候就必须给定,并且之后不会再改变。可以说,数组的长度是其类型的一部分。比如,[1]string[2]string就是两个不同的数组类型。

而切片的类型字面量中只有元素的类型,而没有长度。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减小。

(数组与切片的字面量)

我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。

也正因为如此,Go语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而Go语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。

注意,Go语言里不存在像Java等编程语言中令人困惑的“传值或传引用”问题。在Go语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。

如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。

我们在数组和切片之上都可以应用索引表达式,得到的都会是某个元素。我们在它们之上也都可以应用切片表达式,也都会得到一个新的切片。

我们通过调用内建函数len,得到数组和切片的长度。通过调用内建函数cap,我们可以得到它们的容量。

但要注意,数组的容量永远等于其长度,都是不可变的。切片的容量却不是这样,并且它的变化是有规律可寻的。

下面我们就通过一道题来了解一下。我们今天的问题就是:怎样正确估算切片的长度和容量?

为此,我编写了一个简单的命令源码文件demo15.go。

package main

import "fmt"

func main() {
	// 示例1。
	s1 := make([]int, 5)
	fmt.Printf("The length of s1: %d\n", len(s1))
	fmt.Printf("The capacity of s1: %d\n", cap(s1))
	fmt.Printf("The value of s1: %d\n", s1)
	s2 := make([]int, 5, 8)
	fmt.Printf("The length of s2: %d\n", len(s2))
	fmt.Printf("The capacity of s2: %d\n", cap(s2))
	fmt.Printf("The value of s2: %d\n", s2)
}

我描述一下它所做的事情。

首先,我用内建函数make声明了一个[]int类型的变量s1。我传给make函数的第二个参数是5,从而指明了该切片的长度。我用几乎同样的方式声明了切片s2,只不过多传入了一个参数8以指明该切片的容量。

现在,具体的问题是:切片s1s2的容量都是多少?

这道题的典型回答:切片s1s2的容量分别是58