切片

在Go语言中,我们很少直接使用数组。取而代之的是使用切片。切片是轻量的包含并表示数组的一部分的结构。 这里有几种创建切片的方式,我们来看看什么情况下使用它们。首先在数组的基础之上进行一点点变化:

scores := []int{1,4,293,4,9}

和数组声明不同的是,我们的切片没有在方括号中定义长度。为了理解两者的不同,我们来看看另一种使用make来创建切片的方式:

scores := make([]int, 10)

我们使用 make 关键字代替 new, 是因为创建一个切片不仅是只分配一段内存(这个是 new关键字的功能)。具体来讲,我们必须要为一个底层数组分配一段内存,同时也要初始化这个切片。在上面的代码中,我们初始化了一个长度是 10 ,容量是 10 的切片。长度是切片的长度,容量是底层数组的长度。在使用 make 创建切片时,我们可以分别的指定切片的长度和容量:

scores := make([]int, 0, 10)

上面的代码创建了一个长度是 0 ,容量是 10 的切片。(如果你仔细观察的话,你会注意到 makelen 重载了。Go 的一些特性没有暴露出来给开发者使用,这也许会让你感到沮丧。)

为了更好的理解切片的长度和容量之间的关系,我们来看下面的的例子:

func main() {
  scores := make([]int, 0, 10)
  scores[7] = 9033
  fmt.Println(scores)
}

我们上面的这个例子不能运行,为什么呢?因为切片的长度是 0 。没错,底层数组可以放 10 个元素,但是我们需要显式的扩展切片,才能访问到底层数组的元素。一种扩展切片的方式是通过 append的关键字来实现:

func main() {
  scores := make([]int, 0, 10)
  scores = append(scores, 5)
  fmt.Println(scores) // prints [5]
}

但是那并没有改变原始代码的意图。追加一个值到长度为0的切片中将会设置第一个元素。无论什么原因,我们崩溃的代码想去设置索引为7的元素值。为了实现这个,我们可以重新切片:

func main() {
  scores := make([]int, 0, 10)
  scores = scores[0:8]
  scores[7] = 9033
  fmt.Println(scores)
}

我们可以调整的切片大小最大范围是多少呢?达到它的容量,这个例子中,是10。你可能在想 这实际上并没有解决数组固定长度的问题。但是 append 是相当特别的。如果底层数组满了,它将创建一个更大的数组并且复制所有原切片中的值(这个就很像动态语言 PHP,Python,Ruby,JavaScript 的工作方式)。这就是为什么上面的例子中我们必须重新将 append 返回的值赋值给 scores 变量:append 可能在原有底层数组空间不足的情况下创建了新值。

如果我告诉你 Go 使用 2x 算法来增加数组长度,你猜下面将会打印什么?

func main() {
  scores := make([]int, 0, 5)
  c := cap(scores)
  fmt.Println(c)

  for i := 0; i < 25; i++ {
    scores = append(scores, i)

    // 如果容量改变了
    // Go 必须增加数组长度来容纳新的数据
    if cap(scores) != c {
      c = cap(scores)
      fmt.Println(c)
    }
  }
}

初始 scores 的容量是5。为了存储25个值,它必须扩展三次容量,分别是 10,20,最终是40。

最后一个例子,考虑这个:

func main() {
  scores := make([]int, 5)
  scores = append(scores, 9332)
  fmt.Println(scores)
}

这里输出是 [0, 0, 0, 0, 0, 9332],可能你觉得是[9332, 0, 0, 0, 0]?对一个用户而言,这可能逻辑上是正确的。然而,对于一个编译器,你告诉他的是追加一个值到一个已经有5个值的切片。

最终,这有四种方式初始化一个切片:

names := []string{"leto", "jessica", "paul"}
checks := make([]bool, 10)
var names []string
scores := make([]int, 0, 20)

什么时候该用哪个呢?第一个不用过多解释。当你事先知道数组中的值的时候,你可以使用这个方式。

当你想要写入切片具体的索引时,第二个方法很有用,例如:

func extractPowers(saiyans []*Saiyans) []int {
  powers := make([]int, len(saiyans))
  for index, saiyan := range saiyans {
    powers[index] = saiyan.Power
  }
  return powers
}

第三个版本是指向空的切片,用于当元素数量未知时与 append 连接。

最后一个版本是让我们声明一个初始的容量。如果我们大概知道元素的数量将是很有用的。

即使当你知道大小的时候,append 也可以使用,取决于个人偏好:

func extractPowers(saiyans []*Saiyans) []int {
  powers := make([]int, 0, len(saiyans))
  for _, saiyan := range saiyans {
    powers = append(powers, saiyan.Power)
  }
  return powers
}

切片作为数组的包装是一个很强大的概念。许多语言有切片数组的概念。JavaScript 和 Ruby 数组都有一个 slice 方法。Ruby 中你可以使用 [START..END] 获取一个切片,或者 Python 中可以通过 [START:END] 实现。然而,在这些语言中,一个切片实际上是复制了原始值的新数组。如果我们使用 Ruby,下面这段代码的输出是什么呢?

scores = [1,2,3,4,5]
slice = scores[2..4]
slice[0] = 999
puts scores

答案是 [1, 2, 3, 4, 5] 。那是因为 slice 是一个新数组,并且复制了原有的值。现在,考虑 Go 中的情况:

scores := []int{1,2,3,4,5}
slice := scores[2:4]
slice[0] = 999
fmt.Println(scores)

输出是 [1, 2, 999, 4, 5]

这改变了你编码的方式。例如,许多函数采用一个位置参数。JavaScript 中,如果你想去找到字符串中前五个字符后面的第一个空格(当然,在Go中切片也可以用于字符串),我们会这样写:

haystack = "the spice must flow";
console.log(haystack.indexOf(" ", 5));

在 Go 中,我们这样使用切片:

strings.Index(haystack[5:], " ")

我们可以从上面的例子中看到,[X:]从 X 到结尾 的简写,然而 [:X]从开始到 X 的简写。不像其他的语言,Go 不支持负数索引。如果我们想要切片中除了最后一个元素的所有值,可以这样写:

scores := []int{1, 2, 3, 4, 5}
scores = scores[:len(scores)-1]

上面是从未排序的切片中移除元素的有效方法的开始:

func main() {
  scores := []int{1, 2, 3, 4, 5}
  scores = removeAtIndex(scores, 2)
  fmt.Println(scores) // [1 2 5 4]
}

// 不会保持顺序
func removeAtIndex(source []int, index int) []int {
  lastIndex := len(source) - 1
  // 交换最后一个值和想去移除的值
  source[index], source[lastIndex] = source[lastIndex], source[index]
  return source[:lastIndex]
}

最后,我们已经了解了切片,我们再看另一个通用的内建函数:copy。正常情况下,将值从一个数组复制到另一个数组的方法有5个参数,sourcesourceStartcountdestinationdestinationStart。使用切片,我们仅仅需要两个:

import (
  "fmt"
  "math/rand"
  "sort"
)

func main() {
  scores := make([]int, 100)
  for i := 0; i < 100; i++ {
    scores[i] = int(rand.Int31n(1000))
  }
  sort.Ints(scores)

  worst := make([]int, 5)
  copy(worst, scores[:5])
  fmt.Println(worst)
}

花点时间试试上面的代码,并尝试改动。去看看如果你这么做 copy(worst[2:4], scores[:5]) 或者复制多于或少于 5 个值给 worst 会发什么?

热门教程

最新教程