1. 程式人生 > 實用技巧 >切片Slice的使用

切片Slice的使用

引言

每個人學習的方式不一樣,對於我而言,最好的方式就是通過coding來學習,這樣自己能夠解決自己的疑惑和發現新的問題;
在學習go的過程中,我自己都是通過coding來掌握相關的知識和用法,並且這樣也能夠用於解決生活上的實際問題;
在下面的程式碼中,我自己通過註釋的方式闡述了一些自己的看法和可能需要注意的地方。

程式碼示例

package basic

import (
	"fmt"
	"unicode/utf8"
)

// 引用型別 :slice, map, channel, function
// 引用型別的零值是 nil
func TestSlice() {
	// ---------
	// 宣告和初始化
	// ---------

	// 建立一個含有4個元素的slice
	// make是一個特殊的內建函式,僅適用於slce、map、channel
	// 在下面的例子中,make建立一個含有4個string的slice,建立完成之後可以得到對應的資料結構如下:
	// 第一個是返回陣列的指標,第二個是的長度,第三個是slice的容量
	// --------
	// | * | --> | nil | | nil | | nil | | nil |
	// -------   | 0   | | 0   | | 0   | | 0   |
	// | 4 |
	// -------
	// | 4 |
	// -------

	// ---------------
	// 長度和容量的區別
	// ---------------

	// 長度是指標指向的我們可以訪問的元素的個數
	// 容量是指標指向的陣列中總的元素的個數

	// 語法糖 --> 看起來很像陣列
	// 需要注意的一件事情是:make建立的slice中是沒有值的,這和陣列有很大的區別
	slicestring := make([]string, 4)
	slicestring[0] = "Apple"
	slicestring[1] = "Banana"
	slicestring[2] = "Grape"
	slicestring[3] = "Plum"

	// 不能訪問超過slice長度的元素,或者會報錯, index out of range
	// slicestring[5] = "Runtime error"

	fmt.Println("slicestring is ", slicestring)
	for _, val := range(slicestring) {
		fmt.Println("slicestring is ", val)
	}
	fmt.Println()
	
	// --------------------
	// 使用引用型別建立slice
	// --------------------

	// 建立一個長度為5,容量為8的slice
	// make可以指定建立slice的容量和長度在初始化的時候
	// ----------
	// | * | --> | nil | nil | nil | nil | nil | nil | nil | nil |
	// ----      |  0  |  0  |  0  |  0  |  0  |  0  |  0  |  0  |
	// | 5 |
	// | 8 |
	// ----------
	// 表示可以讀取前5個元素並且還有三個元素的容量,這三個元素可以之後再用
	slice2 := make([]string, 5, 8)
	slice2[0] = "A"
	slice2[1] = "B"
	slice2[2] = "C"
	slice2[3] = "D"
	slice2[4] = "E"

	fmt.Println("capacity 和 length的區別-->")
	inspectSlice := func(slice []string) {
		fmt.Printf("Length[%d], Capacity[%d]\n", len(slice), cap(slice))
		for i := range slice {
			fmt.Printf("[%d] %p %s \n",i , &slice[i], slice[i])
		}
	}

	inspectSlice(slice2)

	//-------------------------------------------------
	// 更進一步, 讓slice成為動態的資料結構,就和vector一樣
	//-------------------------------------------------

	// 宣告一個string的slice,一開始將是0值
	// 結構分別是nil, 0, 0, 使用var 宣告將會是nil
	var name []string

	// name := string{}, 這和name slice是不一樣的, 因為在這裡name將不會賦予nil型別
	// var會自動為變數賦予零值,而其他建立的空值型別的變數不一定總是會自動賦予零值,這是區別
	// 上面宣告的name slice含有一個指標,指向nil, 所以這是一個空的slice,而不是一個nil slice
	// 空slice和nil slice之間還是有區別的,任何引用型別的值都可以被設為零值nil,

	// 獲取slice的capacity
	lastCapacity := cap(name)
	fmt.Printf("last cap of name is [%d]\n", lastCapacity)

	// 往name中新增2000個元素
	for index := 1; index <= 2000; index++ {
		name = append(name, fmt.Sprintf("Name: %d", index))

		// 每次使用append方法時候,都會檢查slice的容量和長度
		// 如果獲取的長度和容量是一樣的,意味著slice沒有空間存放元素了;這個時候需要返回一個新建的陣列,容量是之前的兩倍
		// 複製原來的元素後在新增新的元素, 在go的堆疊上slice副本發生變化,並且會返回新的引用,我們需要用這個新的引用代替之前的
		// 如果獲取的資料是不一樣的,那麼意味著還有空間可以存放元素,這個時候效率比較高

		// 注意下面輸出的最後一列,當返回的陣列元素小於或等於1000個元素的時候,增長速度是一倍,超過這個數字的時候,增長速度變成25%
		if lastCapacity != cap(name) {
			// 計算變速速率
			capChg := float64(cap(name) - lastCapacity) / float64(lastCapacity) * 100

			// 儲存新值
			lastCapacity = cap(name)

			// 輸出對應的結果
			fmt.Printf("Addr[%p]\tIndex[%d]\t\tCap[%d - %2.f%%]\n", &name[0], index, cap(name), capChg)
		}
	}


	// ----------------
	// slice of slice
	// ----------------

	// 在這我們將name切片取出一部分當做另外一個slice
	// namecopy 切片的長度是2
	// 引數是[開始索引: 開始索引 + 長度],左閉右開
	// 通過看輸出結果,可以知道這兩個切片是在共享記憶體
	namecopy := name[2: 4]
	fmt.Printf("\n=> Slice of slice (before)\n")

	inspectSlice(name)
	inspectSlice(namecopy)

	// 既然是引用,那麼如果我們改變namecopy[0]的值,那麼name[0]應該也會變化
	namecopy[0] = "Modify Value"

	// 所以如果需要修改某些切片的值,那麼我們就必須時刻知道有誰在用這個切片
	fmt.Printf("\n=> Slice of slice (after)\n")
	inspectSlice(name)
	inspectSlice(namecopy)

	// namecopy := append(namecopy, "CHANGED") 的行為是怎麼樣的
	// 如果長度和容量不一樣的話,那麼append中會有同樣的問題產生
	// 由於namecopy的長度是2,容量是6,所以還有其他空間可以容納元素
	// 但是這樣將會改變name切片的元素,這不符合常理

	// -----------
	// 複製一個切片
	// -----------

	// copy函式只對string和切片有效
	// 宣告一個容量足夠的slice,讓他能夠儲存slice2的全部元素
	// copy 函式的使用

	slice4 := make([]string, len(slice2))
	copy(slice4, slice2)
	fmt.Printf("\n=> Copy a slice\n")
	inspectSlice(slice4)

	// -------------
	// 切片和引用型別
	// -------------

	// 宣告一個包含5個整數的切片
	x := make([]int, 5)

	// 隨便賦予初值
	for i := 0; i < 5; i++ {
		x[i] = i * 100
	}

	// 將x切片的第二個元素的地址賦予指標
	pointer := &x[1]

	// 往X中新加一個元素1000, 這將會觸發錯誤
	// 由於x中長度和容量一樣都是5,那麼append方法將會擴容一倍
	// 所以pointer指標將會指向不一樣的記憶體區域
	x = append(x, 1000)

	// 當我們改變第二個元素的值,pointer指標將不會發生變化,因為指標指向的是舊的x切片
	// 所以每次使用pointer的值的時候,都會讀取到一個錯誤的值
	x[1]++

	// 通過檢視輸出結果,我們就可以看出
	fmt.Printf("\n=> 切片和引用型別\n")
	fmt.Println("Pointer: ", *pointer, "\t x[1:] ", x[1])

	// ----------------
	// UTF-8
	// ----------------
	fmt.Printf("\n=> UTF-8\n")

	// go中的字符集都是基於utf-8的
	// 所以當使用不同的編碼的時候,有可能會引發一些其他問題

	// 宣告一個字串,裡面包含中文和英語字元
	// 對於每個中文字元,我們需要3個位元組來儲存
	// utf-8建立在字元、程式碼點、位元組三個層面上, go中的string僅僅是位元組
	// 在我們的例子中,前三個位元組代表著一個字元
	// 我們可以從1到4用4個位元組表示一個字元(程式碼點, 一個程式碼點是32個bit), 所以用多個bit位表示任意一個字元
	// 在例子中,我用3個位元組表示一個字元,因此可以讀取字元按照以下方式-->
	// 3 bytes, 3 bytes, 1 bytes, 1 bytes, ... 
	str := "世界 means world"

	// UTFMax是4 --  
	var buf [utf8.UTFMax]byte

	for i, r := range str {
		r1 := utf8.RuneLen(r)
		si := i + r1
		copy(buf[:], str[i:si])
		fmt.Printf("%2d: %q; codepoint:  %#6x; encoded bytes: %#v\n", i, r, r, buf[:r1])
	}
}