Go語言實戰之切片的內部實現和基礎功能
寫在前面
嗯,學習GO,所以有了這篇文章
博文內容為《GO語言實戰》讀書筆記之一
主要涉及切片相關知識
沒事,只不過是恢復原狀罷了,我本來就是一無所有的。 ——瀨川初原《食靈零》
切片的內部實現和基礎功能
切片是一種數據結構(類似于Java的ArrayList),圍繞動態數組的概念構建的,可以按需自動增長和縮小。切片的動態增長是通過內置函數append來實現的。這個函數可以快速且高效地增長切片。還可以通過對切片再次切片來縮小一個切片的大小。
因為切片的底層內存也是在連續塊中分配的,所以切片還能獲得索引、迭代以及為垃圾回收優化的好處。
內部實現
切片是一個很小的對象,對底層數組進行了抽象,并提供相關的操作方法。切片有3個字段的數據結構,這些數據結構包含Go語言需要操作底層數組的元數據
指向底層數組的指針
切片訪問的元素的個數(即長度)
切片允許增長到的元素個數(即容量)
創建和初始化
Go語言中有幾種方法可以創建和初始化切片。是否能提前知道切片需要的容量通常會決定要如何創建切片。
make 和切片字面量
如果只指定長度,那么切片的容量和長度相等
// 其長度和容量都是 5 個元素 slice := make([]string, 5)
使用長度和容量聲明整型切片
func main() { // 其長度和容量都是 5 個元素 slice := make([]int, 3, 5) fmt.Println(slice) } ============ [Running] go run "d:\GolandProjects\code-master\demo\make.go" [0 0 0]
剩余的2 個元素可以在后期操作中合并到切片,如果基于這個切片創建新的切片,新切片會和原有切片共享底層數組,也能通過后期操作來訪問多余容量的元素。
不允許創建容量小于長度的切片,
func main() { // 其長度和容量都是 5 個元素 slice := make([]int, 5, 3) fmt.Println(slice) } ================= [Running] go run "d:\GolandProjects\code-master\demo\make.go" # command-line-arguments d:\GolandProjects\code-master\demo\make.go:10:15: len larger than cap in make([]int)
另一種常用的創建切片的方法是使用切片字面量,只是不需要指定[]運算符里的值。初始的長度和容量會基于初始化時提供的元素的個數確定.
通過切片字面量來聲明切片
slice:= [] string{"Red", "Blue", "Green", "Yellow", "Pink"} //其長度和容量都是 3 個元素 slice := []int{10, 20, 30}
當使用切片字面量時,可以設置初始長度和容量,創建長度和容量都是100 個元素的切片
使用索引聲明切片
// 使用空字符串初始化第 100 個元素 slice := []string{99: ""}
聲明數組和聲明切片的不同
// 創建有 3 個元素的整型數組 array := [3]int{10, 20, 30} // 創建長度和容量都是 3 的整型切片 slice := []int{10, 20, 30}
nil 和空切片
創建nil切片:描述一個不存在的切片時
// 創建 nil 整型切片 var slice []int
聲明空切片:表示空集合時空切片很有用
// 使用 make 創建空的整型切片 slice := make([]int, 0) // 使用切片字面量創建空的整型切片 slice := []int{}
不管是使用 nil 切片還是空切片,對其調用內置函數 append、len 和 cap 的效果都是一樣的。
使用切片
賦值和切片
對切片里某個索引指向的元素賦值和對數組里某個索引指向的元素賦值的方法完全一樣。使用[]操作符就可以改變某個元素的值
使用切片字面量來聲明切片
// 其容量和長度都是 5 個元素 slice := []int{10, 20, 30, 40, 50} // 改變索引為 1 的元素的值 slice[1] = 25
切片之所以被稱為切片,是因為創建一個新的切片就是把底層數組切出一部分
使用切片創建切片
// 其長度和容量都是 5 個元素 slice := []int{10, 20, 30, 40, 50}
使用切片創建切片,如何計算長度和容量
// 其長度和容量都是 5 個元素 slice := []int{10, 20, 30, 40, 50} // 創建一個新切片 // 其長度為 2 個元素,容量為 4 個元素 newSlice := slice[1:3]
對底層數組容量是k的切片 slice[i:j]來說
長度: j - i = 2
容量: k - i = 4
這里書里講的個人感覺不太好理解,其實類似Java中String的subString,換句話講,前開后閉(即前包后不包),切取原數組索引1到3的元素,這里的元素個數即為新的切片長度,切取的容量為原數組第一個切點到數組末尾(默認)。其實這里有第三個索引值,后面我們會講.
我們有了兩個切片,它們共享同一段底層數組,但通過不同的切片會看到底層數組的不同部分,這個和java里的List方法subList特別像,都是通控制索引來對底層數組進行切片,所以本質上,切片后的數組可以看做是原數組的視圖。
修改切片內容可能導致的結果
// 其長度和容量都是 5 個元素 slice := []int{10, 20, 30, 40, 50} // 其長度是 2 個元素,容量是 4 個元素 newSlice := slice[1:3] // 修改 newSlice 索引為 1 的元素 // 同時也修改了原來的 slice 的索引為 2 的元素 newSlice[1] = 35
表示索引越界的語言運行時錯誤
// 其長度和容量都是 5 個元素 slice := []int{10, 20, 30, 40, 50} // 其長度為 2 個元素,容量為 4 個元素 newSlice := slice[1:3] // 修改 newSlice 索引為 3 的元素 // 這個元素對于 newSlice 來說并不存在 newSlice[3] = 45
切片增長
相對于數組而言,使用切片的一個好處是,可以按需增加切片的容量。Go語言內置的 append函數會處理增加長度時的所有操作細節。
函數append總是會增加新切片的長度,而容量有可能會改變,也可能不會改變,這取決于被操作的切片的可用容量
使用append向切片增加元素
package main import ( "fmt" ) func main() { // 其長度和容量都是 5 個元素 slice := []int{10, 20, 30, 40, 50} // 創建一個新切片 // 其長度為 2 個元素,容量為 4 個元素 newSlice := slice[1:3] fmt.Println(newSlice) // 使用原有的容量來分配一個新元素 // 將新元素賦值為 60 newSlice = append(newSlice, 60) fmt.Println(newSlice) }
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" [20 30] [20 30 60] [Done] exited with code=0 in 1.28 seconds
如果切片的底層數組沒有足夠的可用容量,append函數會創建一個新的底層數組,將被引用的現有的值復制到新數組里,再追加新的值.
package main import ( "fmt" ) func main() { // 其長度和容量都是 5 個元素 slice := []int{10, 20, 30, 40, 50} // 使用原有的容量來分配一個新元素 // 將新元素賦值為 60 newSlice := append(slice, 60) fmt.Println(newSlice) }
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" [10 20 30 40 50 60] [Done] exited with code=0 in 1.236 seconds
函數append會智能地處理底層數組的容量增長。在切片的容量小于1000個元素時,總是會成倍地增加容量。一旦元素個數超過 1000,容量的增長因子會設為1.25,也就是會每次增加 25%的容量。隨著語言的演化,這種增長算法可能會有所改變。
創建切片時的 3 個索引
通過第三個索引值設置容量,如果沒有第三個索引值,默認容量是到數組最后一個。
package main import ( "fmt" ) func main() { // 創建字符串切片 // 其長度和容量都是 5 個元素 source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"} // 將第三個元素切片,并限制容量 // 其長度為 1 個元素,容量為 2 個元素 slice := source[2:3:4] fmt.Println(slice) }
為了設置容量,從索引位置 2 開始,加上希望容量中包含的元素的個數(2),就得到了第三個值 4。
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" [Plum] [Done] exited with code=0 in 0.998 seconds
設置容量大于已有容量的語言運行時錯誤
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" panic: runtime error: slice bounds out of range [::9] with capacity 5
如果在創建切片時設置切片的容量和長度一樣,就可以強制讓新切片的第一個append操作創建新的底層數組,與原有的底層數組分離。新切片與原有的底層數組分離后,可以安全地進行后續修改.
設置長度和容量一樣的好處
package main import ( "fmt" ) func main() { // 創建字符串切片 // 其長度和容量都是 5 個元素 source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"} // 將第三個元素切片,并限制容量 // 其長度為 1 個元素,容量為 1 個元素 slice := source[2:3:3] // 向 slice 追加新字符串 slice = append(slice, "Kiwi") fmt.Println(slice) }
通過設置長度和容量一樣,之后對數組的append操作都是復制原有元素新建的數組,實現了和原來數組完全隔離。
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" [Plum Kiwi] [Done] exited with code=0 in 1.286 seconds
內置函數append也是一個可變參數的函數,如果使用...運算符,可以將一個切片的所有元素追加到另一個切片里
package main import ( "fmt" ) func main() { // 創建兩個切片,并分別用兩個整數進行初始化 s1 := []int{1, 2} s2 := []int{3, 4} // 將兩個切片追加在一起,并顯示結果 fmt.Printf("%v\n", append(s1, s2...)) }
使用 Printf 時用來顯示 append 函數返回的新切片的值
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" [1 2 3 4] [Done] exited with code=0 in 1.472 second
迭代切片
既然切片是一個集合,可以迭代其中的元素。Go語言有個特殊的關鍵字range,它可以配合關鍵字for來迭代切片里的元素
使用for range迭代切片
package main import ( "fmt" ) func main() { // 創建一個整型切片 // 其長度和容量都是 4 個元素 slice := []int{10, 20, 30, 40} // 迭代每一個元素,并顯示其值 for index, value := range slice { fmt.Printf("Index: %d Value: %d\n", index, value) } }
當迭代切片時,關鍵字range 會返回兩個值。第一個值是當前迭代到的索引位置,第二個值是該位置對應元素值的一份副本
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" Index: 0 Value: 10 Index: 1 Value: 20 Index: 2 Value: 30 Index: 3 Value: 40 [Done] exited with code=0 in 1.543 seconds
需要強調的是,range 創建了每個元素的副本,而不是直接返回對該元素的引用
range 提供了每個元素的副本
使用空白標識符(下劃線)來忽略索引值
for _, value := range slice { fmt.Printf("Value: %d\n", value) }
使用傳統的for循環對切片進行迭代
package main import ( "fmt" ) func main() { // 創建一個整型切片 // 其長度和容量都是 4 個元素 slice := []int{10, 20, 30, 40} // 迭代每一個元素,并顯示其值 for index := 2; index < len(slice); index++ { fmt.Printf("Index: %d Value: %d\n", index, slice[index]) } }
有兩個特殊的內置函數len和cap,可以用于處理數組、切片和通道。對于切片,函數len返回切片的長度
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" Index: 2 Value: 30 Index: 3 Value: 40 [Done] exited with code=0 in 1.235 seconds
函數cap返回切片的容量
package main import ( "fmt" ) func main() { // 創建一個整型切片 // 其長度和容量都是 4 個元素 slice := []int{10, 20, 30, 40} // 迭代每一個元素,并顯示其值 for index := cap(slice)-1; index >= 0; index-- { fmt.Printf("Index: %d Value: %d\n", index, slice[index]) } } ======================== [Running] go run "d:\GolandProjects\code-master\demo\hello.go" Index: 3 Value: 40 Index: 2 Value: 30 Index: 1 Value: 20 Index: 0 Value: 10 [Done] exited with code=0 in 1.372 seconds
多維切片
聲明多維切片
// 創建一個整型切片的切片 slice := [][]int{{10}, {100, 200}}
組合切片的切片
package main import ( "fmt" ) func main() { // 創建一個整型切片的切片 slice := [][]int{{10}, {100, 200}} // 為第一個切片追加值為 20 的元素 slice[0] = append(slice[0], 20) fmt.Print(slice) }
Go語言里使用append函數處理追加的方式很簡明:先增長切片,再將新的整型切片賦值給外層切片的第一個元素
[Running] go run "d:\GolandProjects\code-master\demo\hello.go" [[10 20] [100 200]] [Done] exited with code=0 in 1.451 seconds
在函數間傳遞切片
在函數間傳遞切片就是要在函數間以值的方式傳遞切片。由于切片的尺寸很小,在函數間復制和傳遞切片成本也很低。讓我們創建一個大切片,并將這個切片以值的方式傳遞給函數 foo,
// 分配包含 100 萬個整型值的切片 slice := make([]int, 1e6) // 將 slice 傳遞到函數 foo slice = foo(slice) // 函數 foo 接收一個整型切片,并返回這個切片 func foo(slice []int) []int { ... return slice }
在 64位架構的機器上,一個切片需要24字節的內存:指針字段需要 8 字節,長度和容量字段分別需要 8 字節
由于與切片關聯的數據包含在底層數組里,不屬于切片本身,所以將切片復制到任意函數的時候,對底層數組大小都不會有影響。復制時只會復制切片本身,不會涉及底層數組
在函數間傳遞 24 字節的數據會非常快速、簡單。這也是切片效率高的地方。不需要傳遞指針和處理復雜的語法,只需要復制切片,按想要的方式修改數據,然后傳遞回一份新的切片副本。
Go
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。