从零开始学习 Go 语言的切片
2018-08-19 21:59
633 查看
这篇文章受到了我与同事讨论使用切片slice作为栈stack的一次聊天的启发。后来话题聊到了 Go 语言中的切片是如何工作的。我认为这些信息对别人也有用,所以就把它记录了下来。
数组的长度是固定的;
数组是值类型。看下面这个示例:
切片没有一个固定的长度。切片的长度不是它类型定义的一部分,而是由切片内部自己维护的。我们可以使用内置的
将一个切片赋值给另一个切片时 不会 对切片内容进行复制操作。这是因为切片没有直接持有其内部数据,而是保留了一个指向 底层数组 3 的指针。数据都保留在底层数组里。
基于第二个特性,两个切片可以享有共同的底层数组。看下面的示例:
对切片取切片
将切片传进函数
大多数程序员都能直观地了解 Go 语言切片的底层数组是如何工作的,因为它与其它语言中类似数组的工作方式类似。比如下面就是使用 Python 重写的这一小节的第一个示例:
程序员们都能理解
切片作为值传递而不是作为指针传递这一特殊行为会让很多想要理解切片的工作原理的 Go 程序员感到困惑。你只需要记住,当你对切片进行赋值、取切片、传参或者作为返回值等操作时,你是在复制切片头结构的三个字段:指向底层数组的指针、长度,以及容量。
Go Slices: usage and internals (blog.golang.org)
Arrays, slices (and strings): The mechanics of 'append' (blog.golang.org)
What is the zero value, and why is it useful?
The empty struct
Should methods be declared on T or *T
这不是数组才有的特性,在 Go 语言里中 一切 赋值都是复制过去的。 ↩
你也可以在对数组使用
有时也叫做后台数组backing array,以及更不严谨的说法是后台切片。 ↩
Go 语言里我们倾向于说值类型以及指针类型,因为 C++ 的引用reference类型这个词产生误会。但在这里我认为调用数组作为引用类型是没有问题的。 ↩
如果你的结构体有定义在其上的方法或者用于满足某个接口,那么你传入结构体指针的比率可以飙升到接近 100%。 ↩
证明留做习题。 ↩
via: https://dave.cheney.net/2018/07/12/slices-from-the-ground-up
作者:Dave Cheney 译者:name1e5s 校对:pityonline
数组
任何关于 Go 语言切片的讨论都要从另一个数据结构也就是数组array开始。Go 的数组有两个特性:数组的长度是固定的;
[5]int是由 5 个
int构成的数组,和
[3]int不同。
数组是值类型。看下面这个示例:
语句package mainimport "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中。改变
b对
a中的内容没有影响,因为
a和
b是相互独立的值。1
切片
Go 语言的切片和数组的主要有如下两个区别:切片没有一个固定的长度。切片的长度不是它类型定义的一部分,而是由切片内部自己维护的。我们可以使用内置的
len函数知道它的长度。2
将一个切片赋值给另一个切片时 不会 对切片内容进行复制操作。这是因为切片没有直接持有其内部数据,而是保留了一个指向 底层数组 3 的指针。数据都保留在底层数组里。
基于第二个特性,两个切片可以享有共同的底层数组。看下面的示例:
对切片取切片
在这个例子里,package mainimport "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]}
a和
b享有共同的底层数组 —— 尽管
b在数组里的起始偏移量不同,两者的长度也不同。通过
b修改底层数组的值也会导致
a里的值的改变。
将切片传进函数
在这个例子里,package mainimport "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 重写的这一小节的第一个示例:
以及使用 Ruby 重写的版本: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 darwinType "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]
在大多数将数组视为对象或者是引用类型的语言也是如此。4irb(main):001:0> a = [1,2,3,4,5]=> [1, 2, 3, 4, 5]irb(main):002:0> b = a=> [1, 2, 3, 4, 5]irb(main):003:0> b[2] = 0=> 0irb(main):004:0> a=> [1, 2, 0, 4, 5]
切片头
切片同时拥有值和指针特性的神奇之处在于理解切片实际上是一个结构体struct类型。通常在反射reflect包内相应部分之后的这个结构体被称作切片头slice header。切片头的定义大致如下:这很重要,因为和package runtimetype slice struct { ptr unsafe.Pointer len int cap int}
map以及
chan这两个类型不同,切片是值类型,当被赋值或者被作为参数传入函数时候会被复制过去。
程序员们都能理解
square的形参
v和
main中声明的
v的是相互独立的。请看下面的例子:
因此package mainimport "fmt"func square(v int) { v = v * v}func main() { v := 3 square(v) fmt.Println(v) // prints 3, not 9}
square对自己的形参
v的操作没有影响到
main中的
v。下面这个示例中的
s也是
main中声明的切片
s的独立副本, 而不是 指向
main的
s的指针。
Go 的切片是作为值传递而不是指针这一点不太寻常。当你在 Go 内定义一个结构体时,90% 的时间里传递的都是这个结构体的指针5 。切片的传递方式真的很不寻常,我能想到的唯一与之相同的例子只有package mainimport "fmt"func double(s []int) { s = append(s, s...)}func main() { s := []int{1, 2, 3} double(s) fmt.Println(s, len(s)) // prints [1 2 3] 3}
time.Time。
切片作为值传递而不是作为指针传递这一特殊行为会让很多想要理解切片的工作原理的 Go 程序员感到困惑。你只需要记住,当你对切片进行赋值、取切片、传参或者作为返回值等操作时,你是在复制切片头结构的三个字段:指向底层数组的指针、长度,以及容量。
总结
我们来用引出这一话题的切片作为栈的例子来总结下本文的内容:在package mainimport "fmt"func f(s []string, level int) { if level > 5 { return } s = append(s, fmt.Sprint(level)) f(s, level+1) fmt.Println("level:", level, "slice:", s)}func main() { f(nil, 0)}
main函数的最开始我们把一个
nil切片传给了函数
f作为
level0 。在函数
f里我们把当前的
level添加到切片的后面,之后增加
level的值并进行递归。一旦
level大于 5,函数返回,打印出当前的
level以及它们复制到的
s的内容。
你可以注意到在每一个level: 5 slice: [0 1 2 3 4 5]level: 4 slice: [0 1 2 3 4]level: 3 slice: [0 1 2 3]level: 2 slice: [0 1 2]level: 1 slice: [0 1]level: 0 slice: [0]
level内
s的值没有被别的
f的调用影响,尽管当计算更高的
level时作为
append的副产品,调用栈内的四个
f函数创建了四个底层数组6 ,但是没有影响到当前各自的切片。
扩展阅读
如果你想要了解更多 Go 语言内切片运行的原理,我建议看看 Go 博客里的这些文章:Go Slices: usage and internals (blog.golang.org)
Arrays, slices (and strings): The mechanics of 'append' (blog.golang.org)
相关文章:
If a map isn't a reference variable, what is it?What is the zero value, and why is it useful?
The empty struct
Should methods be declared on T or *T
这不是数组才有的特性,在 Go 语言里中 一切 赋值都是复制过去的。 ↩
你也可以在对数组使用
len函数,但是其结果本来就人尽皆知。 ↩
有时也叫做后台数组backing array,以及更不严谨的说法是后台切片。 ↩
Go 语言里我们倾向于说值类型以及指针类型,因为 C++ 的引用reference类型这个词产生误会。但在这里我认为调用数组作为引用类型是没有问题的。 ↩
如果你的结构体有定义在其上的方法或者用于满足某个接口,那么你传入结构体指针的比率可以飙升到接近 100%。 ↩
证明留做习题。 ↩
via: https://dave.cheney.net/2018/07/12/slices-from-the-ground-up
作者:Dave Cheney 译者:name1e5s 校对:pityonline
相关文章推荐
- go语言学习----字符串、数组和切片的应用
- Go语言学习(十)bytes包处理字节切片
- Go语言学习笔记之数组、数组切片和map
- go语言学习----字符串、数组和切片的应用
- go语言学习-数组切片的创建和使用方法
- Go语言学习(十)bytes包处理字节切片
- Go语言学习笔记 --- slice切片
- Go语言学习笔记十一: 切片(slice)
- Go语言学习笔记(三)数组 & 切片 & map
- Go语言学习笔记十一: 切片(slice)
- Go语言学习笔记1
- 在纠结新语言的学习是go还是rust
- Go语言学习示例
- GO语言学习-复杂类型(2)
- go语言学习
- Go语言学习笔记九: 指针
- 学习go语言的第四天
- go语言中数组与切片的区别
- go语言学习基础
- Go 语言切片