Contents

Golang - 複合類型 (Composite Types)

本篇文章基本上是介紹 Go 的複合類型(Array, Slice, Map)與內置函式,另外會簡單介紹 struct。

Arrays

如同其他程式語言,Go 也有 Array,Array 中的所有元素,都必須是指定的類型。

var x [3]int

此時會建立長度為 3 的 Array,但未指定初始值,上一章提到若未指定初始值,則預設值會是 Zero Value,在這個 Array 中,類型是 int,故初始值會是 0

若要指定初始值,則可以使用這個方式。

var x = [3]int{10,20,30}

也可以用 index 的方式指定初始值。

// index:value
var x = [10]int{1,5:2}
// [1,0,0,0,0,2,0,0,0,0]

若已經很明確知道,陣列內的所有數值,在宣告時則可以用 [...] 省略陣列的長度。

var x = [...]int{10,20,30}
var y = [3]int{10,20,30}
// 這兩個 Array 是一樣的。

在 Go 中只有一維陣列,但可以模擬出多維陣列。

var x [5][10]int

可以透過內置函式 len() 得到 Array 的長度。

fmt.Println(len(x))

但在 Go 中,很少使用 Array,因為 Array 的大小會被視為是 Array 類型的一部分,[3]int是一種類型,[4]int是另一種類型。 因此:

  • 不能使用變數來指定 Array 的大小,因為類型必須在編譯前解析,而不是在運作時解析。
  • 不能使用類型轉換,無法將不同大小的 Array 轉換成相同的類型。
  • 最好是知道需要的長度,否則盡量不要使用 Array。

Slices

若資料的長度會變化,且不想受到 Array 的類型限制時,應該使用 Slices,宣告的方式與 Array 極為相似,但不指定大小。

這樣會宣告一個長度為 3 的 Slice。

var x = []int{10,20,30}

也可以用 index 的方式指定初始值。

// index:value
var x = []int{1,5:2,88}
// [1,0,0,0,0,2,88]

Slice 也可以模擬出多維的 Slice。

var x [][]int

比較大的差別在於,Slice 的 Zero Value 並不會是宣告時之類型的 Zero Value,而會是 nil,它是一種標示符號,代表某些類型缺少數值。 nil 也沒有類型,因此可以賦予值或是與其他不同類型的數值進行比較。

Slice 內置函數

len

在前面的例子中有使用過了 len ,傳入 Array 會獲得該 Array 的長度,len 也適用於 Slice。若傳入 nil Slice 會獲得 0。

append

append 用於附加值至 Slice。最少要兩個傳入參數:

  • 一個任意類型的 Slice
  • 一個該類型的數值。
var x []int
x = append(x,10)// [10]
// 可以同時附加多個數值。
x = append(x,20,30,40) //[10,20,30,40]
// 甚至是附加 Slice。但需要使用...這個運算符
y := []int{50,60,70}
x = append(x,y...) //[10,20,30,40,50,60,70]

cap (Capacity)

Slice 是一系列連續的數值,每個元素被分配到連續的記憶體位置,這樣可以快速讀取或寫入這些數值。每個 Slice 都有一個 Capacity,即保留的連續記憶體位置的數量。

當使用 append 附加數值至 Slice 時,長度也會增加,當長度達到 Capacity,代表沒有空間存放資料了,若又使用 append 附加數值時,則會分配有更大 Capacity 的 Slice,並將原本 Slice 的數值複製到新的 Slice 後,將新的數值附加到 Slice 的最後,並返回新的 Slice。

cap() 可以獲得該 Slice 目前的 Capacity。

但 Capacity 增長,視 Go 語言的版本不同,基本上都會是增長原本大小的一倍,當已經知道某組數值的確切長度時,是否能指定 Capacity 的大小而不造成記憶體的浪費呢? 此時可以透過 make 內置函數建立確切 Capacity 的 Slice。

make

make 可以聲明指定長度、類型與容量的 Slice。

這樣會建立一個類型為 int、長度與容量為5的slice,由於它的長度為5,故它的第0個至第4個元素是有效元素,會被初始化為 int 的 Zero Value,也就是 0 。

x := make([]int, 5)

若要指定容量,則在傳入參數內加入所需的容量大小。

x := make([]int, 5, 10)

甚至建立一個長度為 0 的 Slice 也是可行的。

