https://dave.cheney.net/2018/07/12/slices-from-the-ground-up
作者 | Dave Cheney
譯者 | Name1e5s (name1e5s) ???共計翻譯:19 篇 貢獻時間:930 天
這篇文章受到了我與同事討論使用切片作為棧的一次聊天的啟發。後來話題聊到了 Go 語言中的切片是如何工作的。我認為這些資訊對別人也有用,所以就把它記錄了下來。
陣列
任何關於 Go 語言切片的討論都要從另一個資料結構也就是陣列開始。Go 的陣列有兩個特性:
[5]int
是由 5 個 int
構成的陣列,和 [3]int
不同。
package main
import "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基於第二個特性,兩個切片可以享有共同的底層陣列。看下麵的示例:
package main
import "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 main
import "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 重寫的這一小節的第一個示例:
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 darwin
Type "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]
以及使用 Ruby 重寫的版本:
irb(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
=> 0
irb(main):004:0> a
=> [1, 2, 0, 4, 5]
在大多數將陣列視為物件或者是取用型別的語言也是如此。4
切片頭
切片同時擁有值和指標特性的神奇之處在於理解切片實際上是一個結構體型別。通常在反射包內相應部分之後[1]的這個結構體被稱作切片頭。切片頭的定義大致如下:
package runtime
type slice struct {
ptr unsafe.Pointer
len int
cap int
}
這很重要,因為和 map
以及 chan
這兩個型別不同[1],切片是值型別,當被賦值或者被作為引數傳入函式時候會被覆制過去。
程式員們都能理解 square
的形參 v
和 main
中宣告的 v
的是相互獨立的。請看下麵的例子:
package main
import "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
的指標。
package main
import "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
}
Go 的切片是作為值傳遞而不是指標這一點不太尋常。當你在 Go 內定義一個結構體時,90% 的時間裡傳遞的都是這個結構體的指標5 。切片的傳遞方式真的很不尋常,我能想到的唯一與之相同的例子只有 time.Time
。
切片作為值傳遞而不是作為指標傳遞這一特殊行為會讓很多想要理解切片的工作原理的 Go 程式員感到困惑。你只需要記住,當你對切片進行賦值、取切片、傳參或者作為傳回值等操作時,你是在複製切片頭結構的三個欄位:指向底層陣列的指標、長度,以及容量。
總結
我們來用引出這一話題的切片作為棧的例子來總結下本文的內容:
package main
import "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
作為 level
0 。在函式 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 部落格裡的這些文章:
相關文章:
len
函式,但是其結果本來就人盡皆知。 ↩via: https://dave.cheney.net/2018/07/12/slices-from-the-ground-up
作者:Dave Cheney[9] 譯者:name1e5s 校對:pityonline
本文由 LCTT 原創編譯,Linux中國 榮譽推出