这篇文章受到了我与同事讨论使用切片slice作为栈stack的一次聊天的启发。后来话题聊到了 Go 语言中的切片是如何工作的。我认为这些信息对别人也有用,所以就把它记录了下来。
-- Dave Cheney
致谢
编译自 |
https://dave.cheney.net/2018/07/12/slices-from-the-ground-up
作者 | Dave Cheney
译者 | Name1e5s (name1e5s) 共计翻译:19篇 贡献时间:930 天
这篇文章受到了我与同事讨论使用切片slice作为栈stack的一次聊天的启发。后来话题聊到了 Go 语言中的切片是如何工作的。我认为这些信息对别人也有用,所以就把它记录了下来。
数组
任何关于 Go 语言切片的讨论都要从另一个数据结构也就是数组array开始。Go 的数组有两个特性:
1. 数组的长度是固定的; 是由 5 个 构成的数组,和 不同。
2.数组是值类型。看下面这个示例:
语句 定义了一个类型是 的新变量 ,然后把 中的内容复制到 中。改变 对 中的内容没有影响,因为 和 是相互独立的值。1
切片
Go 语言的切片和数组的主要有如下两个区别:
1. 切片没有一个固定的长度。切片的长度不是它类型定义的一部分,而是由切片内部自己维护的。我们可以使用内置的 函数知道它的长度。2
2. 将一个切片赋值给另一个切片时不会对切片内容进行复制操作。这是因为切片没有直接持有其内部数据,而是保留了一个指向底层数组3的指针。数据都保留在底层数组里。
基于第二个特性,两个切片可以享有共同的底层数组。看下面的示例:
1.对切片取切片
在这个例子里, 和 享有共同的底层数组 —— 尽管 在数组里的起始偏移量不同,两者的长度也不同。通过 修改底层数组的值也会导致 里的值的改变。
2.将切片传进函数
在这个例子里, 作为形参 的实参传进了 函数,这个函数遍历 内的元素并改变其符号。尽管 没有返回值,且没有访问到 函数里的 。但是当将之传进 函数内时, 里面的值却被改变了。
大多数程序员都能直观地了解 Go 语言切片的底层数组是如何工作的,因为它与其它语言中类似数组的工作方式类似。比如下面就是使用 Python 重写的这一小节的第一个示例:
以及使用 Ruby 重写的版本:
在大多数将数组视为对象或者是引用类型的语言也是如此。4
切片头
切片同时拥有值和指针特性的神奇之处在于理解切片实际上是一个结构体struct类型。通常在反射reflect包内相应部分之后
[1]
的这个结构体被称作切片头slice header。切片头的定义大致如下:
这很重要,因为和以及 这两个类型不同
[1]
,切片是值类型,当被赋值或者被作为参数传入函数时候会被复制过去。
程序员们都能理解 的形参 和 中声明的 的是相互独立的。请看下面的例子:
因此 对自己的形参 的操作没有影响到 中的 。下面这个示例中的 也是 中声明的切片 的独立副本,而不是指向 的 的指针。
Go 的切片是作为值传递而不是指针这一点不太寻常。当你在 Go 内定义一个结构体时,90% 的时间里传递的都是这个结构体的指针5。切片的传递方式真的很不寻常,我能想到的唯一与之相同的例子只有 。
切片作为值传递而不是作为指针传递这一特殊行为会让很多想要理解切片的工作原理的 Go 程序员感到困惑。你只需要记住,当你对切片进行赋值、取切片、传参或者作为返回值等操作时,你是在复制切片头结构的三个字段:指向底层数组的指针、长度,以及容量。
总结
我们来用引出这一话题的切片作为栈的例子来总结下本文的内容:
在 函数的最开始我们把一个 切片传给了函数 作为 0 。在函数 里我们把当前的 添加到切片的后面,之后增加 的值并进行递归。一旦 大于 5,函数返回,打印出当前的 以及它们复制到的 的内容。
你可以注意到在每一个 内 的值没有被别的 的调用影响,尽管当计算更高的 时作为 的副产品,调用栈内的四个 函数创建了四个底层数组6,但是没有影响到当前各自的切片。
领取专属 10元无门槛券
私享最新 技术干货