go 學習筆記之數組還是切片都沒什麼不一樣

  • 2019 年 10 月 3 日
  • 筆記

上篇文章中詳細介紹了 Go 的基礎語言,指出了 Go 和其他主流的編程語言的差異性,比較側重於語法細節,相信只要稍加記憶就能輕鬆從已有的編程語言切換到 Go 語言的編程習慣中,儘管這種切換可能並不是特別順暢,但多加練習尤其是多多試錯,總是可以慢慢感受 Go 語言之美!

在學習 Go 的內建容器前,同樣的,我們先簡單回顧一下 Go 的基本語言,溫度而知新可以為師矣!

上節知識回顧

如需了解詳情,請於微信公眾號[雪之夢技術驛站]內查看 go 學習筆記之值得特別關注的基礎語法有哪些 文章,覺得有用的話,順手轉發一下唄!

內建類型種類

  • bool

布爾類型,可選 true|false,默認初始化零值 false .

  • (u)int ,(u)int8 , (u)int16, (u)int32,(u)int64,uintptr

2^0=1,2^1=2 ,2^2=4位元組長度的整型,包括有符號整型和無符號整型以及 uintptr 類型的指針類型,默認初始化零值 0 .

  • byte(uint8) ,rune(int32),string

byte 是最基礎位元組類型,是 uint8 類型的別名,而 runeGo 中的字符類型,是 int32 的別名.最常用的字符串類型 string 應該不用介紹了吧?

  • float32 ,float64 ,complex64 ,complex128

只有 float 類型的浮點型,沒有 double 類型,同樣是以位元組長度來區分,complex64 是複數類型,實部和虛部由 float32 類型複合而成,因此寫作 complex64 這種形式.

內建類型特點

  • 類型轉換隻有顯示轉換,不存在任何形式的隱式類型轉換

不同變量類型之間不會自動進行隱式類型轉換,Go 語言的類型轉換隻有強制的,只能顯示轉換.

  • 雖然提供指針類型,但指針本身不能進行任何形式的計算.

指針類型的變量不能進行計算,但是可以重新改變內存地址的指向.

  • 變量聲明後有默認初始化零值,變量零值視具體類型而定

int 類型的變量的初始化零值是 0,string 類型的初始化零值是空字符串,並不是 nil

基本運算符

  • 算術運算符沒有 ++i--i

只有 i++i-- 這種自增操作,再也不用擔心兩種方式的差異性了!

  • 比較運算符 == 可以比較數組是否相等

當兩個數組的維度和數組長度相等時,兩個數組可以進行比較,順序完全一致時,結果為 true,其他情況則是 false .

  • 位運算符新增按位清零運算符 &^

其他主流的編程語言雖然沒有這種操作符,通過組合命令也可以實現類似功能,但既然提供了按位清零運算符,再也不用自己進行組合使用了!

流程控制語句

  • if 條件表達式不需要小括號並支持變量賦值操作

先定義臨時變量並根據該變量進行邏輯判斷,然後按照不同情況進行分類處理,Go 處理這種臨時變量的情況,直接對條件表達式進行增強,這種情況以後會很常見!

  • if 條件表達式內定義的變量作用域僅限於當前語句塊

條件表達式內定義的變量是為了方便處理不同分支的邏輯,既然是臨時變量,出了當前的 if 語句塊就無法使用,也變得可以理解.

  • switch 語句可以沒有 break,除非使用了 fallthrough

switch 語句的多個 case 結尾處可以沒有 break,系統會自動進行 break 處理.

  • switch 條件表達式不限制為常數或整數

和其他主流的編程語言相比,Go 語言的 switch 條件表達式更加強大,類型也較為寬鬆.

  • switch 條件表達式可以省略,分支邏輯轉向 case 語言實現.

省略 switch 條件表達式,多個 case 語言進行分支流程控制,功能效果和多重 if else 一樣.

  • 省略 switch 條件表達式後,每個 case 條件可以有多個條件,用逗號分隔.

swicth 語句本質上是根據不同條件進行相應的流程控制,每個 case 的條件表達式支持多個,更是增強了流程控制的能力.

  • for 循環的條件表達式也不需要小括號,且沒有其他形式的循環.

