日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Go 学习笔记(11)— 切片定义、切片初始化、数组和切片差异、字符串和切片转换、len()、cap()、空 nil 切片、append()、copy() 函数、删除切片元素

發布時間:2023/11/27 生活经验 78 豆豆

1. 切片定義

Go 語言切片是對數組的抽象。 Go 中提供了一種靈活,功能強悍的內置類型切片(“動態數組”),與數組相比切片的長度是不固定的,可以追加元素,在追加時可能使切片的容量增大。

切片(slice)是對數組的一個連續片段的引用,所以切片是一個引用類型,這個片段可以是整個數組,也可以是由起始和終止索引標識的一些項的子集,需要注意的是,終止索引標識的項不包括在切片內。

SliceHeader 是切片在 go 的底層結構。

type SliceHeader struct {array unsafe.Pointerlen   intcap   int
}

所有切片的大小相同;

  • array: 是指向底層數組的指針;
  • len: 是切片的長度,即切片中當前元素的個數;
  • cap: 是底層數組的長度,也是切片的最大容量,cap 值永遠大于等于 len 值;

將一個 slice 變量分配給另一個變量只會復制三個機器字。所以 拷貝大切片跟小切片的代價應該是一樣的。

我們其實可以把切片看做是對數組的一層簡單的封裝,因為在每個切片的底層數據結構中,一定會包含一個數組。數組可以被叫做切片的底層數組,而切片也可以被看作是對數組的某個連續片段的引用。

Go 語言中切片的內部結構包含地址、大小和容量,切片一般用于快速地操作一塊數據集合,如果將數據集合比作切糕的話,切片就是你要的“那一塊”,切的過程包含從哪里開始(切片的起始位置)及切多大(切片的大小),容量可以理解為裝切片的口袋大小,如下圖所示。

聲明一個未指定大小的數組來定義切片:

var varName []type

切片不需要說明長度。或使用 make() 函數來創建切片:

var varName []type = make([]type, len)// 也可以簡寫為varName := make([]type, len)

也可以指定容量,其中 capacity 為可選參數。

make([]type, length, capacity)

其中 type 是指切片的元素類型, length 指的是為這個類型分配多少個元素, capacity 為預分配的元素數量,這個值設定后不影響 size,只是能提前分配空間,降低多次分配空間造成的性能問題,在初始化 的時候盡量補全 cap。這里 length 是數組的長度并且也是切片的初始長度。

a := make([]int, 2)
b := make([]int, 2, 10)fmt.Println(a, b)	// [0 0] [0 0]
fmt.Println(len(a), len(b))	// 2 2

其中 a 和 b 均是預分配 2 個元素的切片,只是 b 的內部存儲空間已經分配了 10 個,但實際使用了 2 個元素。容量不會影響當前的元素個數,因此 a 和 b 取 len 都是 2。

注意:使用 make() 函數生成的切片一定發生了內存分配操作,但給定開始與結束位置(包括切片復位)的切片只是將新的切片結構指向已經分配好的內存區域,設定開始與結束位置,不會發生內存分配操作。

Go 編譯器會自動為每個新創建的切片,建立一個底層數組,默認底層數組的長度與切片初始元素個數相同。

2. 切片初始化

  1. 直接初始化切片,[] 表示是切片類型,{1,2,3} 初始化值依次是 1,2,3, 其 cap=len=3
s := []int{1,2,3 }
  1. 切片默認指向一段連續內存區域,可以是數組,也可以是切片本身。初始化切片 s,是數組 arr 的引用
s := arr[:]
  1. 將 arr 中從下標 startIndex 到 endIndex-1 下的元素創建為一個新的切片
s := arr[startIndex:endIndex]

從數組生成切片,代碼如下:

var a  = [3]int{1, 2, 3}
fmt.Println(a, a[1:2]) // [1 2 3]  [2]
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]

基于數組創建的切片,它的起始元素從 low 所標識的下標值開始,切片的長度(len)是 high - low,它的容量是 max - low。而且,由于切片 sl 的底層數組就是數組 arr,對切片 sl 中元素的修改將直接影響數組 arr 變量。比如,如果我們將切片的第一個元素加 10,那么數組 arr 的第四個元素將變為 14:

sl[0] += 10
fmt.Println("arr[3] =", arr[3]) // 14

