Go語言切片一網打盡,別和Java語法傻傻分不清楚

白澤來了發表於2022-01-29

前言

我總想著搞清楚,什麼樣的技術文章才算是好的文章呢?因為寫一篇今後自己還願意閱讀的文章並不容易,暫時只能以此為目標努力。

最近開始用Go刷一些題,遇到了一些切片相關的細節問題,這裡做一些總結。切片的設計想法是由動態陣列概念而來,為了開發者可以更加方便的使一個資料結構可以自動增加和減少。但是切片本身並不是動態資料或者陣列指標。

切片的結構

type slice struct {
	array unsafe.Pointer	 // 一個指向底層陣列的指標(切片中元素是存在這個指向的陣列中)
	len int								 // 切片的長度:包含的元素個數
	cap int								 // 切片的容量,len <= cap,如果len == cap,則新增元素會觸發切片的擴容
}

長度為3,容量為5的int切片的圖示如下,此時切片陣列中可訪問的部分只有下標0,1,2,超過部分不能訪問。

image-20220129100007594

宣告和初始化

nil切片

宣告nil切片,宣告之後就會初始化(預設會用nil初始化),此時slice == nil成立,用於描述一個不存在的切片。

var slice []int	// 此時 slice == nil 成立

image-20220129100614622

空切片

宣告並初始化空切片,表示一個空的集合,空切片指向地址不是nil。

slice := make([]int, 0) // 此時 slice == nil 不成立
slice := []int{}

image-20220129100934875

無論是nil切片還是空切片在呼叫內建函式append、len和cap的效果都是一樣的。

含有元素的切片

此時切片非空。

slice := []int{1, 2, 3, 4, 5} 	// 宣告並初始化一個切片,len和cap都是5
slice := make([]int, 4)					// 宣告並初始化一個切片,第二個參數列示切片的長度len和容量cap都為4
slice := make([]int, 4, 6) 			// 宣告並初始化一個切片,第二個參數列示len,第三個表示cap
-----------------------------------------------------------
array := [4]int{1, 2, 3, 4}			
slice := array[1:2]							// 對於array[i:j]來說,新切片的len=j-i,且cap=k-i(這裡k是原陣列的大小)

image-20220129105637373

測試上面第四種初始化切片的方法:

func main() {
	array := [...]int{1, 2, 3, 4}
	slice := array[1:2]
	fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
}
/*
輸出:
0xc00000c030 1 3 [2]
*/

拷貝

使用 := 拷貝

注意:下面程式碼中newSlice切片是通過slice切片宣告並初始化的,雖然兩個切片列印的地址不同,但是切片的地址指標指向的陣列是同一個。修改slice[0] = 100之後,newSlice[0]也變成100。這種規則適用於將切片作為引數傳遞給函式,在函式的內部使用的是傳入切片的值拷貝(建立一塊新的記憶體存放切片,但切片的地址指標指向的是同一個陣列)

image-20220129111918283

func main() {
	array := [...]int{1, 2, 3, 4}
	slice := array[1:2]
	newSlice := slice				// 拷貝,newSlice由一塊新的記憶體存放slice切片資訊
  /*
  	上面這種初始化newSlice的寫法等價於:
  	var newSlice []int
  	newSlice = slice
  */
	fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
	fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
	slice[0] = 100
	fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
	fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
}
/*
輸出:
0xc00000c030 1 3 [2]
0xc00000c048 1 3 [2]
0xc00000c030 1 3 [100]
0xc00000c048 1 3 [100]
*/

使用copy函式拷貝

copy函式的兩個引數是兩個切片(將第二個切片的值覆蓋到第一個切片),二者地址指標指向兩個不同的陣列。

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := []int{5, 4, 3}
	fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
	fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
	//copy(slice2, slice1) 				// 只會複製slice1的前3個元素到slice2中
	copy(slice1, slice2) 					// 只會複製slice2的3個元素到slice1的前3個位置
	fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
	//fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
	slice2[0] = 200			
	slice1[0] = 100
	fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
	fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
}
/*
輸出:
5 5 0xc00000c018 [1 2 3 4 5]
3 3 0xc00000c030 [5 4 3]
5 5 0xc00000c018 [5 4 3 4 5]
5 5 0xc00000c018 [100 4 3 4 5]		copy函式的兩個切片的地址指標分別指向兩個不同的陣列(修改值互不影響)
3 3 0xc00000c030 [200 4 3]
*/

擴容

擴容使用append方法。

len == cap 時新增元素

func main() {
	slice := []int{1, 2, 3}			// 此時slice的len == cap == 3,append元素會觸發擴容,擴容後切片地址指向新陣列(具體擴容策略這裡暫時不多深入)
	fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
	newSlice := append(slice, 1)
	fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
	slice[0] = 100
	fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
	fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
}
/*
輸出:
0xc00000c030 3 3 [1 2 3]
0xc00000c060 4 6 [1 2 3 1]		// append一個元素之後,切片中指標指向一個擴容後新的陣列(地址指標變化,這裡列印出的是切片地址,無論是否擴容newSlice和slice記憶體地址必然是不同的,和二者擁有的地址指標不要搞混)
0xc00000c030 3 3 [100 2 3]		// 修改slice[0]的元素為100
0xc00000c060 4 6 [1 2 3 1]		// 但是newSlice[0]中的元素沒有變化(這裡才可以證明兩個切片的地址指標指向兩個不同的陣列)
*/

len < cap 時新增元素

此時呼叫append方法新增一個元素1並不會建立新的陣列,但是1會去覆蓋掉array[2]。

image-20220129114640983

func main() {
	array := [4]int{1, 2, 3, 4}
	slice := array[0:2]
	fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
	newSlice := append(slice, 1)
	fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
	slice[0] = 100
	fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
	fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
  fmt.Println("array = ", array)
}
/*
輸出:
0xc00000c030 2 4 [1 2]				// 這裡看到從陣列中初始化的slice的len==2且cap==4
0xc00000c060 3 4 [1 2 1]			// append之後,新增一個元素,len變為3(且append元素會覆蓋底層陣列,這裡1覆蓋了之前的3)
0xc00000c030 2 4 [100 2]			// 注意,這裡slice列印出來的len還是2,slice的len和cap與newSlice的len和cap是隔離的,儘管它們地址指標指向的是同一個陣列,這裡修改了slice[0]=100
0xc00000c060 3 4 [100 2 1]		// newSlice[0]也被修改為100
array =  [100 2 1 4]					// 證明指向的array陣列也被修改了
*/

結束

快過年了,祝大家新年快樂,春招offer拿不停。

建了一個春秋招備戰/內推/閒聊群,歡迎大家加入。

image-20220128171234433

關注公眾號【程式設計師白澤】,帶你走近一個有點話癆的程式設計師/學生黨。

image-20220110183620835

相關文章