Go 語言只有 for 循環,沒有 while 等其他形式的循環.

  • for 循環的初始條件,終止條件和自增表達式都可以省略或者同時省略

條件表達式進行省略後可以實現 while 循環的效果,全部省略則是死循環.

函數和參數傳遞

  • 函數聲明按照函數名,入參,出參順序定義,並支持多返回值

不論是變量定義還是函數定義,Go 總是和其他主流的編程語言反着來,如果按照輸入輸出的順序思考就會發現,這種定義方式其實挺有道理的.

  • 函數有多個返回值時可以給返回值命名,但對調用者而言沒有差別

函數返回多個值時可以有變量名,見名知意方便調用者快速熟悉函數聲明,但調用者並非一定要按照返回值名稱接收調用結果.

  • 函數的入參沒有必填參數,可選參數等複雜概念,只支持可變參數列表

可變參數列表和其他主流的編程語言一樣,必須是入參的最後一個.

  • 函數參數傳遞只有值傳遞,沒有引用傳遞,即全部需要重新拷貝變量

參數傳遞只有值傳遞,邏輯上更加簡單,但是處理複雜情況時可以傳遞指針實現引用傳遞的效果.

內建容器有哪些

複習了 Go 語言的基礎語法後,開始繼續學習變量類型的承載者也就是容器的相關知識.

承載一類變量最基礎的底層容器就是數組了,大多數高級的容器底層都可以依靠數組進行封裝,所以先來了解一下 Go 的數組有何不同?

數組和切片

  • 數組的聲明和初始化

數組的明顯特點就是一組特定長度的連續存儲空間,聲明數組時必須指定數組的長度,聲明的同時可以進行初始化,當然不指定數組長度時也可以使用 ... 語法讓編譯器幫我們確定數組的長度.