x := make([]int, 0, 10)
// 但後面使用,記得用 append 附加數值,長度為 0 是沒辦法做 index 的。
  • 記住,絕對不要指定一個小於長度的 Capacity,也盡量不要使用變數指定 Capacity。

宣告 Slice

Slice 在宣告時有不同的方式,但最主要的目標是將 Slice 增長的次數最小化,若 Slice 不會增長,請使用 var 建立 nil slice。

var data []int

若有初始值,或是 Slice 的數值不會改變,會建議就是以賦值方式宣告。

data := []int(1, 2, 3, 4)

若很清楚 Slice 需要多大,但不清楚內部的數值會是甚麼,請使用 make 宣告 Slice。

  • 若以 Slice 作為 buffer 使用,則指定一個非 0 長度的 Slice。
  • 若確定 Slice 的大小,則可以指定 Slice 的長度。
  • 在其他情況下,使用 make 宣告 0 長度與指定 Capacity 的 Slice,並使用 append 增加數值。

Slicing Slices

Slice 的表達式由一個起始偏移量和一個結束偏移量組成,並用 : 分隔。若省略起始偏移量,則為0,若省略結束偏移量,則為結尾。

x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
d := x[1:3]
e := x[:]
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
fmt.Println("d:", d)
fmt.Println("e:", e)
// 輸出
x: [1 2 3 4] 
y: [1 2] 
z: [2 3 4] 
d: [2 3] 
e: [1 2 3 4]

另外以上述的 Slice 表達式,他們之間是共享記憶體的。使用上請小心。

x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
x[1] = 20
y[0] = 10
z[1] = 30
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
// 輸出
x[10 20 30 4] 
y[10 20] 
z[20 30 4]

Copy

若我想要建立一個 Slice,並使用原始 Slice 的數值,但不共享記憶體的獨立 Slice 時,請使用內置的 Copy 函式。

Copy 函式有兩個參數,第一個是目標 Slice,第二個是來源 Slice,並會盡量把數值複製到目標,並返回複製的 element 數量。

x := []int{1, 2, 3, 4}
y := make([]int, 4)
num := copy(y, x)
fmt.Println(y, num)
z := make([]int, 2)
num := copy(z, x)
fmt.Println(z, num)
// 輸出
[1 2 3 4] 4
[1 2] 2

也可以透過 Slice 表達式,複製部分的數值。

x := []int{1, 2, 3, 4}
y := make([]int, 2)
copy(y, x[2:]) // 若不需要返回值,不用特地設一個變數然後執行 copy()。

copy 也可以用來把 Slice 的某部分覆蓋至別的部分。

x := []int{1, 2, 3, 4}
num = copy(x[:3], x[1:])
fmt.Println(x, num)
// 輸出
[2 3 4 4] 3

Strings and Runes and Bytes

Go 使用一個 Bytes 序列代表一個 String。

var s string = "Hello there"
var b byte = s[6] // t

前述提到的 Slice 表達式,也可以用在 String。 但建議在 String 每一個字元都是一個 Byte 大小時再使用。(e.g. emoji是4個bytes)

單個 rune 或是 byte 可以使用類型轉換成 string。

但常使用的 int ,透過類型轉換的話,會變成 ascii 而不是直接轉換,若要單純的把 int 轉換成 string,請使用 strconv.Itoa()參考資料

字串也可以轉成 rune slice 或是 byte slice,使用方式也不困難,通常會使用 byte slice 做轉換。

var s string = "Hello, world!"
var bs []byte = []byte(s)
var rs []rune = []rune(s)

Map

intro

Map 其實與其他程式語言相似,將一個數值關聯到另一個數值的類型。Map 的 Zero Value 是 nil。

var nilMap map[string]int

但這種方式,在寫入 nil 時會導致恐慌,可以透過另一個方法建立映射變量。

myMap := map[string]int{}

若知道確切的數值,可以用賦值宣告的方式建立Map。

宣告方式為 key:value,每組數值都用 , 分隔,即使是最後一組也要加上逗號。

rank := map[string]int{
    "I" : 1,
    "you" : 2,
    "she" : 3,
}

若知道 Map 的確切大小但不清楚內部數值,可以使用 make 建立有默認大小的 map

ages := make(map[int][]string, 10)

