iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
自我挑戰組

Go in 3o系列 第 6

[Day06] Go in 30 - 複合型別

  • 分享至 

  • xImage
  •  

一、本章簡介

本章會介紹的是Go語言中較為複雜一點的型別 :

  • 陣列(array)
  • 切片(slice)
  • 映射表(map)

二、集合型別(Collection types)

2.0 陣列 array

最基本的集合形式,它的定義方式如下:

var arr [n]type // n為陣列長度

陣列元素可以是任意型別,但只能是一種型別。
此外,宣告陣列時必須指定長度
如果沒指定長度,反而會變成切片(slice),切片是一種操作彈性更大的的集合形式。

範例:

var arr [10]int  // 宣告了一個 int 型別的陣列
arr[0] = 10      // 陣列是從 0 開始的
arr[1] = 11      // 賦值

2.0.1 如果要在宣告時為陣列給予初值 :

var arr [n]type{<value1>, <value2>, <value3>,...<valueN>}

例如:

[5]int{1} //如此一來就宣告了一個陣列且第一個元素為1,其餘四個為0值

2.0.2 根據提供的初始值數量來設定陣列長度 :
用三個點...表示。

var arr [...]type{<value1>, <value2>, <value3>,...<valueN>}

例如:

var arr [...]int{1,2,3,4,5} //宣告了一個長度為5的陣列

不管你用哪一種宣告方式,陣列的長度會在最一開始就決定!!

2.0.3 透過索引鍵賦值

var arr [...]type{<索引鍵1> : <value1>, <索引鍵2> : <value2>, <索引鍵3> : <value3>,...<索引鍵N> : <valueN>}

例如 :

package main

import "fmt"

func main() {
    var arr1 [10]int
    var arr2 = [...]int{9:0}
    var arr3 = [10]int{1, 9: 10, 4: 5}
    fmt.Println(arr1)
    fmt.Println(arr2)
    fmt.Println(arr3)
}

結果

[0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0]
[1 0 0 0 5 0 0 0 0 10]

2.0.4 讀取陣列元素

package main

import "fmt"

func main() {
    // 創建一個整數陣列
    numbers := [5]int{1, 2, 3, 4, 5}

    // 讀取陣列中的元素
    // 陣列索引從 0 開始,所以第一個元素是 numbers[0]
    firstElement := numbers[0]
    // 第二個元素是 numbers[1]
    secondElement := numbers[1]
    // 以此類推
    thirdElement := numbers[2]
    fourthElement := numbers[3]
    fifthElement := numbers[4]

    // 輸出讀取的元素值
    fmt.Println("第一個元素:", firstElement)   // 輸出:第一個元素: 1
    fmt.Println("第二個元素:", secondElement) // 輸出:第二個元素: 2
    fmt.Println("第三個元素:", thirdElement)   // 輸出:第三個元素: 3
    fmt.Println("第四個元素:", fourthElement) // 輸出:第四個元素: 4
    fmt.Println("第五個元素:", fifthElement)   // 輸出:第五個元素: 5
}

2.0.5 將值寫入陣列

package main

import "fmt"

func main() {
    // 創建一個整數陣列
    numbers := [5]int{}

    // 將值寫入陣列
    numbers[0] = 1
    numbers[1] = 2
    numbers[2] = 3
    numbers[3] = 4
    numbers[4] = 5

    // 輸出陣列的內容
    fmt.Println("陣列的內容:", numbers) // 輸出:陣列的內容: [1 2 3 4 5]
}

2.0.6 走訪陣列

for i := 0; i < len(<陣列>); i++ {
 // 存取<陣列>[i]
}

範例:

package main

import "fmt"

func main() {
    // 創建一個整數陣列
    numbers := [5]int{1, 2, 3, 4, 5}

    // 使用 for 迴圈走訪陣列
    for i := 0; i < len(numbers); i++ {
        fmt.Printf("索引 %d 的元素值是 %d\n", i, numbers[i])
    }

    // 使用 range 關鍵字走訪陣列
    for index, value := range numbers {
        fmt.Printf("索引 %d 的元素值是 %d\n", index, value)
    }
}

Note len()效率: 我們不需要去擔心Go每次執行len()時都需要重新計算陣列大小,因為陣列的長度早就是陣列型別定義的一部分,Go語言平時也會追蹤他們元素的數量

2.1 切片(slice)

陣列雖然方便但硬性規定宣告時必須要長度,只能處理特定長度,在很多應用場景中,陣列並不能滿足我們的需求。在初始定義陣列時,我們不確定需要多大的陣列,所以我們就需要“動態陣列”。在 Go 裡面這種資料結構叫slice,也就是希望兼顧陣列的便利性,與不受長度限制。

切片也可以使用append()去新增切片元素。

2.1.1 使用切片

// 和宣告 array 一樣,只是少了長度
var iAmSlice []int

例如:

slice := []byte {'a', 'b', 'c', 'd'}