在 Go 語言中,數組更多是“退居幕后”,承擔的是底層存儲空間的角色。切片就是數組的“描述符”,也正是因為這一特性,切片才能在函數參數傳遞時避免較大性能開銷。因為我們傳遞的并不是數組本身,而是數組的“描述符”,而這個描述符的大小是固定的(見上面的三元組結構),無論底層的數組有多大,切片打開的“窗口”長度有多長,它都是不變的。此外,我們在進行數組切片化的時候,通常省略 max,而 max 的默認值為數組的長度。

  1. 缺省 endIndex 時將表示一直到 arr 的最后一個元素
s := arr[startIndex:]
  1. 缺省 startIndex 時將表示從 arr 的第一個元素開始
s := arr[:endIndex]
  1. 通過切片 s 初始化切片 s1
s1 := s[startIndex:endIndex]
  1. 通過內置函數 make() 初始化切片 s,[]int 標識為其元素類型為 int 的切片,由 make 創建的切片各元素默認為該類型零值。
s :=make([]int, len, cap)

從數組或切片生成新的切片擁有如下特性:

  • 取出的元素數量為:結束位置 - 開始位置;

  • 取出元素不包含結束位置對應的索引,切片最后一個元素使用 slice[len(slice)] 獲取;

  • 當缺省開始位置時,表示從連續區域開頭到結束位置;

  • 當缺省結束位置時,表示從開始位置到整個連續區域末尾;

  • 兩者同時缺省時,與切片本身等效;

a := []int{1, 2, 3}
fmt.Println(a[:])	// [1 2 3]

a 是一個擁有 3 個元素的切片,將 a 切片使用 a[:] 進行操作后,得到的切片與 a 切片一致。

  • 兩者同時為 0 時,等效于空切片,一般用于切片復位。
a := []int{1, 2, 3}
fmt.Println(a[0:0]) 	// []

根據索引位置取切片 slice 元素值時,取值范圍是(0~len(slice)-1),超界會報運行時錯誤,生成切片時,結束位置可以填寫 len(slice) 但不會報錯。

3. 數組和切片聲明差異

數組類型的值(以下簡稱數組)的長度是固定的,而切片類型的值(以下簡稱切片)是可變長的。

如果在 [] 運算符里指定了一個值,那么創建的就是數組而不是切片。只有不指定值的時候,才會創建切片,如下所示。

// 創建有3個元素的整型數組
array := [3]int{10, 20, 30}// 創建長度和容量都是3的整型切片
slice := []int{10, 20, 30}

4. 字符串和切片轉換

s := "hello"
a := []byte(s)	// 將字符串轉換為 byte 類型切片
b := []rune(s)	// 將字符串轉換為 rune 類型切片

5. len() 和 cap() 函數

數組的容量永遠等于其長度,都是不可變的。

切片是可索引的,并且可以由 len() 方法獲取長度。

切片提供了計算容量的方法 cap() 可以測量切片最長可以達到多少。

package mainimport "fmt"func main() {var numbers = make([]int,3,5)printSlice(numbers)
}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

輸出結果為:

len=3 cap=5 slice=[0 0 0]

示例:

func main() {s1 := make([]int, 5)                           // 用make函數初始化切片時,如果不指明其容量,那么它就會和長度一致fmt.Printf("The length of s1: %d\n", len(s1))   // 5fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5fmt.Printf("The value of s1: %d\n", s1)         // [0 0 0 0 0]s2 := make([]int, 5, 8)fmt.Printf("The length of s2: %d\n", len(s2))   // 5fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 8fmt.Printf("The value of s2: %d\n", s2)         // [0 0 0 0 0]
}

當我們通過切片表達式基于某個數組或切片生成新切片的時候,如下

func main() {s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}s4 := s3[3:6]fmt.Printf("The length of s4: %d\n", len(s4))   // 3fmt.Printf("The capacity of s4: %d\n", cap(s4)) // 5fmt.Printf("The value of s4: %d\n", s4)         // [4 5 6]
}

切片的容量代表了它的底層數組的長度,但這僅限于使用 make 函數或者切片值字面量初始化切片的情況。