Map 與 Slice 相似的地方:

  • 增加 key:value pair 數據時,Map 會自動增長。
  • 若知道會有多少筆數據,則可以使用 make 建立有初數大小的 Map。
  • 將 Map 傳遞給 len ,可以獲得 key:value pair 的數量。
  • Map 的 Zero Value 是 nil。
  • Map 沒有可比性,只能檢查是否等於 nil,但無法檢查兩個 Map 是否有相同的 key:value pair。

另外有些要注意的點:

  • key 必須是可以比較的類型,像是slice或是map這種無法比較,無可比性的類型就不能使用。
  • 若數據要按照順序處理,建議使用 Slice,若數據不用嚴格按照順序處理,則可以使用 Map。

Read and Write

myMap := map[string]int{}
myMap["Taipei"] = 1
fmt.Println(totalWins["Taipei"])
fmt.Println(totalWins["I-lan"])
myMap["I-lan"]++
fmt.Println(totalWins["I-lan"])
// 輸出
1
0
1

透過 key 的方式分配 value,這邊要注意是使用 = 而不能使用 :=,若要讀取未分配 value 的 key 之 value 時,則會返回 Zero Value。

The comma ok Idiom

那要如何知道 Map 中,我所需要的 key:value pair 是否在 Map 中?可以使用 comma ok Idiom 的方式區分 key:value pair 是否在 Map。

m := map[string]int{
    "hello": 5,
    "world": 0,
}
v, ok := m["hello"]
fmt.Println(v, ok)

v, ok = m["world"]
fmt.Println(v, ok)

v, ok = m["goodbye"]
fmt.Println(v, ok)
// 輸出
5 true
0 true
0 false

Deleting from Maps

刪除 key:value pair 的方式,透過內置函數 delete即可,

m := map[string]int{
    "hello": 5,
    "world": 10,
}
delete(m, "hello")

若 key 不存在於 map 內,或是說 map 是 nil,則甚麼都不會發生 !!

(補充 : Using Maps as Sets)

因為 Go 沒有 set 這個類型,但可以透過 Map 去實現。

intSet := map[int]bool{}
vals := []int{5, 10, 2, 5, 8, 7, 3, 9, 1, 2, 10}
for _, v := range vals {
    intSet[v] = true

透過 map 與迴圈,將設定的數值與 bool 連結,若有的數值則設定為 true,其他未在內的數值,因為 bool 的 Zero Value,都會是 false。這樣使用上就可以達到 set 的功能。


Struct

若有想要組合在一起的相關數據時,應該訂意一個 struct。

type person struct{
    name string
    age int
    pet string
}

一個 struct 透過關鍵字 type、結構類型的名稱與struct組成。struct 內部則是field,由變數名稱與變數類型。 聲明 struct 後,就可以定義該類型的變數。

基本上這兩種方式,都會將 struct 內的所有 field 設定為 Zero Value。

var renne person
lapis := person{}

若有初始值的話則是依據 field 宣告,記得按照順序,依據類型宣告。

nadia := person{
    "Nadia",
    "18",
    "cat",
}

或者是以類似 key:value pair 的方式宣告。可以不必按照順序宣告,可以指定部分變數即可,沒被指定的會被設定為 Zero Value。(建議用這種 !!)

Tio := person{
    name: "Tio",
    age: "18",
    pet: "cat",
}

struct 內的 field 用 . 進行訪問。

Tio.name = "Tio Plato"
fmt.Println(Tio.name)

Anonymous Structs(匿名結構)

簡單來說,就是實現一個 struct 但不需要先命名,稱為匿名結構。通常用在將外部數據轉換成 struct,或是將 struct 轉換成外部數據(e.g. json),這被稱為unmarshaling and marshaling data

pet := struct {
    name string
    kind string
}{
    name: "Cute",
    kind: "cat",
}

比較與轉換結構

不同類型的結構之變數之間,是不能比較的,除非兩個struct的field具有相同的名稱、順序與類型,才允許進行比較與類型轉換。

在 struct 的比較中,若其中至少有一個匿名結構的話,若兩個結構的 field 有相同的名稱,則可以在不進行類型轉換的情況下進行比較,若兩個結構的 field 具有相同的名稱、順序與類型,還可以在兩個結構之間進行 assign。

type firstPerson struct {
    name string
    age  int
}
f := firstPerson{
    name: "Bob",
    age:  50,
}
var g struct {
    name string
    age  int
}

// compiles -- can use = and == between identical named and anonymous structs
g = f
fmt.Println(f == g)

參考資料(Reference)

  1. Learning Go (書籍)
  2. How to convert an int value to string in go