Go語言入門經典:數組與切片詳解
數組和切片是Go語言中兩種重要的數據結構。數組是一種固定長度的集合,而切片則是一種靈活的動態集合。本章將詳細講解數組和切片的定義、初始化、訪問元素、動態操作等內容,幫助讀者全面掌握這兩種數據結構。
1 數組
數組是一種集合,它將一定數量且類型相同的對象放到一起,形成一個整體。數組在定義時就會確定元素的個數,初始化之后無法更改元素數量。
1.1 數組的初始化
數組實例可以使用以下幾種格式來初始化:
var x [4]byte
var x = [n]T{...}
x := [n]T{...}
其中,n
表示元素的個數,即數組對象的長度。n
是一個表達式,其計算結果必須是 int
類型的常量,而且不能出現負值。
如果數組在聲明時沒有進行初始化,就會為每個元素分配一個默認值。例如:
var f [5]uint32{18, 75, 42, 3, 105}
變量 f
初始化后,數組中第一個元素為 18
,第二個元素為 75
,第三個元素為 42
,依此類推。
如果只想為第一個元素賦值,其他元素保留默認值(即 0
),那么初始化語句可以進行簡化:
var f = [5]uint32{18,}
也可以定義變量后,通過元素索引來逐個賦值。元素索引從 0
開始,即第一個元素的索引為 0
,第二個元素的索引為 1
,依此類推。
var r [4]float32
r[0] = 1.112
r[1] = 0.000054
r[2] = 370.303
r[3] = -16.75
按照語法規則,數組變量在定義時已確定類型,數組中所有元素都必須是同一類型。因此,下面代碼所示的初始化方式會發生錯誤:
a := [2]uint{1.7, 33}
數組變量 a
的元素類型被聲明為 uint
(無符號整數),而初始化時第一個元素的值是浮點數值,與數組所定義的類型不符。
不過,如果將數組變量聲明為空接口(interface{}
)類型,那么其中的元素就可以是任意類型的值了。
s := [3]interface{}{"abc", 887, 'H'}
這說明接口的動態類型機制也適用于數組。例如:
type music interface {play()pause()
}type popMusic struct {}
func (x popMusic) play() {fmt.Println("開始播放流行音樂")
}
func (x popMusic) pause() {fmt.Println("暫停播放流行音樂")
}type classicMusic struct {}
func (x classicMusic) play() {fmt.Println("開始播放古典音樂")
}
func (x classicMusic) pause() {fmt.Println("暫停播放古典音樂")
}func main() {// 初始化數組實例var arr = [2]music{popMusic{}, classicMusic{}}// 調用數組實例中各個元素的方法arr[0].play()arr[0].pause()arr[1].play()arr[1].pause()
}
在 main
函數中,數組變量 arr
的長度為 2
,并且指定元素類型為 music
接口。由于接口類型的兼容性,在初始化數組實例時可以使用 popMusic
結構體或者 classicMusic
結構體。
在數組初始化時也可以不指定長度,通過元素個數自動確定數組長度。例如:
r := [...]int32{800, 500, 1600, 2400, 900, 700}
根據所賦值的元素個數,自動推斷出數組長度為 6
,即 [6]int32
。
1.2 訪問數組元素
通過索引可以隨機訪問數組元素,索引值必須為 int
類型數值,不能是負值。索引范圍為 [0, n-1]
(n
為數組長度)。
下面示例中,創建了一個包含 5
個元素的數組實例,然后通過索引讀取最后兩個元素:
var x = [5]rune{'a', 'e', 'i', 'o', 'u'}
// 倒數第二個元素,索引為 3
last1 := x[3]
// 最后一個元素,索引為 4
last2 := x[4]
fmt.Printf("最后兩個元素: %c, %c\n", last1, last2)
最后兩個元素的索引分別為 3
和 4
。此示例還可以這樣處理:
var x = [5]rune{'a', 'e', 'i', 'o', 'u'}
// 獲取數組的長度
n := len(x)
// 最后一個元素的索引為 n-1,倒數第二個的索引為 n-2
last1 := x[n-2]
last2 := x[n-1]
fmt.Printf("最后兩個元素: %c, %c\n", last1, last2)
len
是內置函數,其作用是獲得數組的長度。隨后,最后兩個元素的索引可以由 n
的值來確定。
上述示例的運行結果如下:
最后兩個元素: o, u
如果想順序訪問數組中的所有元素,可以使用 for
循環。
第一種格式是使用帶三個子句的 for
循環,通過一個臨時變量來存儲索引值。例如:
arr1 := [4]float32{0.11, 0.23, 5.001, 12.63}
for i := 0; i < len(arr1); i++ {fmt.Println(arr1[i])
}
初始化子句將變量 i
的值設置為 0
,可訪問數組中第一個元素。執行循環的條件子句指定 i
的值應小于數組的長度(即最大值為 n-1
),每一輪循環后將變量 i
的值加 1
。
第二種格式是與 range
子句一起使用。
arr2 := [3]string{"zh-CN", "en-US", "zh-TW"}
for index, value := range arr2 {fmt.Printf("索引: %d, 值: %s\n", index, value)
}
range
子句從數組中取出一個子項,其中包含兩個值——元素的索引和元素的值。
1.3 [n]T
與 *[n]T
的區別
[n]T
與 *[n]T
這兩種聲明格式看起來很像,但它們的含義是完全不同的。
*[n]T
:指針類型,存放類型為[n]T
的實例內存地址。[n]*T
:數組類型,其元素類型為指向int
數值的指針類型(*int
)。
可以通過兩個簡單的示例來說明。先看第一個示例,定義數組變量 d
,元素類型為 float32
,數組長度為 3
。
var d = [3]float32{0.001, 0.002, 0.003}
再定義變量 pd
,賦值時通過取地址運算符 &
獲取數組實例的內存地址。返回的類型是 *[3]float32
。
var pd = &d
變量 pd
是指針類型,它的值是數組實例 d
的內存地址。
下面是第二個例子。定義三個變量并初始化,類型都是 int
。
var a, b, c = 50, 60, 70
接著定義變量 ax
,初始化時引用上述三個變量的內存地址。
var ax = [3]*int{&a, &b, &c}
變量 ax
為數組類型,它的元素是 *int
類型。
1.4 多維數組
多維數組指的是維度為二或二以上的數組。Go語言的多維數組更像是“數組的數組”,例如,A數組中包含元素 B,而元素 B 本身也是一個數組。
二維數組的表示形式為:
[m][n]Type
相當于:
[m]([n]Type)
三維數組的表示形式為:
[m][n][o]Type
相當于:
[m]([n]([o]Type))
讀取或修改多維數組的元素,也可以通過索引來完成。
a[m][n] = x
y = a[x][y][z]
下面代碼分別演示了二維數組和三維數組的使用。
// 二維數組
var a = [2][4]uint8{{12, 13, 14, 15},{16, 17, 18, 19},
}// 輸出各元素
fmt.Println("----- 二維數組中的元素 -----")
for i := 0; i < 2; i++ {for j := 0; j < 4; j++ {fmt.Printf("%d ", a[i][j])}fmt.Println() // 換行
}
fmt.Println()// 三維數組
var b = [5][4][3]int32{{{1, 2, 3},{7, 8, 9},{12, 15, 18},{25, 26, 27},},{{-2, -3, -6},{-20, 35, -7},{60, 62, 64},{-100, -101, -102},},{{65, 66, 67},{305, 405, 505},{125, 135, 145},{-6, -17, 810},},{{2200, 130, -96},{-72, 160, 400},{215, -76, -320},{57, 58, 59},},{{8850, 3756, 418},{-600, -520, 307},{2125, 1102, -4720},{-595, -116, 907},},
}fmt.Println("----- 三維數組中的元素 -----")
for i := 0; i < 5; i++ {for j := 0; j < 4; j++ {for k := 0; k < 3; k++ {fmt.Printf("%d ", b[i][j][k])}fmt.Println() // 換行}fmt.Println("\n") // 換行
}
運行代碼后,得到結果如下:
----- 二維數組中的元素 -----
12 13 14 15
16 17 18 19----- 三維數組中的元素 -----
1 2 3
7 8 9
12 15 18
25 26 27-2 -3 -6
-20 35 -7
60 62 64
-100 -101 -10265 66 67
305 405 505
125 135 145
-6 -17 8102200 130 -96
-72 160 400
215 -76 -320
57 58 598850 3756 418
-600 -520 307
2125 1102 -4720
-595 -116 907
2 切片
切片(slice
)與數組類似,但要比數組靈活,可以在運行階段動態地添加元素,在實際開發中會用得比較多。
切片類型的底層是通過數組來存儲元素的。這個數組實例既可以是代碼中已經定義的,也可以由應用程序隱式產生的。
2.1 創建切片實例
以下幾種方法都可以創建切片實例:
- 從現有的(代碼中已定義過的)數組實例中“截取”出新的切片實例。格式如下:
s := a[L:H]
數組實例 a
中被提取的元素索引范圍為 L <= index < H
。例如:
var x = [5]int32{2, 4, 6, 8, 10}
s := x[2:4]
變量 x
為數組對象,共 5
個元素,切片對象 s
從 x
中提取索引為 2
和 3
的元素(即第三、第四個元素),所以 s
中包含的元素為 6
和 8
。
如果將上述代碼做以下修改,那么 s2
中就包含 6
、8
、10
三個元素。
s2 := a[2:5]
索引讀取范圍為 2 <= index < 5
,即被使用的索引為 2
、3
、4
。
將 L
和 H
兩個值省略,表示使用數組中的所有元素。
s3 := a[:]
從同一個數組實例產生的所有切片實例都會共享數組中的元素,也就是說,當數組中的元素被更改,切片中對應的元素也會同步更新;反過來,如果切片中的元素被更改,數組中對應的元素也會同步更新。以下示例代碼將說明這一點。
// 實例化一個數組對象
src := [4]uint32{10, 20, 30, 40}
// 從數組產生兩個切片實例
s1 := src[0:2]
s2 := src[1:4]fmt.Println("----- 修改數組前 -----")
fmt.Printf("數組: %v\n", src)
fmt.Printf("切片 1: %v\n", s1)
fmt.Printf("切片 2: %v\n", s2)// 修改數組中的元素
src[0] = 100
src[2] = 300
fmt.Println("\n----- 修改數組后 -----")
fmt.Printf("數組: %v\n", src)
fmt.Printf("切片 1: %v\n", s1)
fmt.Printf("切片 2: %v\n", s2)// 修改切片中的元素
s1[1] = 700
s2[2] = 900
fmt.Println("\n----- 修改切片后 -----")
fmt.Printf("數組: %v\n", src)
fmt.Printf("切片 1: %v\n", s1)
fmt.Printf("切片 2: %v\n", s2)
數組 src
包含 4
個元素,切片 s1
使用了數組中前兩個元素(索引是 0
和 1
);切片 s2
使用了第二、三、四個元素(索引為 1
、2
、3
)。這段代碼的運行結果如下:
----- 修改數組前 -----
數組: [10 20 30 40]
切片 1: [10 20]
切片 2: [20 30 40]----- 修改數組后 -----
數組: [100 20 300 40]
切片 1: [100 20]
切片 2: [20 300 40]----- 修改切片后 -----
數組: [100 700 300 900]
切片 1: [100 700]
切片 2: [700 300 900]
數組中的第一個元素被修改為 100
,切片 s1
的第一個元素也同步更新為 100
;同理,數組中第三個元素被修改為 300
,切片 s2
的第二個元素也同步更新為 300
。對切片實例的修改也會同步到數組實例上,因為切片 s1
、s2
都是以數組 src
為存儲基礎的,它們共享數組中的元素。
- 直接初始化。格式與數組接近,示例如下:
var (s = []string{"how", "do", "you", "do"}t = []float64{999.0000065, -73.30000082}
)
切片的初始化表達式中不需要指定元素個數(長度),但一對空白中括號([]
)必須保留。
- 使用
make
函數。
s := make([]byte, 30)
第一個參數指定要創建實例的類型,此處必須指明是切片類型。因為 make
函數不僅可以創建切片(slice
)實例,也可以創建通道(channel
)、映射(map
)實例。第二個參數指定切片的長度。上述代碼中,創建了一個長度為 30
的切片,而且每個元素都會使用 byte
類型的默認值來初始化。
2.2 添加和刪除元素
向切片添加元素,可以調用 append
函數。函數原型如下:
func append(slice []Type, elems ...Type) []Type
slice
參數是要追加元素的切片實例,elems
是個數可變的參數,它表示要添加到切片實例中的元素,可以是一個元素,也可以是多個元素。
如果切片所引用的基礎數組有足夠的容量容納新添加的元素,那么 append
函數將原來的切片實例返回;如果基礎數組的容量不足,append
函數會創建新的數組實例并分配更大的空間,然后把舊數組實例的元素復制到新實例中,并添加新的元素,最后返回由新數組實例所產生的切片實例。
在調用 append
函數前,代碼不需要驗證切片的容量是否足夠,因為 append
函數會自動處理。但是,為了在調用 append
函數后能夠獲得最新的切片實例,一般會把 append
函數返回的實例重新賦值給切片類型的變量。就像下面這樣:
s = append(s, ...)
下面請看一個示例。
先定義一個函數,用來向屏幕輸出切片實例的長度與容量。
func printSliceInfo(s []float32) {fmt.Printf("長度:%d, 容量:%d, 元素列表:%v\n", len(s), cap(s), s)
}
len
函數獲取的是切片實例的長度,cap
函數獲取的是切片實例的容量,長度是指切片中可以被訪問的元素個數,而容量是指應用程序為切片的基礎數組所分配的空間。為了保證有足夠的空間,容量必須大于或等于長度。
初始化一個切片實例,它包含兩個元素。然后多次調用 append
函數向切片實例添加元素。
var sf = []float32{0.001, 0.0007}
printSliceInfo(sf)
// 添加一個元素
sf = append(sf, 0.0014)
printSliceInfo(sf)
// 添加兩個元素
sf = append(sf, 0.0008, 0.1205)
printSliceInfo(sf)
// 添加三個元素
sf = append(sf, 0.0275, 1.302, 5.0071)
printSliceInfo(sf)
得到的輸出結果如下:
長度:2, 容量:2, 元素列表:[0.001 0.0007]
長度:3, 容量:4, 元素列表:[0.001 0.0007 0.0014]
長度:5, 容量:8, 元素列表:[0.001 0.0007 0.0014 0.0008 0.1205]
長度:8, 容量:8, 元素列表:[0.001 0.0007 0.0014 0.0008 0.1205 0.0275 1.302 5.0071]
如果使用 make
函數來創建切片實例,可以為其設置一個默認的容量(初始容量)。當然,隨著元素的添加,容量會自動增長。
var s = make([]string, 0, 10)
fmt.Printf("初始化后,長度:%d, 容量:%d\n", len(s), cap(s))
// 添加 50 個元素
for i := 1; i <= 50; i++ {str := fmt.Sprintf("Item %d", i)s = append(s, str)
}
fmt.Printf("添加 50 個元素后,長度:%d, 容量:%d\n", len(s), cap(s))
切片實例 s
初始化的容量為 10
,長度為 0
,即基礎數組分配了可容納 10
個元素的空間,但其中包含元素個數為 0
。如果將 make
函數的調用代碼做以下修改,那么創建的切片實例中已包含 3
個元素,這 3
個元素都分配了 string
類型的默認值(空字符串)。
var s = make([]string, 3, 10)
標準庫沒有提供用于刪除切片元素的函數,但是,可以通過截取元素來實現。例如:
var s = []int{1, 2, 3, 4, 5}
fmt.Printf("初始元素列表:%v\n", s)// 截取除最后一個元素外的所有元素
s = s[0 : len(s)-1]
fmt.Printf("刪掉最后一個元素后:%v\n", s)
上面代碼中,切片實例 s
有 5
個元素,截取時從索引 0
開始,到 len(s)-1
,即 [0:4]
,這樣一來,被提取到新切片實例的元素為 1
、2
、3
、4
,刪除最后一個元素的目的便實現了。
輸出結果如下:
初始元素列表:[1 2 3 4 5]
刪掉最后一個元素后:[1 2 3 4]
下面的例子將演示如何刪除切片實例中的前兩個元素。
var s = []int{1, 2, 3, 4, 5}
fmt.Printf("初始元素列表:%v\n", s)
// 刪除前兩個元素
s = s[2:]
fmt.Printf("刪除前兩個元素后:%v\n", s)
[2:]
表示從索引 2
(第三個元素)開始截取,直到最后一個元素。這樣一來就刪除了前兩個元素,結果如下:
初始元素列表:[1 2 3 4 5]
刪除前兩個元素后:[3 4 5]
總結
通過本章的學習,讀者應該對Go語言中的數組和切片有了全面的了解。數組提供固定長度的集合,適合需要明確邊界的數據結構;而切片則提供了靈活的動態集合,適合需要動態擴展的數據結構。在實際開發中,根據需求選擇合適的結構,可以提高代碼的效率和可維護性。希望本章的內容能夠幫助讀者在Go語言的學習和應用中取得更大的進步。