slice可以從一個陣列或一個已經存在的 slice 中再次宣告。
slice透過 array[i:j] 來取得,其中 i 是陣列的開始位置,j是結束位置,但不包含array[j],它的長度是j-i。

package main

import "fmt"

func main() {
     // 建立一個整數數組
     numbers := [5]int{1, 2, 3, 4, 5}

     // 從陣列建立一個切片
     // 切片包含陣列的第二個到第四個元素,不包含 numbers[4]
     slice1 := numbers[1:4]

     // 列印切片的內容
     fmt.Println("切片1的內容:", slice1) // 輸出:切片1的內容: [2 3 4]

     // 修改切片的元素會影響原始數組
     slice1[0] = 99
     fmt.Println("修改後的切片1的內容:", slice1) // 輸出:修改後的切片1的內容: [99 3 4]
     fmt.Println("原始陣列的內容:", numbers) // 輸出:原始陣列的內容: [1 99 3 4 5]

     // 從現有切片建立一個新的切片
     // 新切片包含切片1的第一個到第三個元素,不包含 slice1[2]
     slice2 := slice1[0:2]

     // 列印新切片的內容
     fmt.Println("切片2的內容:", slice2) // 輸出:切片2的內容: [99 3]

     // 修改新切片的元素也會影響原始陣列和原始切片
     slice2[1] = 88
     fmt.Println("修改後的切片2的內容:", slice2) // 輸出:修改後的切片2的內容: [99 88]
     fmt.Println("修改後的切片1的內容:", slice1) // 輸出:修改後的切片1的內容: [99 88 4]
     fmt.Println("原始陣列的內容:", numbers) // 輸出:原始陣列的內容: [1 99 88 4 5]
}

slice 有一些簡便的操作:

  • slice的預設開始位置是 0,array[:n]等價於array[0:n]
  • slice的第二個序列預設是陣列的長度,array[n:]等價於array[n:len(array)]
  • 如果從一個陣列裡面直接取得slice,可以這樣array[:],因為預設第一個序列是 0,第二個是陣列的長度,即等於array[0:len(array)]

2.1.2 為切片加入多個新元素與解包算符(unpack operator)

將兩個切片接起來。

package main

import "fmt"

func main() {
     // 建立一個整數切片
     numbers := []int{1, 2, 3}

     // 在切片中新增一個新元素
     numbers = append(numbers, 4)

     // 列印切片內容
     fmt.Println("切片內容:", numbers) // 輸出:切片內容: [1 2 3 4]

     // 在切片中新增多個新元素
     newElements := []int{5, 6, 7}
     numbers = append(numbers, newElements...)

     // 列印切片內容
     fmt.Println("切片內容:", numbers) // 輸出:切片內容: [1 2 3 4 5 6 7]
}

2.2.3 切片的工作原理

切片的內部結構可以概括為包含三個元素(隱藏屬性):

指標(Pointer):指向底層陣列的第一個元素。
長度(Length):切片中的元素數量,即切片的實際長度。
最大長度(Capacity):切片從其起始位置到底層陣列的末端的最大長度。

這個內部結構使切片非常靈活,因為它們可以動態增長,而不需要複製整個陣列。 當切片的長度超過其最大長度時,會建立一個新的隱藏陣列,並將元素複製到新的數組中。

當我們使用 append() 函數將一個新的元素加入切片時,可能會發生以下情況:

  • 如果切片的容量還有剩餘(切片的長度小於容量),新的元素將直接加入到隱藏陣列中,並更新切片的長度。

  • 如果切片的容量已經滿了,Go 將會建立一個更大的新隱藏陣列,然後將舊數組中的元素複製到新隱藏陣列中,並將新元素新增到新隱藏陣列中。 接著,切片的引用將更新為新的底隱藏陣列,同時也會更新切片的長度。 需要注意的是,切片的起始位置可能會隨著底層陣列的更換而改變。

  • 切片也會記住它在原始隱藏陣列中的起始位置,所以如果一個切片與原始數組具有相同的長度,它將引用隱藏陣列的全部元素。 這使得切片非常適合在處理動態大小的資料集時,因為它們可以自動擴展隱藏陣列以適應更多的元素。

用 make 控制切片容量

使用 make 函數可以建立指定長度和容量的切片。** make 接受三個參數:切片的類型、長度和容量。** 容量是一個可選參數,如果省略,切片的容量將與長度相同。

package main

import "fmt"

func main() {
     // 建立一個長度為3,容量為5的整數切片
     slice1 := make([]int, 3, 5)

     // 列印切片的長度和容量
     fmt.Printf("切片1的長度:%d,容量:%d\n", len(slice1), cap(slice1))

     // 新增元素到切片
     slice1[0] = 1
     slice1[1] = 2
     slice1[2] = 3

     // 列印切片的內容
     fmt.Println("切片1的內容:", slice1)

     // 使用 append 函數為切片新增元素
     slice1 = append(slice1, 4, 5)

     // 列印切片的長度和容量
     fmt.Printf("切片1的長度:%d,容量:%d\n", len(slice1), cap(slice1))

     // 列印切片的內容
     fmt.Println("切片1的內容:", slice1)
}