func TestArray(t *testing.T) {      var arr1 [3]int      arr2 := [5]int{1, 2, 3, 4, 5}      arr3 := [...]int{2, 4, 6, 8, 10}        // [0 0 0] [1 2 3 4 5] [2 4 6 8 10]      t.Log(arr1, arr2, arr3)        var grid [3][4]int        // [[0 0 0 0] [0 0 0 0] [0 0 0 0]]      t.Log(grid)  }

[3]int 指定數組長度為 3,元素類型為 int,當然也可以聲明時直接賦值 [5]int{1, 2, 3, 4, 5} ,如果懶得指定數組長度,可以用 [...]int{2, 4, 6, 8, 10} 表示.

  • 數組的遍歷和元素訪問

最常見的 for 循環進行遍歷就是根據數組的索引進行訪問,range arr 方式提供了簡化遍歷的便捷方法.

func TestArrayTraverse(t *testing.T) {      arr := [...]int{2, 4, 6, 8, 10}        for i := 0; i < len(arr); i++ {          t.Log(arr[i])      }        for i := range arr {          t.Log(arr[i])      }        for i, v := range arr {          t.Log(i, v)      }        for _, v := range arr {          t.Log(v)      }  }

range arr 可以返回索引值和索引項,如果僅僅關心索引項而不在乎索引值的話,可以使用 _ 佔位符表示忽略索引值,如果只關心索引值,那麼可以不寫索引項.這種處理邏輯也就是函數的多返回值順序接收,不可以出現未使用的變量.

  • 數組是值類型可以進行比較

數組是值類型,這一點和其他主流的編程語言有所不同,因此相同緯度且相同元素個數的數組可以比較,關於這方面的內容前面也已經強調過,這裡再次簡單回顧一下.

func printArray(arr [5]int) {      arr[0] = 666      for i, v := range arr {          fmt.Println(i, v)      }  }    func TestPrintArray(t *testing.T) {      var arr1 [3]int      arr2 := [5]int{1, 2, 3, 4, 5}      arr3 := [...]int{2, 4, 6, 8, 10}        // [0 0 0] [1 2 3 4 5] [2 4 6 8 10]      t.Log(arr1, arr2, arr3)        // cannot use arr1 (type [3]int) as type [5]int in argument to printArray      //printArray(arr1)        fmt.Println("printArray(arr2)")      printArray(arr2)        fmt.Println("printArray(arr3)")      printArray(arr3)        // [1 2 3 4 5] [2 4 6 8 10]      t.Log(arr2, arr3)  }

因為參數傳遞是值傳遞,所以 printArray 函數無法更改調用者傳遞的外部函數值,如果想要在函數 printArray 內部更改傳遞過來的數組內容,可以通過指針來實現,但是有沒有更簡單的做法?

想要在 printArrayByPointer 函數內部修改參數數組,可以通過數組指針的方式,如果有不熟悉的地方,可以翻看上一篇文章回顧查看.

func printArrayByPointer(arr *[5]int) {      arr[0] = 666      for i, v := range arr {          fmt.Println(i, v)      }  }    func TestPrintArrayByPointer(t *testing.T) {      var arr1 [3]int      arr2 := [5]int{1, 2, 3, 4, 5}      arr3 := [...]int{2, 4, 6, 8, 10}        // [0 0 0] [1 2 3 4 5] [2 4 6 8 10]      t.Log(arr1, arr2, arr3)        fmt.Println("printArrayByPointer(arr2)")      printArrayByPointer(&arr2)        fmt.Println("printArrayByPointer(arr3)")      printArrayByPointer(&arr3)        // [666 2 3 4 5] [666 4 6 8 10]      t.Log(arr2, arr3)  }

修改數組的元素可以通過傳遞數組指針來實現,除此之外,Go 語言中數組還有一個近親 slice,也就是切片,它可以實現類似的效果.

  • 切片的聲明和初始化

切片和數組非常類似,創建數組時如果沒有指定數組的長度,那麼最終創建的其實是切片並不是數組.

func TestSliceInit(t *testing.T) {      var s1 [5]int      // [0 0 0 0 0]      t.Log(s1)        var s2 []int      // []      t.Log(s2,len(s2))  }

[]int 沒有指定長度,此時創建的是切片,默認初始化零值是 nil,並不是空數組!

同理,數組可以聲明並初始化,切片也可以,並且語法也很類似,稍不注意還以為是數組呢!

func TestSliceInitValue(t *testing.T) {      var s1 = [5]int{1, 3, 5, 7, 9}      // [1 3 5 7 9]      t.Log(s1)        var s2 = []int{1, 3, 5, 7, 9}      // [1 3 5 7 9]      t.Log(s2)  }

僅僅是沒有指定 [] 中的長度,最終創建的結果就變成了切片,真的讓人眼花繚亂!

數組和切片如此相像,讓人不得不懷疑兩者之間有什麼見不得人的勾當?其實可以從數組中得到切片,下面舉例說明:

func TestSliceFromArray(t *testing.T) {      arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}      // arr =  [0 1 2 3 4 5 6 7 8 9]      t.Log("arr = ", arr)        // arr[2:6] =  [2 3 4 5]      t.Log("arr[2:6] = ", arr[2:6])      // arr[:6] =  [0 1 2 3 4 5]      t.Log("arr[:6] = ", arr[:6])      // arr[2:] =  [2 3 4 5 6 7 8 9]      t.Log("arr[2:] = ", arr[2:])      // arr[:] =  [0 1 2 3 4 5 6 7 8 9]      t.Log("arr[:] = ", arr[:])  }

arr[start:end] 截取數組的一部分得到的結果就是切片,切片的概念也是很形象啊!

和其他主流的編程語言一樣,[start:end] 是一個左閉右開區間,切片的含義也非常明確:

忽略起始索引 start 時,arr[:end] 表示原數組從頭開始直到終止索引 end 的前一位;
忽略終止索引 end 時,arr[ start:] 表示原數組從起始索引 start 開始直到最後一位;
既忽略起始索引又忽略終止索引的情況,雖然不常見但是含義上將應該就是原數組,但是記得類型是切片不是數組喲!

目前為止,我們知道切片和數組很相似,切片相對於數組只是沒有大小,那麼切片和數組的操作上是否一樣呢?

func updateSlice(s []int) {      s[0] = 666  }    func TestUpdateSlice(t *testing.T) {      arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}        // arr =  [0 1 2 3 4 5 6 7 8 9]      t.Log("arr = ", arr)        s1 := arr[2:6]      // s1 =  [2 3 4 5]      t.Log("s1 = ", s1)        s2 := arr[:6]      // s2 =  [0 1 2 3 4 5]      t.Log("s2 = ", s2)        updateSlice(s1)      // s1 =  [666 3 4 5]      t.Log("s1 = ", s1)      // arr =  [0 1 666 3 4 5 6 7 8 9]      t.Log("arr = ", arr)        updateSlice(s2)      // s2 =  [666 1 666 3 4 5]      t.Log("s2 = ", s2)      // arr =  [666 1 666 3 4 5 6 7 8 9]      t.Log("arr = ", arr)  }

切片竟然可以更改傳遞參數,這一點可是數組沒有做到的事情啊!除非使用數組的指針類型,切片竟然可以輕易做到?除非切片內部是指針,因為參數傳遞只有值傳遞,根本沒有引用傳遞方式!

切片和數組在參數傳遞的表現不同,具體表現為數組進行參數傳遞時無法修改數組,想要想改數組只有傳遞數組指針才行,而切片卻實現了數組的改變!

由於參數傳遞只有值傳遞一種方式,因此推測切片內部肯定存在指針,參數傳遞時傳遞的是指針,所以函數內部的修改才能影響到到函數外部的變量.

go-container-about-slice-struct.png

slice 的內部實現中有三個變量,指針 ptr,個數 len 和容量 cap ,其中 ptr 指向真正的數據存儲地址.

正是由於切片這種內部實現,需要特性也好表現形式也罷才使得切換和數組有着千絲萬縷的聯繫,其實這種數據結果就是對靜態數組的擴展,本質上是一種動態數組而已,只不過 Go 語言叫做切片!

切片是動態數組,上述問題就很容易解釋了,參數傳遞時傳遞的是內部指針,因而雖然是值傳遞拷貝了指針,但是指針指向的真正元素畢竟是一樣的,所以切片可以修改外部參數的值.

數組可以在一定程度上進行比較,切片是動態數組,能不能進行比較呢?讓接下來的測試方法來驗證你的猜想吧!

go-container-about-slice-compare.png

不知道你有沒有猜對呢?切片並不能進行比較,只能與 nil 進行判斷.

  • 切片的添加和刪除

數組是靜態結構,數組的大小不能擴容或縮容,這種數據結構並不能滿足元素個數不確定場景,因而才出現動態數組這種切片,接下來重點看下切片怎麼添加或刪除元素.

func printSlice(s []int) {      fmt.Printf("s = %v, len(s) = %d, cap(s) = %dn", s, len(s), cap(s))  }    func TestSliceAutoLonger(t *testing.T) {      var s []int      // []      t.Log(s)        for i := 0; i < 10; i++ {          s = append(s, i)            printSlice(s)      }        // [0 1 2 3 ...,98,99]      t.Log(s)        for i := 0; i < 10; i++ {          s = s[1:]            printSlice(s)      }        // [0 1 2 3 ...,98,99]      t.Log(s)  }

添加元素 s = append(s, i) 需要擴容時,每次以 2 倍進行擴容,刪除元素 s[1:] 時,遞減縮容.

s = append(s, i) 向切片中添加元素並返回新切片,由於切片是動態數組,當切片內部的數組長度不夠時會自動擴容以容納新數組,擴容前後的內部數組會進行元素拷貝過程,所以 append 會返回新的地址,擴容後的地址並不是原來地址,所以需要用變量接收添加後的切片.

當不斷進行切片重新截取時 s[1:] ,切片存儲的元素開始縮減,個數遞減,容量也遞減.

go-container-about-slice-add-and-delete.png

其實除了基於數組創建切片和直接創建切片的方式外,還存在第三種創建切片的方式,也是使用比較多的方式,那就是 make 函數.

func TestMakeSlice(t *testing.T) {      s1 := make([]int,10)        // s1 = [0 0 0 0 0 0 0 0 0 0], len(s1) = 10, cap(s1) = 10      t.Logf("s1 = %v, len(s1) = %d, cap(s1) = %d", s1, len(s1), cap(s1))        s2 := make([]int, 10, 32)        // s2 = [0 0 0 0 0 0 0 0 0 0], len(s2) = 10, cap(s2) = 32      t.Logf("s2 = %v, len(s2) = %d, cap(s2) = %d", s2, len(s2), cap(s2))  }

通過 make 方式可以設置初始化長度和容量,這是字面量創建切片所不具備的能力,並且這種方式創建的切片還支持批量拷貝功能!

func TestCopySlice(t *testing.T) {      var s1 = []int{1, 3, 5, 7, 9}      var s2 = make([]int, 10, 32)        copy(s2, s1)        // s2 = [1 3 5 7 9 0 0 0 0 0], len(s2) = 10, cap(s2) = 32      t.Logf("s2 = %v, len(s2) = %d, cap(s2) = %d", s2, len(s2), cap(s2))        var s3 []int        copy(s3, s1)        // s3 = [], len(s3) = 0, cap(s3) = 0      t.Logf("s3 = %v, len(s3) = %d, cap(s3) = %d", s3, len(s3), cap(s3))  }

func copy(dst, src []Type) int 是切片之間拷貝的函數,神奇的是,只有目標切片是 make 方式創建的切片才能進行拷貝,不明所以,有了解的小夥伴還請指點一二!

切片的底層結構是動態數組,如果切片是基於數組截取而成,那麼此時的切片從效果上來看,切片就是原數組的一個視圖,對切片的任何操作都會反映到原數組上,這也是很好理解的.

那如果對切片再次切片呢,或者說切片會不會越界,其實都比較簡單了,還是稍微演示一下,重點就是動態數組的底層結構.

func TestSliceOutOfBound(t *testing.T) {      arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}        s1 := arr[2:6]      // s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6      t.Logf("s1 = %v, len(s1) = %d, cap(s1) = %d", s1, len(s1), cap(s1))        s2 := s1[3:5]      // s2 = [5 6], len(s2) = 2, cap(s2) = 3      t.Logf("s2 = %v, len(s2) = %d, cap(s2) = %d", s2, len(s2), cap(s2))  }

[] 只能訪問 len(arr) 範圍內的元素,[:] 只能訪問 cap(arr) 範圍內的元素,一般而言 cap >= len 所以某些情況看起來越界,其實並不沒有越界,只是二者的標準不同!

我們知道切片 slice 的內部數據結構是基於動態數組,存在三個重要的變量,分別是指針 ptr,個數 len 和容量 cap ,理解了這三個變量如何實現動態數組就不會掉進切片的坑了!

個數 len 是通過下標訪問時的有效範圍,超過 len 後會報越界錯誤,而容量 cap 是往後能看到的最大範圍,動態數組的本質也是控制這兩個變量實現有效數組的訪問.

go-container-about-slice-outOfBound-len.png

因為 s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6 ,所以 [] 訪問切片 s1 元素的範圍是[0,4) ,因此最大可訪問到s1[3],而 s1[4] 已經越界了!

go-container-about-slice-outOfBound-cap.png

因為 s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6 ,所以 [:] 根據切片 s1 創建新切片的範圍是 [0,6] ,因此最大可訪問範圍是 s1[0:6] ,而 s1[3:7] 已經越界!

集合 map

集合是一種鍵值對組成的數據結構,其他的主流編程語言也有類似概念,相比之下,Go 語言的 map 能裝載的數據類型更加多樣化.

  • 字面量創建 map 換行需保留逗號 ,
func TestMap(t *testing.T) {      m1 := map[string]string{          "author": "snowdreams1006",          "website": "snowdreams1006",          "language": "golang",      }        // map[name:snowdreams1006 site:https://snowdreams1006.github.io]      t.Log(m1)  }

一對鍵值對的結尾處加上逗號 , 可以理解,但是最後一個也要有逗號這就讓我無法理解了,Why ?

  • make 創建的 map 和字面量創建的 map 默認初始化零值不同
func TestMapByMake(t *testing.T) {      // empty map      m1 := make(map[string]int)        // map[] false      t.Log(m1, m1 == nil)        // nil      var m2 map[string]int        // map[] true      t.Log(m2, m2 == nil)  }

make 函數創建的 map 是空 map,而通過字面量形式創建的 mapnil,同樣的規律也適合於切片 slice.

  • range 遍歷 map 是無序的
func TestMapTraverse(t *testing.T) {      m := map[string]string{          "name": "snowdreams1006",          "site": "https://snowdreams1006.github.io",      }        // map[name:snowdreams1006 site:https://snowdreams1006.github.io]      t.Log(m)        for k, v := range m {          t.Log(k, v)      }        t.Log()        for k := range m {          t.Log(k)      }        t.Log()        for _, v := range m {          t.Log(v)      }  }

這裡再一次遇到 range 形式的遍歷,忽略鍵或值時用 _ 佔位,也是和數組,切片的把遍歷方式一樣,唯一的差別就是 map 沒有索引,遍歷結果也是無序的!

  • 獲取元素時需判斷元素是否存在
func TestMapGetItem(t *testing.T) {      m := map[string]string{          "name": "snowdreams1006",          "site": "https://snowdreams1006.github.io",      }        // snowdreams1006      t.Log(m["name"])        // zero value is empty string      t.Log(m["author"])        // https://snowdreams1006.github.io      if site, ok := m["site"]; ok {          t.Log(site)      } else {          t.Log("key does not exist ")      }  }

Go 語言的 map 獲取不存在的鍵時,返回的是值對應類型的零值,map[string]string 返回的默認零值就是空字符串,由於不會報錯進行強提醒,這也就要求我們調用時多做一步檢查.當鍵值對存在時,第二個返回值返回 true,不存在時返回 false.

  • 刪除鍵值對時用 delete 函數
func TestMapDeleteItem(t *testing.T) {      m := map[string]string{          "name": "snowdreams1006",          "site": "https://snowdreams1006.github.io",      }        // map[name:snowdreams1006 site:https://snowdreams1006.github.io]      t.Log(m)        delete(m, "name")        // map[site:https://snowdreams1006.github.io]      t.Log(m)        delete(m, "id")        // map[site:https://snowdreams1006.github.io]      t.Log(m)  }

delete(map,key) 用於刪除 map 的鍵值對,如果想要驗證是否刪除成功,別忘了使用 value,ok := m[k] 確定是否存在指定鍵值對

  • slice,map,func 外,其餘類型均可鍵

因為 map 是基於哈希表實現,所以遍歷是無序的,另一方面因為 slice,map,func 不可比較,因為也不能作為鍵.當然若自定義類型 struc 不包含上述類型,也可以作為鍵,並不要求實現 hashcodeequal 之類的.

  • value 可以承載函數 func 類型
func TestMapWithFunValue(t *testing.T) {      m := map[int]func(op int) int{}        m[1] = func(op int) int {          return op      }      m[2] = func(op int) int {          return op * op      }      m[3] = func(op int) int {          return op * op * op      }        // 1 4 27      t.Log(m[1](1), m[2](2), m[3](3))  }

再一次說明函數是一等公民,這部分會在以後的函數式編程中進行詳細介紹.

沒有 set

Go 的默認類型竟然沒有 set 這種數據結構,這在主流的編程語言中算是特別的存在了!

正如 Go 的循環僅支持 for 循環一樣,沒有 while 循環一樣可以玩出 while 循環的效果,靠的就是增強的 for 能力.

所以,即使沒有 set 類型,基於現有的數據結構一樣能實現 set 效果,當然直接用 map 就可以封裝成 set.

func TestMapForSet(t *testing.T) {      mySet := map[int]bool{}        mySet[1] = true        n := 3        if mySet[n] {          t.Log("update", mySet[n])      } else {          t.Log("add", mySet[n])      }        delete(mySet, 1)  }

使用 map[type]bool 封裝實現 set 禁止重複性元素的特性,等到講解到面向對象部分再好好封裝,這裡僅僅列出核心結構.

知識點總結梳理

Go 語言是十分簡潔的,不論是基礎語法還是這一節的內建容器都很好的體現了這一點.

數組作為各個編程語言的基礎數據結構,Go 語言和其他主流的編程語言相比沒有什麼不同,都是一片連續的存儲空間,不同之處是數組是值類型,所以也是可以進行比較的.

這並不是新鮮知識,畢竟上一節內容已經詳細闡述過該內容,這一節的重點是數組的衍生版切片 slice .

因為數組本身是特定長度的連續空間,因為是不可變的,其他主流的編程語言中有相應的解決方案,其中就有不少數據結構的底層是基於數組實現的,Go 語言的 slice 也是如此,因此個人心底里更願意稱其為動態數組!

切片 slice 的設計思路非常簡單,內部包括三個重要變量,包括數組指針 ptr,可訪問元素長度 len 以及已分配容量 cap .

當新元素不斷添加進切片時,總會達到已最大分配容量,此時切片就會自動擴容,反之則會縮容,從而實現了動態控制的能力!

  • 指定元素個數的是數組,未指定個數的是切片
func TestArrayAndSlice(t *testing.T) {      // array      var arr1 [3]int      // slice      var arr2 []int        // [0 0 0] []      t.Log(arr1,arr2)  }
  • 基於數組創建的切片是原始數組的視圖
func TestArrayAndSliceByUpdate(t *testing.T) {      arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}        // arr =  [0 1 2 3 4 5 6 7 8 9]      t.Log("arr = ", arr)        s := arr[2:6]        // before update s = [2 3 4 5], arr = [0 1 2 3 4 5 6 7 8 9]      t.Logf("before update s = %v, arr = %v", s, arr)        s[0] = 666        // after update s = [666 3 4 5], arr = [0 1 666 3 4 5 6 7 8 9]      t.Logf("after update s = %v, arr = %v", s, arr)  }
  • 添加或刪除切片元素都返回新切片
func TestArrayAndSliceIncreasing(t *testing.T) {      var s []int        fmt.Println("add new item to slice")        for i := 0; i < 10; i++ {          s = append(s, i)            fmt.Printf("s = %v, len(s) = %d, cap(s) = %dn", s, len(s), cap(s))      }        fmt.Println("remove item from slice")        for i := 0; i < 10; i++ {          s = s[1:]            fmt.Printf("s = %v, len(s) = %d, cap(s) = %dn", s, len(s), cap(s))      }  }
  • [index] 訪問切片元素僅僅和切片的 len 有關,[start:end] 創建新切片僅僅和原切片的 cap 有關
func TestArrayAndSliceBound(t *testing.T) {      arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}        s1 := arr[5:8]        // s1[0] = 5, s1[2] = 7      t.Logf("s1[0] = %d, s1[%d] = %d", s1[0], len(s1)-1, s1[len(s1)-1])      // s1 = [5 6 7], len(s1) = 3, cap(s1) = 5      t.Logf("s1 = %v, len(s1) = %d, cap(s1) = %d", s1, len(s1), cap(s1))        s2 := s1[3:5]        // s2[0] = 8, s2[1] = 9      t.Logf("s2[0] = %d, s2[%d] = %d", s2[0], len(s2)-1, s2[len(s2)-1])      // s2 = [8 9], len(s2) = 2, cap(s2) = 2      t.Logf("s2 = %v, len(s2) = %d, cap(s2) = %d", s2, len(s2), cap(s2))  }
  • 只有 map 沒有 set
func TestMapAndSet(t *testing.T) {      m := map[string]string{          "name": "snowdreams1006",          "site": "https://snowdreams1006.github.io",          "lang": "go",      }        // https://snowdreams1006.github.io      if site, ok := m["site"]; ok {          t.Log(site)      } else {          t.Log("site does not exist ")      }        s := map[string]bool{          "name": true,          "site": true,          "lang": true,      }        // Pay attention to snowdreams1006      if _, ok := m["isFollower"]; ok {          t.Log("Have an eye on snowdreams1006")      } else {          s["isFollower"] = true          t.Log("Pay attention to snowdreams1006")      }  }
  • delete 函數刪除集合 map 鍵值對
func TestMapAndSetByDelete(t *testing.T) {      m := map[string]string{          "name": "snowdreams1006",          "site": "https://snowdreams1006.github.io",          "lang": "go",      }        delete(m, "lang")        // delete lang successfully      if _,ok := m["lang"];!ok{          t.Log("delete lang successfully")      }  }

關於 Go 語言中內建容器是不是都已經 Get 了呢?如果有表述不對的地方,還請指正哈,歡迎一起來公眾號[雪之夢技術驛站]學習交流,每天進步一點點!

雪之夢技術驛站.png