go語言之數組和切片使用及說明
Array(數組)
在 Go 語言中,數組是一種固定大小的數據結構,用于存儲同類型的元素。數組的大小在編譯時確定,定義后不能更改。
數組的注意事項:
數組是值類型,當將數組傳遞給函數時,實際上是傳遞了數組的副本。如果希望函數能夠修改原數組,可以使用指向數組的指針。
- 數組的長度是數組類型的一部分,因此 [5]int 和 [10]int 是不同類型。
- 數組在 Go 語言中是一個基本的數據結構,用于存儲固定大小的同一類型元素。
- 雖然 Go 提供了數組的支持,但在實際開發(fā)中,切片(slice)通常更受歡迎,
- 因為它們更靈活(可以動態(tài)調整大?。子谑褂?。數組主要用于需要固定大小的場景。
數組支持 “==“、”!=” 操作符,因為內存總是被初始化過的。
[n]*T表示指針數組,[n]T表示數組指針
定義數組
數組的定義語法如下:
var arrayName [size]dataType arrayName 是數組的名稱。 size 是數組的長度(固定的)。 dataType 是數組中元素的數據類型。
聲明和初始化數組
package main
import "fmt"
func main() {
// 聲明一個長度為 5 的整數數組
var numbers [5]int
// 初始化數組
numbers[0] = 1
numbers[1] = 2
numbers[2] = 3
numbers[3] = 4
numbers[4] = 5
fmt.Println("數組內容:", numbers)
}
聲明并初始化數組
可以在聲明數組時直接初始化它:
package main
import "fmt"
func main() {
// 聲明并初始化
colors := [3]string{"紅", "綠", "藍"}
fmt.Println("顏色數組:", colors)
}
使用簡短聲明
用簡短聲明也可以創(chuàng)建數組:
package main
import "fmt"
func main() {
fruits := [...]string{"蘋果", "香蕉", "橙子"} // 根據初始化的元素數量來確定數組長度
fmt.Println("水果數組:", fruits) // 輸出: 水果數組: [蘋果 香蕉 橙子]
fmt.Println("長度:", len(fruits)) // 輸出: 3
}
數組的訪問
可以通過索引訪問數組的元素,索引從 0 開始:
package main
import "fmt"
func main() {
days := [7]string{"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天"}
for i := 0; i < len(days); i++ {
fmt.Printf("索引: %d, 值: %s\n", i, days[i])
}
// 方法2:for range遍歷
for i, day := range days {
fmt.Printf("索引: %d, 值: %s\n", i, day)
}
}
數組的長度
使用內置的 len() 函數可以獲取數組的長度:
package main
import "fmt"
func main() {
numbers := [5]int{10, 20, 30, 40, 50}
fmt.Println("數組長度:", len(numbers))
}
我們還可以使用指定索引值的方式來初始化數組
func main() {
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}
多維數組
Go 語言支持多維數組,常用的如二維數組。二維數組可以被看作是數組的數組:
package main
import "fmt"
func main() {
// 聲明一個 3x3 的整數二維數組
matrix := [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
// 遍歷二維數組
for i := 0; i < len(matrix); i++ {
for j := 0; j < len(matrix[i]); j++ {
fmt.Print(matrix[i][j], " ")
}
fmt.Println()
}
}
切片(slice)
切片(Slice)是 Go 語言中一個非常重要且常用的數據類型,它是對數組的一個輕量級抽象。
切片可以動態(tài)地調整大小,靈活性更高,操作也更加簡便。切片本質上是對底層數組的一個引用。
切片的基本概念
切片由三部分組成:
- 指針:指向切片的第一個元素的地址(指向底層數組的某個位置)。
- 長度:切片中元素的數量。
- 容量:切片從其第一個元素開始到底層數組的長度。
切片的底層結構定義在 runtime 包中,具體結構如下
type slice struct {
array unsafe.Pointer
len int
cap int
}
array: 指向底層數組的指針。
len: 切片當前的長度(元素個數)。
cap: 切片的容量(底層數組的總大?。?
切片的定義
var 變量名 []切片中元素類型
package main
import "fmt"
func main() {
// 聲明切片類型
var a0 []string //聲明一個字符串切片 此時沒有初始化,是nil
var a = []string{} //聲明一個字符串切片 并初始化為空切片
var b = []int{} //聲明一個整型切片并初始化
var c = []bool{false, true} //聲明一個布爾切片并初始化
//var d = []bool{false, true} //聲明一個布爾切片并初始化
if a0 == nil {
fmt.Println("a0 is nil")
}
if a == nil {
fmt.Println("a is nil")
}
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
//fmt.Println(c == d) //切片是引用類型,不支持直接比較,只能和nil比較
}
從數組創(chuàng)建切片
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // 創(chuàng)建從索引1到索引3的切片
fmt.Println("切片:", slice) // 輸出: [2 3 4]
}
func main() {
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:3] //意思是從索引1開始,到索引3結束不包括3,但容量為2
t := a[1:3:5] //意思是從索引1開始,到索引3結束不包括3,但容量為4 容量是從切片的起始索引1到原數組的最大索引(在這里是5)之間的元素數量,包括a[4]。所以從索引1到4的元素有效。
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
}
從數組創(chuàng)建切片注意
切片的底層就是一個數組,所以我們可以基于數組通過切片表達式得到切片。
切片表達式中的low 和high 表示一個索引范圍(左包含,右不包含),
對切片再執(zhí)行切片表達式時(切片再切片),high的上限邊界是切片的容量cap(a), 而不是長度。
常量索引必須是非負的,并且可以用int類型的值表示;
對于數組或常量字符串,常量索引也必須在有效范圍內。
如果low和high兩個指標都是常數,它們必須滿足low <= high。
如果索引在運行時超出范圍,就會發(fā)生運行時panic
- //切片是對底層數組的一個視圖,并不會復制整個數組。當你從一個數組創(chuàng)建切片時,
- //切片只是創(chuàng)建了一個指向原始數組的引用,因此切片對原始數組的修改會影響到原始數組,反之亦然。
func main() {
a := [5]int{1, 2, 3, 4, 5} //定義一個數組
s := a[1:3] // s := a[low:high]
//s:[2 3] len(s):2 cap(s):4
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
//切片是對底層數組的一個視圖,并不會復制整個數組。當你從一個數組創(chuàng)建切片時,
//切片只是創(chuàng)建了一個指向原始數組的引用,因此切片對原始數組的修改會影響到原始數組,反之亦然。
s2 := s[3:4] // 索引的上限是cap(s)而不是len(s)
//s2:[5] len(s2):1 cap(s2):1
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
a[1] = 10
fmt.Printf("a:%v len(a):%v cap(a):%v\n", a, len(a), cap(a))
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}
如何不受限地通過數組創(chuàng)建切片
1. 使用 copy 函數(推薦)
copy 函數可以用來復制一個切片的內容到另一個切片。這樣你就能得到一個不受原切片影響的新切片。
package main
import "fmt"
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // 創(chuàng)建切片 s,內容為 [2, 3]
// 創(chuàng)建一個新的切片,并使用 copy 復制內容
newSlice := make([]int, len(s)) // 創(chuàng)建一個新的切片,長度與 s 相同
copy(newSlice, s) // 復制 s 的內容到 newSlice
// 現(xiàn)在,newSlice 是 s 的一個副本
fmt.Printf("newSlice before modification: %v\n", newSlice)
// 修改原數組
a[3] = 10
// 輸出結果
fmt.Printf("Original array a: %v\n", a) // a:[1 2 3 10 5]
fmt.Printf("Slice s: %v\n", s) // s: [2 3]
fmt.Printf("Copied slice newSlice: %v\n", newSlice) // newSlice: [2 3]
}
2.手動創(chuàng)建切片
另一個方法是直接手動創(chuàng)建一個新的切片,并使用原始切片的元素來初始化它
package main
import "fmt"
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // 創(chuàng)建切片 s,內容為 [2, 3]
// 手動創(chuàng)建新切片
newSlice := []int{s[0], s[1]} // 直接從 s 中取值初始化 newSlice
fmt.Printf("newSlice before modification: %v\n", newSlice)
// 修改原數組
a[3] = 10
// 輸出結果
fmt.Printf("Original array a: %v\n", a) // a:[1 2 3 10 5]
fmt.Printf("Slice s: %v\n", s) // s:[2 3]
fmt.Printf("Copied slice newSlice: %v\n", newSlice) // newSlice:[2 3]
}
使用內置函數 make 創(chuàng)建切片
package main
import "fmt"
func main() {
slice := make([]int, 5) // 創(chuàng)建一個長度為5的整數切片
fmt.Println("切片:", slice) // 輸出: [0 0 0 0 0]
// 可以指定初始容量
sliceWithCap := make([]int, 5, 10) // 長度為5,容量為10
fmt.Println("切片,容量:", len(sliceWithCap), cap(sliceWithCap)) // 輸出: 5 10
}
使用字面量創(chuàng)建切片
package main
import "fmt"
func main() {
slice := []string{"蘋果", "香蕉", "橙子"}
fmt.Println("切片:", slice) // 輸出: [蘋果 香蕉 橙子]
}
判斷切片是否為空
1. 檢查切片的長度
切片的長度可以通過len函數獲取。如果切片的長度為0,則說明切片是空的。
package main
import "fmt"
func main() {
var a []int // 聲明一個零值切片(nil切片)
b := []int{} // 空切片的初始化
fmt.Println("a is empty:", len(a) == 0) // 輸出: a is empty: true
fmt.Println("b is empty:", len(b) == 0) // 輸出: b is empty: true
// 還可以直接檢查長度
if len(a) == 0 {
fmt.Println("Slice a is empty.")
}
if len(b) == 0 {
fmt.Println("Slice b is empty.")
}
}
2. 檢查切片是否為nil
如果一個切片沒有被初始化(即沒有指向任何底層數組),它的值將是nil。你可以通過直接比較切片與nil來判斷
package main
import "fmt"
func main() {
var a []int // 聲明一個零值切片(nil切片)
b := []int{} // 一個空切片(已初始化)
// 判斷a是否為nil
if a == nil {
fmt.Println("Slice a is nil.")
} else {
fmt.Println("Slice a is not nil.")
}
// 判斷b是否為nil
if b == nil {
fmt.Println("Slice b is nil.")
} else {
fmt.Println("Slice b is not nil.") // 這個會被執(zhí)行,因為b是一個空切片,已初始化
}
}
空切片與nil切片
一個空切片(如b)雖然長度為0,但它已經被初始化,因此b != nil。
一個未初始化的切片(如a)被視為nil,所以a == nil。
判斷切片是否為空時,通常建議同時檢查長度和是否為nil,以避免潛在的意外。
package main
import "fmt"
func isEmpty(slice []int) bool {
return len(slice) == 0 && slice == nil
}
func main() {
var a []int // nil切片
b := []int{} // 空切片
fmt.Println("Is slice a empty?", isEmpty(a)) // true
fmt.Println("Is slice b empty?", isEmpty(b)) // false
}
切片不能直接比較
切片之間是不能比較的,我們不能使用==操作符來判斷兩個切片是否含有全部相等元素。
切片唯一合法的比較操作是和nil比較。 一個nil值的切片并沒有底層數組
,一個nil值的切片的長度和容量都是0。
但是我們不能說一個長度和容量都是0的切片一定是nil
切片的賦值拷貝
下面的代碼中演示了拷貝前后兩個變量共享底層數組,對一個切片的修改會影響另一個切片的內容
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //將s1直接賦值給s2,s1和s2共用一個底層數組
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
}
切片遍歷
切片的遍歷方式和數組是一致的,支持索引遍歷和for range遍歷。
func main() {
s := []int{1, 3, 5}
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}
for index, value := range s {
fmt.Println(index, value)
}
}
訪問元素
package main
import "fmt"
func main() {
slice := []int{10, 20, 30, 40}
fmt.Println("切片的第一個元素:", slice[0]) // 輸出: 10
}
修改元素 可以直接通過索引修改切片中的元素
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
slice[1] = 5 // 修改第二個元素
fmt.Println("修改后的切片:", slice) // 輸出: [1 5 3]
}
追加元素 使用 append 函數可以向切片追加元素。
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // 追加多個元素
fmt.Println("追加后的切片:", slice) // 輸出: [1 2 3 4 5]
}
切片的切割 可以通過切片操作來獲取切片的子切片
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
subSlice := slice[1:4] // 獲取子切片
fmt.Println("子切片:", subSlice) // 輸出: [2 3 4]
}
切片的注意事項
切片是引用類型,這意味著多個切片可以共享同一個底層數組的部分或全部。
對一個切片的修改可能會影響到其他切片。
切片的容量會隨著元素的增加而自動增長,但每次增長會分配新的底層數組。
如果頻繁地使用 append,可以先為切片分配一個足夠大的容量,以減少內存分配的開銷。
使用 copy 函數可以復制切片中的元素到另一個切片中。
切片的底層數組地址
func main() {
a := [5]int{1, 2, 3, 4, 5} //定義一個數組
//打印地址
fmt.Printf("打印的是數組的地址 a:%p\n", &a) // a:0xc0000ae000
s := a[0:3]
fmt.Println(s) //[1 2 3]
fmt.Printf("打印的是數組的地址 s:%p\n", s) // s:0xc0000ae000
s1 := a[1:3]
fmt.Println(s1) //[2 3]
fmt.Printf("打印的是數組的地址 偏移了8個字節(jié) s1:%p\n", s1) // s:0xc0000ae008 (偏移了8個字節(jié)) ,不過由于切片從索引1開始,所以地址是數組的第二個元素的地址
fmt.Printf("打印切片的地址 &s:%p\n", &s) // &s:0xc0000081b0
fmt.Printf("打印切片的地址 &s1:%p\n", &s1) // &s1:0xc0000081f8
}
切片本身的大小
package main
import (
"fmt"
"unsafe"
)
func main() {
// 聲明一個切片
var intSlice []int
// 獲取切片本身的大小
sliceSize := unsafe.Sizeof(intSlice)
fmt.Printf("切片大小: %d\n", sliceSize)// 切片大小: 24
}
append()方法為切片添加元素詳解
append() 函數非常靈活,可以一次添加一個或多個元素。當切片的容量不足以容納新添加的元素時,append() 自動分配一個新的底層數組。
func append(slice []Type, elems ...Type) []Type slice 是要添加元素的切片。 elems... 是要添加到切片中的一個或多個元素。 返回值是一個新的切片,包含原切片的所有元素和新添加的元素。
可以一次添加一個元素,可以添加多個元素,也可以添加另一個切片中的元素(后面加…)。
func main(){
var s []int //通過var聲明的零值切片可以在append()函數直接使用,無需初始化。
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}
切片的擴容策略
- 快速增長:在切片較小的情況下,選擇雙倍擴容可以較快地滿足需求。
- 漸進增長:當切片已經較大時,使用逐步增加的方式將容量擴增少量,這樣可以避免一次分配過大的內存,避免可能的頻繁分配和額外的內存壓力。
- 防止溢出:在擴容計算過程中,檢查容量是否溢出是很重要的,以此防止出現(xiàn)無限循環(huán)或系統(tǒng)崩潰。
newcap := old.cap //newcap 被初始化為當前切片的容量(old.cap)。
doublecap := newcap + newcap //doublecap 是新容量的兩倍,可以為切片提供更大的擴容空間。
if cap > doublecap {
newcap = cap //如果請求的新容量大于 doublecap,則直接將 newcap 設置為請求的新容量。這是一種確保可以滿足用戶需求的方式。
} else {
if old.len < 1024 {
newcap = doublecap //當當前切片的長度小于 1024 時,newcap 設置為 doublecap,即雙倍擴容。這樣可以快速增長容量,適用于較小的切片。
} else {
//當當前切片的長度大于或等于 1024 時,使用一個循環(huán)逐步增加容量:
//在每次循環(huán)中, newcap 增加其自身的四分之一,直到 newcap 大于等于請求的新容量 cap。
//這個檢查 0 < newcap 是為了防止 newcap 發(fā)生溢出,從而導致無限循環(huán)。
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
//如果在調整容量時出現(xiàn)溢出(即 newcap <= 0),則將 newcap 設置為請求的新容量 cap。
if newcap <= 0 {
newcap = cap
}
}
}
從切片中刪除元素
切片是一種動態(tài)數組,刪除切片中的元素通常涉及到重新創(chuàng)建切片以排除指定的元素。由于切片是引用類型,刪除操作并不會改變原始切片的長度和容量,而是通過切片的重新切割來達到節(jié)省存儲空間的效果。
使用切片重組:
最簡單的方式是通過切片的組合將要刪除的元素排除。假設我們有一個整數切片,并希望刪除指定索引的元素
package main
import "fmt"
func removeAtIndex(slice []int, index int) []int {
// 檢查索引是否有效
if index < 0 || index >= len(slice) {
return slice // 返回原切片
}
// 將切片分為兩部分并組合
return append(slice[:index], slice[index+1:]...) // 刪除索引 index 處的元素
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
fmt.Println("初始切片:", numbers)
// 刪除索引為 2 的元素(值為 3)
numbers = removeAtIndex(numbers, 2)
fmt.Println("刪除后的切片:", numbers) // 輸出: [1 2 4 5]
}
刪除多個元素:
如果要刪除多個元素,可以使用循環(huán)并根據條件過濾元素。
package main
import "fmt"
func removeElements(slice []int, value int) []int {
result := []int{}
for _, v := range slice {
if v != value { // 僅保留不等于 value 的元素
result = append(result, v)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 3, 5}
fmt.Println("初始切片:", numbers)
// 刪除值為 3 的所有元素
numbers = removeElements(numbers, 3)
fmt.Println("刪除后的切片:", numbers) // 輸出: [1 2 4 5]
}
使用 copy 函數:
有時,我們會希望在刪除元素后保留原切片中的數據結構??梢允褂?copy 函數來實現(xiàn)。
package main
import "fmt"
func removeAtIndexUsingCopy(slice []int, index int) []int {
if index < 0 || index >= len(slice) {
return slice // 返回原切片
}
// 使用 copy 函數
copy(slice[index:], slice[index+1:]) // 將后面的元素前移
return slice[:len(slice)-1] // 切割到減少后的長度
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
fmt.Println("初始切片:", numbers)
// 刪除索引為 2 的元素(值為 3)
newNumbers := removeAtIndexUsingCopy(numbers, 2)
fmt.Println("初始切片:", numbers) // 輸出: [1 2 4 5]
fmt.Println("刪除后的切片:", newNumbers) // 輸出: [1 2 4 5]
}
以上示例展示了從切片中刪除元素的幾種不同方法
在使用這些方法時,注意以下幾點:
- 指針和引用:切片是引用類型,刪除元素的過程通常通過新切片引用來實現(xiàn),并不會改變原切片本身的內存結構。
- 性能考慮:在刪除大量元素時,要考慮性能,循環(huán)和過濾可能會導致較大開銷,如果只需要刪除一個元素,使用簡單的切割方式會更高效。
- 負索引檢查:確保在進行刪除操作時對索引或元素值進行有效性檢查,避免運行時錯誤。
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