輸出結果:

切片1的長度:3,容量:5
切片1的內容: [1 2 3]
切片1的長度:5,容量:5
切片1的內容: [1 2 3 4 5]

slice 幾個內建函式 :

  • len 取得 slice 的長度
  • cap 取得 slice 的最大容量
  • append 向 slice 裡面追加一個或者多個元素,然後回傳一個和 slice 一樣型別的slice。
  • copy 函式 copy 從原 slice 的中複製元素到目標,並且回傳複製的元素的個數

**2.3 映射表 map **

Go語言的map是一種雜湊表(hashmap),具備key與value,鍵值對。

map [keyType] valueType

建立、讀取、寫入map範例:

package main

import "fmt"

func main() {
     // 建立一個空的map,鍵是字串,值是整數
     var ages map[string]int

     // 使用make函數初始化map
     ages = make(map[string]int)

     // 新增鍵值對到map
     ages["Alice"] = 25
     ages["Bob"] = 30
     ages["Charlie"] = 35

     // 存取map中的值
     aliceAge := ages["Alice"]
     fmt.Println("Alice的年齡是:", aliceAge)

     // 刪除map中的鍵值對
     delete(ages, "Bob")

     // 檢查map中是否存在某個鍵
     if _, exists := ages["Bob"]; !exists {
         fmt.Println("Bob不存在於map")
     }

     // 遍歷map的所有鍵值對
     for name, age := range ages {
         fmt.Printf("%s的年齡是:%d\n", name, age)
     }
}

輸出結果:

Alice的年齡是: 25
Bob不存在於map
Alice的年齡是:25
Charlie的年齡是:35

說明一下:

由於Go不會幫你初始化map鍵值(map的零值就是nil),必須在定義map時一併附於鍵值:

map [keyType] valueType {<key1>:<value1>,<key2>:<value2>,<key3>:<value3>,...<keyN>:<valueN>}

如果嘗試對一個未經初始化的map賦值,就會引發執行期間panic,所以一定要避免定義一個零值map。

2.3.1 初始化
更常用的方式是使用make()函示回傳一個經過初始化的map,但make()在此傳入的參數會與初始化切片時有所不同。

make(map [keyType] valueType , capacity)

Go不會替map建立索引鍵,所以沒辦法像在slice使用make()時給他指定長度,有不能用cap()來查詢map容量。

2.3.2 初始化後加入元素

<map名稱>[<索引鍵>] = <值>

從map讀取元素

在Go語言中,當你嘗試透過鍵在map中取得值時,會傳回兩個值:值本身和一個存在狀態(true表示存在,false表示不存在)。 這種方式可以幫助你判斷鍵是否存在,避免了嘗試取得不存在的鍵而導致的運行時錯誤。

當然也是可以先檢查零值來判斷索引鍵是否存在,但這樣無法永遠擔保零值就表示查無此鍵(零值有可能是其他意義的資料,這取決於具體的資料類型和上下文)。

因此,為了準確判斷鍵是否存在,最好還是使用存在狀態來檢查,而不是只依賴零值。這種方式更加可靠和安全。
在判斷鍵是否存在時,可以使用_, exists := mapName[key]的方式,確保了你可以明確知道鍵是否存在,而不會受到零值的影響。

<值>, <存在狀態> := <map名稱>[<索引值>]
package main

import "fmt"

func main() {
     // 建立一個範例 map,鍵為字串,值為整數
     ages := map[string]int{
         "Alice": 25,
         "Bob": 30,
         "Charlie": 35,
     }

     // 要檢查的鍵
     keyToCheck := "Bob"

     // 使用存在狀態來檢查鍵是否存在
     value, exists := ages[keyToCheck]

     // 判斷鍵是否存在
     if exists {
         fmt.Printf("%s 的年齡是 %d 歲\n", keyToCheck, value)
     } else {
         fmt.Printf("%s 不在 map 中\n", keyToCheck)
     }

     // 另一個例子,檢查一個不存在的鍵
     keyToCheck = "David"
     value, exists = ages[keyToCheck]

     // 判斷鍵是否存在
     if exists {
         fmt.Printf("%s 的年齡是 %d 歲\n", keyToCheck, value)
     } else {
         fmt.Printf("%s 不在 map 中\n", keyToCheck)
     }
}

以上就是複合型別的部分整理,關於slice底層運作還有一些東西需要說明,先交了之後再補充,要沒時間了牙敗~~


上一篇
[Day05] Go in 30 - 核心型別
下一篇
[Day07] Go in 30 - 簡易自訂型別(custom types)、結構(Struct)介紹
系列文
Go in 3o30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言