[譯] part 11: goalng 陣列和切片

咔嘰咔嘰發表於2019-04-16

這一章我們將學習 Go 語言中的陣列和切片。

陣列

陣列是屬於同一型別的元素的集合。例如,整數 5, 8, 9, 79, 76 的集合可以構成陣列。Go 中的陣列不允許混合不同型別的值,例如包含字串和整數。

陣列的宣告

陣列的表示為[n] T。n 表示陣列中元素的數量,T 表示每個元素的型別。元素的數量 n 也是型別的一部分(我們將在稍後更詳細地討論它。)

有多種宣告陣列的方法,我們一個一個來看看,

package main

import (  
    "fmt"
)


func main() {  
    var a [3]int //int array with length 3
    fmt.Println(a)
}
複製程式碼

Run in playground var a [3]int 宣告瞭一個長度為 3 的整型陣列,該陣列所有的元素會被自動初始化為陣列型別的零值。在這個例子中陣列的型別為整數,所以 a 陣列所有的元素會被初始化為 0。執行程式碼會輸出,[0 0 0]

陣列的索引從 0 開始到陣列的長度減 1,我們來給陣列 a 賦一些值,

package main

import (  
    "fmt"
)


func main() {  
    var a [3]int //int array with length 3
    a[0] = 12 // array index starts at 0
    a[1] = 78
    a[2] = 50
    fmt.Println(a)
}
複製程式碼

Run in playground

賦值後,程式將輸出[12 78 50]

我們也可以使用簡短宣告來建立一個陣列。

package main 

import (  
    "fmt"
)

func main() {  
    a := [3]int{12, 78, 50} // short hand declaration to create array
    fmt.Println(a)
}
複製程式碼

Run in playground

該程式和上一個輸出的一樣[12 78 50]

在簡短宣告的時候,不需要為陣列中的所有元素分配值。

package main

import (  
    "fmt"
)

func main() {  
    a := [3]int{12} 
    fmt.Println(a)
}
複製程式碼

Run in playground

上述程式碼的第 8 行a := [3]int{12}宣告瞭一個長度為 3 的陣列,但是隻提供了一個值 12,剩下的兩個元素將會被零值自動填充,程式將輸出,[12 0 0]

在宣告陣列的時候,甚至可以不指定長度,而用...替代,該方法編譯器會幫你計算出長度。來看看下面的程式碼,

package main

import (  
    "fmt"
)

func main() {  
    a := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a)
}
複製程式碼

Run in playground

陣列的大小是型別的一部分。因此[5] int[25] int是不同的型別。因此,陣列的大小是無法調整的。不要擔心這個問題,因為切片就是為了解決這個問題。

package main

func main() {  
    a := [3]int{5, 78, 8}
    var b [5]int
    b = a //not possible since [3]int and [5]int are distinct types
}
複製程式碼

Run in playground

在上述程式的第 6 行,我們嘗試把[3]int型別的值賦給[5]int,這是不允許的,所以編譯器將會報錯main.go:6: cannot use a (type [3]int) as type [5]int in assignment.

陣列是值傳遞型別

Go 中的陣列是值傳遞型別而不是引用傳遞型別。這意味著當它們賦值給新變數時,會將原始陣列的副本賦給新變數。如果對新變數進行了更改,則它不會改變原來的陣列。

package main

import "fmt"

func main() {  
    a := [...]string{"USA", "China", "India", "Germany", "France"}
    b := a // a copy of a is assigned to b
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}
複製程式碼

Run in playground

上述程式的第 7 行,把a賦值給了b,第 8 行將b陣列的第一個元素修改為 Singapore,這不會影響a。所以會輸出,

a is [USA China India Germany France]  
b is [Singapore China India Germany France]  
複製程式碼

類似地,當陣列作為引數傳遞給函式時,它們按值傳遞,原陣列也不會變。

package main

import "fmt"

func changeLocal(num [5]int) {  
    num[0] = 55
    fmt.Println("inside function ", num)

}
func main() {  
    num := [...]int{5, 6, 7, 8, 8}
    fmt.Println("before passing to function ", num)
    changeLocal(num) //num is passed by value
    fmt.Println("after passing to function ", num)
}
複製程式碼

Run in playground

上述程式的第 13 行,我們將陣列num當作引數傳遞給了函式changeLocal。函式呼叫後將不會改變原num陣列的值,程式輸出,

