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

上篇文章中詳細介紹了 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) = %d\n", 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) = %d\n", 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) = %d\n", 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章