Merge pull request #9884 from wxy/20180712-Slices-from-the-ground-up

PRF&PUB:20180712 Slices from the ground up
This commit is contained in:
pityonline 2018-08-19 22:27:38 +08:00 committed by GitHub
commit 53b38aefbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,4 +1,4 @@
从零开始学习切片
从零开始学习 Go 语言的切片
======
这篇文章受到了我与同事讨论使用<ruby>切片<rt>slice</rt></ruby>作为<ruby><rt>stack</rt></ruby>的一次聊天的启发。后来话题聊到了 Go 语言中的切片是如何工作的。我认为这些信息对别人也有用,所以就把它记录了下来。
@ -30,7 +30,7 @@
Go 语言的切片和数组的主要有如下两个区别:
1. 切片没有一个固定的长度。切片的长度不是它类型定义的一部分,而是由切片内部自己维护的。我们可以使用内置的 `len` 函数知道它的长度。[^2]
2. 将一个切片赋值给另一个切片时 _不会_ 对切片进行复制操作。这是因为切片没有直接保存它的内部数据,而是保留了一个指向 _底层数组_ [^3] 的指针。数据都保留在底层数组里。
2. 将一个切片赋值给另一个切片时 _不会_ 对切片内容进行复制操作。这是因为切片没有直接持有其内部数据,而是保留了一个指向 _底层数组_ [^3] 的指针。数据都保留在底层数组里。
基于第二个特性,两个切片可以享有共同的底层数组。看下面的示例:
@ -71,7 +71,7 @@ Go 语言的切片和数组的主要有如下两个区别:
}
```
在这个例子里,`a` 作为形参 `s` 的实参传进了 `negate` 函数,这个函数遍历 `s` 内的元素并改变其符号。尽管 `nagate` 没有返回值,且没有接触`main` 函数里的 `a`。但是当将之传进 `negate` 函数内时,`a` 里面的值却被改变了。
在这个例子里,`a` 作为形参 `s` 的实参传进了 `negate` 函数,这个函数遍历 `s` 内的元素并改变其符号。尽管 `nagate` 没有返回值,且没有访问`main` 函数里的 `a`。但是当将之传进 `negate` 函数内时,`a` 里面的值却被改变了。
大多数程序员都能直观地了解 Go 语言切片的底层数组是如何工作的,因为它与其它语言中类似数组的工作方式类似。比如下面就是使用 Python 重写的这一小节的第一个示例:
@ -103,7 +103,7 @@ irb(main):004:0> a
### 切片头
切片同时拥有值和指针特性的神奇之处在于理解切片实际上是一个<ruby>结构体<rt>struct</rt></ruby>类型。这个结构体通常叫做 _切片头_,这里是[<ruby>反射<rt>reflect</rt></ruby>包内的相关定义][7]。切片头的定义大致如下:
切片同时拥有值和指针特性的神奇之处在于理解切片实际上是一个<ruby>结构体<rt>struct</rt></ruby>类型。通常在[<ruby>反射<rt>reflect</rt></ruby>包内相应部分之后][7]的这个结构体被称作<ruby>切片头<rt>slice header</rt></ruby>。切片头的定义大致如下:
![](https://dave.cheney.net/wp-content/uploads/2018/07/slice.001-300x257.png)
@ -155,9 +155,9 @@ func main() {
}
```
Go 的切片是作为值传递的这一点很是不寻常。当你在 Go 内定义一个结构体时90% 的时间里传递的都是这个结构体的指针[^5]。切片的传递方式真的很不寻常,我能想到的唯一与之相同的例子只有 `time.Time`
Go 的切片是作为值传递而不是指针这一点不太寻常。当你在 Go 内定义一个结构体时90% 的时间里传递的都是这个结构体的指针[^5] 。切片的传递方式真的很不寻常,我能想到的唯一与之相同的例子只有 `time.Time`
切片作为值传递而不是作为指针传递这一特殊行为会让很多想要理解切片的工作原理的 Go 程序员感到困惑,这是可以理解的。你只需要记住,当你对切片进行赋值,取切片,传参或者作为返回值等操作时,你是在复制切片头结构的三个字段:指向底层数组的指针长度,以及容量。
切片作为值传递而不是作为指针传递这一特殊行为会让很多想要理解切片的工作原理的 Go 程序员感到困惑。你只需要记住,当你对切片进行赋值、取切片、传参或者作为返回值等操作时,你是在复制切片头结构的三个字段:指向底层数组的指针长度,以及容量。
### 总结
@ -182,7 +182,7 @@ func main() {
}
```
`main` 函数的最开始我们把一个 `nil` 切片以及 `level` 的值 0 传给了函数 `f`。在函数 `f` 里我们把当前的 `level` 添加到切片的后面,之后增加 `level` 的值并进行递归。一旦 `level` 大于 5函数返回打印出当前的 `level` 以及它们复制到的 `s` 的内容。
`main` 函数的最开始我们把一个 `nil` 切片传给了函数 `f` 作为 `level` 0 。在函数 `f` 里我们把当前的 `level` 添加到切片的后面,之后增加 `level` 的值并进行递归。一旦 `level` 大于 5函数返回打印出当前的 `level` 以及它们复制到的 `s` 的内容。
```
level: 5 slice: [0 1 2 3 4 5]
@ -193,7 +193,7 @@ level: 1 slice: [0 1]
level: 0 slice: [0]
```
你可以注意到在每一个 `level``s` 的值没有被别的 `f` 的调用影响,尽管当计算更高的 `level` 时作为 `append` 的副产品,调用栈内的四个 `f` 函数创建了四个底层数组[^6],但是没有影响到当前各自的切片。
你可以注意到在每一个 `level``s` 的值没有被别的 `f` 的调用影响,尽管当计算更高的 `level` 时作为 `append` 的副产品,调用栈内的四个 `f` 函数创建了四个底层数组[^6] ,但是没有影响到当前各自的切片。
### 扩展阅读
@ -202,10 +202,8 @@ level: 0 slice: [0]
* [Go Slices: usage and internals][5] (blog.golang.org)
* [Arrays, slices (and strings): The mechanics of 'append'][6] (blog.golang.org)
### 注释
[^1]: 这不是数组才有的特性,在 Go 语言里中 _一切_ 赋值都是复制过去的。
[^2]: 你也可以在对数组使用 `len` 函数,但是得到的结果是多少人尽皆知。
[^2]: 你也可以在对数组使用 `len` 函数,但是其结果本来就人尽皆知。
[^3]: 有时也叫做<ruby>后台数组<rt>backing array</rt></ruby>,以及更不严谨的说法是后台切片。
[^4]: Go 语言里我们倾向于说值类型以及指针类型,因为 C++ 的<ruby>引用<rt>reference</rt></ruby>类型这个词产生误会。但在这里我认为调用数组作为引用类型是没有问题的。
[^5]: 如果你的结构体有[定义在其上的方法或者用于满足某个接口][7],那么你传入结构体指针的比率可以飙升到接近 100%。