before passing to function  [5 6 7 8 8]  
inside function  [55 6 7 8 8]  
after passing to function  [5 6 7 8 8]  
複製程式碼
陣列的長度

陣列的長度通過把陣列當引數傳遞給len函式來計算。

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    fmt.Println("length of a is",len(a))

}
複製程式碼

Run in playground

上述程式碼將輸出,length of a is 4

使用 range 迭代陣列

可以用for迴圈來迭代一個陣列的所有元素。

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ { //looping from 0 to the length of the array
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}
複製程式碼

Run in playground 上述程式碼使用了for迴圈迭代了陣列的所有元素,該程式將輸出,

0 th element of a is 67.70  
1 th element of a is 89.80  
2 th element of a is 21.00  
3 th element of a is 78.00  
複製程式碼

Go 提供了一種更好,更簡潔的方法,通過使用for迴圈的range形式迭代陣列。 range返回索引和該索引處的值。讓我們使用range重寫上面的程式碼,並計算所有元素的和。

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    sum := float64(0)
    for i, v := range a {//range returns both the index and value
        fmt.Printf("%d the element of a is %.2f\n", i, v)
        sum += v
    }
    fmt.Println("\nsum of all elements of a",sum)
}
複製程式碼

Run in playground

上述程式碼的第 8 行,for i, v := range a就是該迴圈的形式。它將返回索引和該索引處的值。我們列印值並計算陣列a的所有元素的和。該程式的輸出是,

0 the element of a is 67.70  
1 the element of a is 89.80  
2 the element of a is 21.00  
3 the element of a is 78.00

sum of all elements of a 256.5  
複製程式碼

在該例子中如果你只想要值,你可以使用_佔位符替代返回索引的位置。

for _, v := range a { //ignores index  
}
複製程式碼

類似的,值也可以被忽略。

多維陣列

目前位置我們建立的都是一維陣列,當然也可以建立多維陣列。

package main

import (  
    "fmt"
)