更通用的規則是:一個切片的容量可以被看作是透過這個窗口最多可以看到的底層數組中元素的個數。由于 s4 是通過在 s3 上施加切片操作得來的,所以 s3 的底層數組就是 s4 的底層數組。又因為,在底層數組不變的情況下,切片代表的窗口可以向右擴展,直至其底層數組的末尾。所以,s4 的容量就是其底層數組的長度 8 減去上述切片表達式中的那個起始索引 3,即 5。

	s5 := s4[:cap(s4)]fmt.Printf("The length of s5: %d\n", len(s5))   // 5fmt.Printf("The capacity of s5: %d\n", cap(s5)) // 5fmt.Printf("The value of s5: %#v\n", s5)        // []int{4, 5, 6, 7, 8}

6. 空切片與 nil 切片

切片是動態結構,只能與 nil 判定相等,不能互相判定相等。一個切片在未初始化之前默認為 nil ,長度為 0,創建 nil 切片

// 創建nil整型切片
var varName []int

只要在聲明時不做任何初始化,就會創建一個 nil 切片。

利用初始化,通過聲明一個切片可以創建一個空切片

// 使用make創建空的整型切片
slice := make([]int, 0)// 使用切片字面量創建空的整型切片
slice := []int{}

不管是使用 nil 切片還是空切片,對其調用內置函數 appendlencap的效果都是一樣的。

實例如下:

package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)if(numbers == nil){fmt.Printf("切片是空的")}
}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

輸出結果為:

len=0 cap=0 slice=[]
切片是空的

空切片和 nil 切片對比

var s1 []int
var s2 = []int{}
  • s1 是聲明,還沒初始化,是 nil 值,lencap 是 0,arraynil, 底層沒有分配內存空間。和nil 比較返回 true
  • s2 初始化了,不是 nil 值,是空切片, 底層分配了內存空間,有地址。和 nil 比較返回 false

更推薦第一種寫法

package mainimport "fmt"func main() {
var sl1 []int
var sl2 = []int{}fmt.Printf("%T, %v, %p\n", sl1, sl1, sl1) // []int, [], 0x0
fmt.Printf("%T, %v, %p\n", sl2, sl2, sl2) // []int, [], 某個地址值fmt.Println(sl1 == nil) // true
fmt.Println(sl2 == nil) // falsefmt.Println(len(sl1), cap(sl1)) // 0, 0
fmt.Println(len(sl2), cap(sl2)) // 0, 0// fmt.Println(sl1[0]) 下標越界 panic
// fmt.Println(sl2[0]) 下標越界 panicsl1 = append(sl1, 1) // 可以 append 操作
sl2 = append(sl2, 1) // 可以 append 操作
}

7. 切片截取

可以通過設置下限及上限來設置截取切片 [lower-bound:upper-bound],實例如下:

package mainimport "fmt"func main() {/* 創建切片 */numbers := []int{0,1,2,3,4,5,6,7,8}   printSlice(numbers)/* 打印原始切片 */fmt.Println("numbers ==", numbers)/* 打印子切片從索引1(包含) 到索引4(不包含)*/fmt.Println("numbers[1:4] ==", numbers[1:4])/* 默認下限為 0*/fmt.Println("numbers[:3] ==", numbers[:3])/* 默認上限為 len(s)*/fmt.Println("numbers[4:] ==", numbers[4:])numbers1 := make([]int,0,5)printSlice(numbers1)/* 打印子切片從索引  0(包含) 到索引 2(不包含) */number2 := numbers[:2]printSlice(number2)/* 打印子切片從索引 2(包含) 到索引 5(不包含) */number3 := numbers[2:5]printSlice(number3)}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

8. append() 函數

Go 語言的內建函數 append() 可以為切片動態添加元素,不過需要注意的是,在使用 append() 函數為切片動態添加元素時,如果空間不足以容納足夠多的元素,切片就會進行“擴容”,此時新切片的長度會發生改變。

切片在擴容時,容量的擴展規律是按容量的 2 倍數進行擴充,例如 1、2、4、8、16……

一旦一個切片無法容納更多的元素,Go 語言就會想辦法擴容。但它并不會改變原來的切片,而是會生成一個容量更大的切片,然后將把原有的元素和新元素一并拷貝到新切片中。

在一般的情況下,你可以簡單地認為新切片的容量(以下簡稱新容量)將會是原切片容量(以下簡稱原容量)的 2 倍。但是,當原切片的長度(以下簡稱原長度)大于或等于 1024 時,Go 語言將會以原容量的 1.25 倍作為新容量的基準(以下新容量基準)。新容量基準會被調整(不斷地與 1.25 相乘),直到結果不小于原長度與要追加的元素數量之和(以下簡稱新長度)。最終,新容量往往會比新長度大一些,當然,相等也是可能的。