func printarray(a [3][2]string) {  
    for _, v1 := range a {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}

func main() {  
    a := [3][2]string{
        {"lion", "tiger"},
        {"cat", "dog"},
        {"pigeon", "peacock"}, //this comma is necessary. The compiler will complain if you omit this comma
    }
    printarray(a)
    var b [3][2]string
    b[0][0] = "apple"
    b[0][1] = "samsung"
    b[1][0] = "microsoft"
    b[1][1] = "google"
    b[2][0] = "AT&T"
    b[2][1] = "T-Mobile"
    fmt.Printf("\n")
    printarray(b)
}
複製程式碼

Run in playground

在上面的程式中的第 17 中,使用簡短語法宣告瞭二維字串陣列a。第 20 行末尾的逗號是必要的,這是因為詞法分析器根據簡單的規則自動插入分號。如果您有興趣瞭解更多,可以閱讀https://golang.org/doc/effective_go.html#semicolons。

另一個二維陣列b在第 23 行被宣告。通過每個索引逐個新增字串是初始化二維陣列的另一種方法。

第 7 行中的printarray函式,使用兩個for range迴圈來列印二維陣列的內容。上述程式的輸出是

lion tiger  
cat dog  
pigeon peacock 

apple samsung  
microsoft google  
AT&T T-Mobile  
複製程式碼

這就是陣列。儘管陣列似乎足夠靈活,但它具有固定長度的限制,無法增加陣列的長度,而切片可以自動擴充長度。事實上,在 Go 中,切片比陣列更常見。

切片

切片是方便,靈活且功能強大的基於陣列的裝飾器。切片本身不擁有任何資料。它們只是對現有陣列的引用。

建立切片

一個元素型別為T的切片用[ ]T表示。

package main

import (  
    "fmt"
)

func main() {  
    a := [5]int{76, 77, 78, 79, 80}
    var b []int = a[1:4] //creates a slice from a[1] to a[3]
    fmt.Println(b)
}
複製程式碼

Run in playground

語法a[start:end]表示用陣列a索引的startend - 1來建立一個切片,所以上述程式第 9 行的a[1:4]建立了一個表示陣列a從索引 1 到 3 的切片(左閉右開區間)。因此,切片b的值為[77 78 79]

讓我們看下建立切片的另一種方式,

package main

import (  
    "fmt"
)

func main() {  
    c := []int{6, 7, 8} //creates and array and returns a slice reference
    fmt.Println(c)
}
複製程式碼

Run in playground

上述程式碼的第 9 行,c := []int{6, 7, 8}建立了一個 3 個整數的切片 c`。

修改切片

切片不擁有自己的任何資料。它只是底層陣列的表示,對切​​片所做的任何修改都將反映在底層陣列中。

package main

import (  
    "fmt"
)

func main() {  
    darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
    dslice := darr[2:5]
    fmt.Println("array before",darr)
    for i := range dslice {
        dslice[i]++
    }
    fmt.Println("array after",darr) 
}
複製程式碼

Run in playground

在上面程式的第 9 行中,我們從陣列的索引 2, 3, 4 建立了dslice切片。 for迴圈將這些索引中的值遞增 1。當我們在for迴圈之後列印陣列時,我們可以看到對切片的更改影響到了陣列。該程式的輸出是

array before [57 89 90 82 100 78 67 69 59]  
array after [57 89 91 83 101 78 67 69 59]  
複製程式碼

當許多切片共享相同的底層陣列時,每個切片所做的更改將反映在陣列中。

package main

import (  
    "fmt"
)

func main() {  
    numa := [3]int{78, 79 ,80}
    nums1 := numa[:] //creates a slice which contains all elements of the array
    nums2 := numa[:]
    fmt.Println("array before change 1",numa)
    nums1[0] = 100
    fmt.Println("array after modification to slice nums1", numa)
    nums2[1] = 101
    fmt.Println("array after modification to slice nums2", numa)
}
複製程式碼

Run in playground

在第 9 行,numa[:]中缺少了起始值和結束值。startend的預設值分別為0len(numa)。切片nums1nums2共享相同的陣列。該程式的輸出是

array before change 1 [78 79 80]  
array after modification to slice nums1 [100 79 80]  
array after modification to slice nums2 [100 101 80]  
複製程式碼

從輸出中可以清楚地看出,當切片共享同一個陣列時,每個陣列所做的修改都會反映在陣列中。

切片的長度和容量

切片的長度是指切片中元素的數量。切片的容量是指從切片的起始索引值開始到陣列末尾的元素數。

寫個程式碼來加深理解,

package main

import (  
    "fmt"
)

func main() {  
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
}
複製程式碼

Run in playground

在上面的程式中,fruitslice是用fruitarray的索引值 1 和 2 建立的。因此,長度為 2。

fruitarray的長度為 7。fruiteslicefruitarray的索引 1 開始建立。因此,fruiteslice的容量是從索引 1 開始的fruitarray中的元素,即從orange開始,該值為 6。因此,fruiteslice的容量為 6。該程式輸出length of slice 2 capacity 6.

切片可以重新切片到他的最大容量。如果超過容量將會導致程式執行時丟擲錯誤。

package main

import (  
    "fmt"
)

func main() {  
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
    fruitslice = fruitslice[:cap(fruitslice)] //re-slicing furitslice till its capacity
    fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
}
複製程式碼

Run in playground

在第 11 行,fruitslice被重新切片到他的最大容量,程式將輸出,

length of slice 2 capacity 6  
After re-slicing length is 6 and capacity is 6  
複製程式碼
使用 make 函式建立切片

func make([]T, len, cap) []T 通過傳遞型別,長度,容量來建立一個切片。容量引數是可選的,預設值是長度。

make函式建立一個陣列並返回一個切片引用。

package main

import (  
    "fmt"
)

func main() {  
    i := make([]int, 5, 5)
    fmt.Println(i)
}
複製程式碼

Run in playground 使用make函式建立的切片預設會被零值填充,上述程式將輸出[0 0 0 0 0]

為切片增加元素

我們知道陣列是固定長度的,並且它們的長度不能擴容。而切片是動態的,可以使用append函式將新元素增加到切片。func append(s []T, x ...T) []Tappend函式的定義。

函式中的x ...T參數列示函式可以接受一個變長的引數x,這種型別的函式稱為變參函式

但有一個問題可能會困擾你。切片的底層依賴了陣列,而陣列本身是固定長度,那麼切片是怎麼實現動態長度的呢?實現原理是,當新元 素新增到切片時,會建立一個新陣列。現有陣列的元素將複製到此新陣列,並返回此新陣列的新切片引用。新切片的容量現在是舊切片的兩倍(譯者注:有一個擴容演算法,並不都是兩倍),下面的程式碼將會讓我更容易理解。

package main

import (  
    "fmt"
)

func main() {  
    cars := []string{"Ferrari", "Honda", "Ford"}
    fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) //capacity of cars is 3
    cars = append(cars, "Toyota")
    fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) //capacity of cars is doubled to 6
}
複製程式碼

Run in playground

在上述程式中,cars的容量最初為 3。我們在第 8 行中為cars新增了一個新元素,並把append(cars, "Toyota")返回的切片賦值給cars。現在cars的容量增加了一倍,變成了 6。上述程式的輸出是

cars: [Ferrari Honda Ford] has old length 3 and capacity 3  
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6  
複製程式碼

切片的零值是nil,一個nil切片的長度和容量都為 0。能使用append函式給nil切片新增元素。

package main

import (  
    "fmt"
)

func main() {  
    var names []string //zero value of a slice is nil
    if names == nil {
        fmt.Println("slice is nil going to append")
        names = append(names, "John", "Sebastian", "Vinay")
        fmt.Println("names contents:",names)
    }
}
複製程式碼

Run in playground

names是一個nil切片,我們給names新增了 3 個字串,該程式輸出,

slice is nil going to append  
names contents: [John Sebastian Vinay]  
複製程式碼

也可以使用...操作把一個切片新增到另一個切片,可以在變參函式這一章節學到更多關於該操作的內容。

package main

import (  
    "fmt"
)

func main() {  
    veggies := []string{"potatoes","tomatoes","brinjal"}
    fruits := []string{"oranges","apples"}
    food := append(veggies, fruits...)
    fmt.Println("food:",food)
}
複製程式碼

Run in playground

在上述程式碼的第 10 行,food切片使用veggies切片新增fruits切片建立。程式的輸出為,food: [potatoes tomatoes brinjal oranges apples]

切片作為函式的引數

切片的內部結構可以被認為是結構型別。這就是像這樣,

type slice struct {  
    Length        int
    Capacity      int
    ZerothElement *byte
}
複製程式碼

切片包含長度,容量和指向陣列的第 0 個元素的指標。當切片傳遞給函式時,即使它是按值傳遞的,指標變數也會引用相同的底層陣列。因此,當切片作為引數傳遞給函式時,函式內部所做的更改也會在函式外部顯示。讓我們寫一個程式來驗證一下。

package main

import (  
    "fmt"
)

func subtactOne(numbers []int) {  
    for i := range numbers {
        numbers[i] -= 2
    }

}
func main() {  
    nos := []int{8, 7, 6}
    fmt.Println("slice before function call", nos)
    subtactOne(nos)                               //function modifies the slice
    fmt.Println("slice after function call", nos) //modifications are visible outside
}
複製程式碼

Run in playground

上述程式的第 16 行中的函式呼叫將每個切片元素遞減 2。當在函式呼叫之後列印切片時,這些修改是可見的。回想一下,這和陣列不同,在陣列中,對函式內部的陣列所做的修改在函式外部是不可見的。上述程式的輸出是,

slice before function call [8 7 6]  
slice after function call [6 5 4]  
複製程式碼
多維切片

跟陣列一樣,切片也支援多維

package main

import (  
    "fmt"
)


func main() {  
     pls := [][]string {
            {"C", "C++"},
            {"JavaScript"},
            {"Go", "Rust"},
            }
    for _, v1 := range pls {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}
複製程式碼

Run in playground

程式輸出,

C C++  
JavaScript  
Go Rust  
複製程式碼
記憶體優化

切片引用了底層陣列,只要切片在記憶體中,就不能對陣列進行垃圾回收。在記憶體管理,這可能會引起關注。我們假設我們有一個非常大的陣列,我們有興趣只處理它的一小部分。此後,我們從該陣列建立一個切片並開始處理切片。需要注意的一點是,由於切片引用了該陣列,因此陣列仍將在記憶體中。

解決此問題的一種方法是使用copy函式func copy(dst, src []T) int來獲取該切片的副本。這樣我們就可以使用新的切片,原始陣列就可以被垃圾收集了。

package main

import (  
    "fmt"
)

func countries() []string {  
    countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
    neededCountries := countries[:len(countries)-2]
    countriesCpy := make([]string, len(neededCountries))
    copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
    return countriesCpy
}
func main() {  
    countriesNeeded := countries()
    fmt.Println(countriesNeeded)
}
複製程式碼

Run in playground

在上述程式碼的第 9 行,neededCountries := countries[:len(countries)-2]建立了一個countries的切片。在第 11 行將neededCountries複製給了countriesCpy,隨後函式返回。現在,countries的底層陣列將會被垃圾回收,因為neededCountries不再被引用。

相關文章