另外,如果我們一次追加的元素過多,以至于使新長度比原容量的 2 倍還要大,那么新容量就會以新長度為基準。注意,與前面那種情況一樣,最終的新容量在很多時候都要比新容量基準更大一些。

package mainimport "fmt"func main() {var a []inta = append(a, 1)                 // 追加1個元素a = append(a, 1, 2, 3)           // 追加多個元素, 手寫解包方式a = append(a, []int{1, 2, 3}...) // 追加一個切片, 切片需要解包fmt.Println(a)var numbers []intfor i := 0; i < 5; i++ {numbers = append(numbers, i)fmt.Printf("len: %d  cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)}
}

輸出結果:

[1 1 2 3 1 2 3]
len: 1  cap: 1 pointer: 0xc0000180d0
len: 2  cap: 2 pointer: 0xc0000180f0
len: 3  cap: 4 pointer: 0xc0000141a0
len: 4  cap: 4 pointer: 0xc0000141a0
len: 5  cap: 8 pointer: 0xc00001a180

從內存地址可以看出:

每次進行 append 之后,如果沒有擴容則是在同一個切片上增加元素,如果已經擴容,則會返回一個新的切片,因為內存地址不一樣

除了在切片的尾部追加,我們還可以在切片的開頭添加元素:

var a = []int{1,2,3}
a = append([]int{0}, a...) // 在開頭添加1個元素
a = append([]int{-3,-2,-1}, a...) // 在開頭添加1個切片

在切片開頭添加元素一般都會導致內存的重新分配,而且會導致已有元素全部被復制 1 次,因此,從切片的開頭添加元素的性能要比從尾部追加元素的性能差很多。

因為 append 函數返回新切片的特性,所以切片也支持鏈式操作,我們可以將多個 append 操作組合起來,實現在切片中間插入元素:

var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i個位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i個位置插入切片

每個添加操作中的第二個 append 調用都會創建一個臨時切片,并將 a[i:] 的內容復制到新創建的切片中,然后將臨時創建的切片再追加到 a[:i] 中。

package mainimport "fmt"func main() {var a = []int{1, 2, 3}a = append([]int{0}, a...)          // 在開頭添加1個元素a = append([]int{-3, -2, -1}, a...) // 在開頭添加1個切片fmt.Println(a)                      // [-3 -2 -1 0 1 2 3]var b []intb = append(a[:0], append([]int{10}, b[0:]...)...)      // 在第0個位置插入10b = append(a[:0], append([]int{1, 2, 3}, b[0:]...)...) // 在第0個位置插入切片fmt.Println(b)                                         // [1 2 3 10]
}

問題 2:切片的底層數組什么時候會被替換?

確切地說,一個切片的底層數組永遠不會被替換。為什么?雖然在擴容的時候 Go 語言一定會生成新的底層數組,但是它也同時生成了新的切片。它只是把新的切片作為了新底層數組的窗口,而沒有對原切片,及其底層數組做任何改動。

請記住,在無需擴容時,append 函數返回的是指向原底層數組的新切片,而在需要擴容時,append 函數返回的是指向新底層數組的新切片。所以,嚴格來講,“擴容”這個詞用在這里雖然形象但并不合適。不過鑒于這種稱呼已經用得很廣泛了,我們也沒必要另找新詞了。

只要新長度不會超過切片的原容量,那么使用 append 函數對其追加元素的時候就不會引起擴容。

基于一個已有數組建立的切片,一旦追加的數據操作觸碰到切片的容量上限(實質上也是數組容量的上界),切片就會和原數組解除“綁定”,后續對切片的任何修改都不會反映到原數組中了

9. copy() 函數

Go 語言的內置函數 copy() 可以將一個數組切片復制到另一個數組切片中,如果加入的兩個數組切片不一樣大,就會按照其中較小的那個數組切片的元素個數進行復制。

copy() 函數的使用格式如下:

copy(destSlice, srcSlice) int

其中 srcSlice 為數據來源切片, destSlice 為復制的目標(也就是將 srcSlice 復制到 destSlice ),目標切片必須分配過空間且足夠承載復制的元素個數,并且來源和目標的類型必須一致, copy() 函數的返回值表示實際發生復制的元素個數。

package mainimport "fmt"func main() {a := []int{10, 20, 30, 4, 5}slice2 := []int{5, 4, 3}copy(slice2, a)     // 只會復制 a 的前3個元素到slice2中fmt.Println(slice2) // [10 20 30]slice2 = []int{5, 4, 3}b := []int{0, 0, 0, 0, 0}ret := copy(b, slice2)      // 只會復制slice2的3個元素到b 的前3個位置fmt.Println(b)              // [5 4 3 0 0]fmt.Println("ret is ", ret) // ret is  3
}

9.1 切片索引生成的切片與原來的切片是同一個地址

如果多個切片指向同一底層數組,那么對其中一個切片的改變影響其它的切片。

package mainimport "fmt"func main() {s1 := []int{1, 2, 3, 4, 5}s2 := s1[0:3]	// 只是基于同一個底層數組生成了一個新的切片(或者說窗口)// s2 := make([]int, 3)copy(s2, s1)s2 = append(s2, 40)s1[2] = 30fmt.Printf("The length of s1: %d\n", len(s1))   // 5fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5fmt.Printf("The value of s1: %d\n", s1)         // [1 2 30 40 5]fmt.Printf("The length of s2: %d\n", len(s2))   // 4fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 5fmt.Printf("The value of s2: %d\n", s2)         // [1 2 30 40]fmt.Printf("The address of s1: %p\n", s1) // 0xc00001a330fmt.Printf("The address of s2: %p\n", s2) // 0xc00001a330
}

9.2 使用make 會生成新的切片

package mainimport "fmt"func main() {s1 := []int{1, 2, 3, 4, 5}// s2 := s1[0:3]s2 := make([]int, 3)copy(s2, s1)s2 = append(s2, 40)s1[2] = 30fmt.Printf("The length of s1: %d\n", len(s1))   // 5fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5fmt.Printf("The value of s1: %d\n", s1)         // [1 2 30 4 5]fmt.Printf("The length of s2: %d\n", len(s2))   // 4fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 6fmt.Printf("The value of s2: %d\n", s2)         // [1 2 3 40]fmt.Printf("The address of s1: %p\n", s1) // 0xc00001a300fmt.Printf("The address of s2: %p\n", s2) // 0xc00001a330
}

10. 切片刪除元素

Go 語言并沒有對刪除切片元素提供專用的語法或者接口,需要使用切片本身的特性來刪除元素,根據要刪除元素的位置有三種情況,分別是從開頭位置刪除、從中間位置刪除和從尾部刪除,其中刪除切片尾部的元素速度最快。

10.1 從開頭位置刪除

刪除開頭的元素可以直接移動數據指針:

a = []int{1, 2, 3}
a = a[1:] // 刪除開頭1個元素
a = a[N:] // 刪除開頭N個元素

也可以不移動數據指針,但是將后面的數據向開頭移動,可以用 append 原地完成(所謂原地完成是指在原有的切片數據對應的內存區間內完成,不會導致內存空間結構的變化):

a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 刪除開頭1個元素
a = append(a[:0], a[N:]...) // 刪除開頭N個元素

還可以用 copy() 函數來刪除開頭的元素:

a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 刪除開頭1個元素
a = a[:copy(a, a[N:])] // 刪除開頭N個元素

10.2 從中間位置刪除

對于刪除中間的元素,需要對剩余的元素進行一次整體挪動,同樣可以用 appendcopy 原地完成:

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 刪除中間1個元素
a = append(a[:i], a[i+N:]...) // 刪除中間N個元素
a = a[:i+copy(a[i:], a[i+1:])] // 刪除中間1個元素
a = a[:i+copy(a[i:], a[i+N:])] // 刪除中間N個元素

10.3 從尾部刪除

a = []int{1, 2, 3}
a = a[:len(a)-1] // 刪除尾部1個元素
a = a[:len(a)-N] // 刪除尾部N個元素

注意:
連續容器的元素刪除無論在任何語言中,都要將刪除點前后的元素移動到新的位置,隨著元素的增加,這個過程將會變得極為耗時,因此,當業務需要大量、頻繁地從一個切片中刪除元素時,如果對性能要求較高的話,就需要考慮更換其他的容器了(如雙鏈表等能快速從刪除點刪除元素)。

總結

以上是生活随笔為你收集整理的Go 学习笔记(11)— 切片定义、切片初始化、数组和切片差异、字符串和切片转换、len()、cap()、空 nil 切片、append()、copy() 函数、删除切片元素的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。