Golang 學習筆記二 字典 字串 結構體
一、字典
1.make
func main() { var m map[int]string = make(map[int]string) fmt.Println(m, len(m)) } ---------- map[] 0
如果你可以預知字典內部鍵值對的數量,那麼還可以給 make 函式傳遞一個整數值,通知執行時提前分配好相應的記憶體。這樣可以避免字典在長大的過程中要經歷的多次擴容操作。
var m = make(map[int]string, 16)
2.初始化
func main() { var m map[int]string = map[int]string{ 90: "優秀", 80: "良好", 60: "及格",// 注意這裡逗號不可缺少,否則會報語法錯誤 } fmt.Println(m, len(m)) } --------------- map[90:優秀 80:良好 60:及格] 3
3.讀寫
func main() { var fruits = map[string]int { "apple": 2, "banana": 5, "orange": 8, } // 讀取元素 var score = fruits["banana"] fmt.Println(score) // 增加或修改元素 fruits["pear"] = 3 fmt.Println(fruits) // 刪除元素 delete(fruits, "pear") fmt.Println(fruits) } ----------------------- 5 map[apple:2 banana:5 orange:8 pear:3] map[orange:8 apple:2 banana:5]
刪除操作時,如果對應的 key 不存在,delete 函式會靜默處理。遺憾的是 delete 函式沒有返回值,你無法直接得到 delete 操作是否真的刪除了某個元素。這時候必須使用字典的特殊語法,如下
func main() { var fruits = map[string]int { "apple": 2, "banana": 5, "orange": 8, } var score, ok = fruits["durin"] if ok { fmt.Println(score) } else { fmt.Println("durin not exists") } fruits["durin"] = 0 score, ok = fruits["durin"] if ok { fmt.Println(score) } else { fmt.Println("durin still not exists") } } ------------- durin not exists 0
4.遍歷
這個和陣列一樣的
func main() { var fruits = map[string]int { "apple": 2, "banana": 5, "orange": 8, } for name, score := range fruits { fmt.Println(name, score) } for name := range fruits { fmt.Println(name) } } ------------ orange 8 apple 2 banana 5 apple banana orange
奇怪的是,Go 語言的字典沒有提供諸於 keys() 和 values() 這樣的方法,意味著如果你要獲取 key 列表,就得自己迴圈一下,如下
func main() { var fruits = map[string]int { "apple": 2, "banana": 5, "orange": 8, } var names = make([]string, 0, len(fruits)) var scores = make([]int, 0, len(fruits)) for name, score := range fruits { names = append(names, name) scores = append(scores, score) } fmt.Println(names, scores) } ---------- [apple banana orange] [2 5 8]
二、字串

image.png
1.按位元組遍歷
func main() { var s = "嘻哈china" for i:=0;i<len(s);i++ { fmt.Printf("%x ", s[i]) } } ----------- e5 98 bb e5 93 88 63 68 69 6e 61
2.按字元 rune 遍歷
package main import "fmt" func main() { var s = "嘻哈china" for codepoint, runeValue := range s { fmt.Printf("%d %d ", codepoint, int32(runeValue)) } } ----------- 0 22075 3 21704 6 99 7 104 8 105 9 110 10 97
對字串進行 range 遍歷,每次迭代出兩個變數 codepoint 和 runeValue。codepoint 表示字元起始位置,runeValue 表示對應的 unicode 編碼(型別是 rune)。
3.字串是隻讀的
你可以使用下標來讀取字串指定位置的位元組,但是你無法修改這個位置上的位元組內容。如果你嘗試使用下標賦值,編譯器在語法上直接拒絕你。
package main func main() { var s = "hello" s[0] = 'H' } -------- ./main.go:5:7: cannot assign to s[0]
4.位元組切片和字串的相互轉換
在使用 Go 語言進行網路程式設計時,經常需要將來自網路的位元組流轉換成記憶體字串,同時也需要將記憶體字串轉換成網路位元組流。Go 語言直接內建了位元組切片和字串的相互轉換語法。
package main import "fmt" func main() { var s1 = "hello world" var b = []byte(s1)// 字串轉位元組切片 var s2 = string(b)// 位元組切片轉字串 fmt.Println(b) fmt.Println(s2) } -------- [104 101 108 108 111 32 119 111 114 108 100] hello world
從節省記憶體的角度出發,你可能會認為位元組切片和字串的底層位元組陣列是共享的。但是事實不是這樣的,底層位元組陣列會被拷貝。如果內容很大,那麼轉換操作是需要一定成本的。
那為什麼需要拷貝呢?因為位元組切片的底層陣列內容是可以修改的,而字串的底層位元組陣列是隻讀的,如果共享了,就會導致字串的只讀屬性不再成立。
5.修改字串
無法直接修改每一個字元元素
angel := "Heros never die" angleBytes := []byte(angel) for i := 5; i <= 10; i++ { angleBytes[i] = ' ' } fmt.Println(string(angleBytes))
字串不可變有很多好處,如天生執行緒安全,大家使用的都是隻讀物件,無須加鎖;再者,方便記憶體共享,而不必使用寫時複製(Copy On Write)等技術;字串 hash 值也只需要製作一份。所以說,程式碼中實際修改的是 []byte,[]byte 在 Go 語言中是可變的,本身就是一個切片。在完成了對 []byte 操作後,在第 9 行,使用 string() 將 []byte 轉為字串時,重新創造了一個新的字串。
三、結構體
1.結構體型別的定義
結構體和其它高階語言裡的「類」比較類似。下面我們使用結構體語法來定義一個「圓」型
type Circle struct { x int y int Radius int }
Circle 結構體內部有三個變數,分別是圓心的座標以及半徑。特別需要注意是結構體內部變數的大小寫,首字母大寫是公開變數,首字母小寫是內部變數,分別相當於類成員變數的 Public 和 Private 類別。內部變數只有屬於同一個 package(簡單理解就是同一個目錄)的程式碼才能直接訪問。
2.建立
func main() { var c Circle = Circle { x: 100, y: 100, Radius: 50,// 注意這裡的逗號不能少 } fmt.Printf("%+v\n", c) } ---------- {x:100 y:100 Radius:50}
可以只指定部分欄位的初值,甚至可以一個欄位都不指定,那些沒有指定初值的欄位會自動初始化為相應型別的「零值」。
func main() { var c1 Circle = Circle { Radius: 50, } var c2 Circle = Circle {} fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) } ---------- {x:0 y:0 Radius:50} {x:0 y:0 Radius:0}
結構體的第二種建立形式是不指定欄位名稱來順序欄位初始化,需要顯示提供所有欄位的初值,一個都不能少。這種形式稱之為「順序形式」。 var c Circle = Circle {100, 100, 50}
結構體變數建立的第三種形式,使用全域性的 new() 函式來建立一個「零值」結構體,所有的欄位都被初始化為相應型別的零值。 var c *Circle = new(Circle)
注意 new() 函式返回的是指標型別。
第四種建立形式,這種形式也是零值初始化,就數它看起來最不雅觀。 var c Circle
3.零值結構體和 nil 結構體
nil 結構體是指結構體指標變數沒有指向一個實際存在的記憶體。這樣的指標變數只會佔用 1 個指標的儲存空間,也就是一個機器字的記憶體大小。
var c *Circle = nil
而零值結構體是會實實在在佔用記憶體空間的,只不過每個欄位都是零值。如果結構體裡面欄位非常多,那麼這個記憶體空間佔用肯定也會很大。
4.結構體的拷貝
func main() { var c1 Circle = Circle {Radius: 50} var c2 Circle = c1 fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) c1.Radius = 100 fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) var c3 *Circle = &Circle {Radius: 50} var c4 *Circle = c3 fmt.Printf("%+v\n", c3) fmt.Printf("%+v\n", c4) c3.Radius = 100 fmt.Printf("%+v\n", c3) fmt.Printf("%+v\n", c4) } --------------- {x:0 y:0 Radius:50} {x:0 y:0 Radius:50} {x:0 y:0 Radius:100} {x:0 y:0 Radius:50} &{x:0 y:0 Radius:50} &{x:0 y:0 Radius:50} &{x:0 y:0 Radius:100} &{x:0 y:0 Radius:100}
5.無處不在的結構體
通過觀察 Go 語言的底層原始碼,可以發現所有的 Go 語言內建的高階資料結構都是由結構體來完成的。
切片頭的結構體形式如下,它在 64 位機器上將會佔用 24 個位元組
type slice struct { array unsafe.Pointer// 底層陣列的地址 len int // 長度 cap int // 容量 }
字串頭的結構體形式,它在 64 位機器上將會佔用 16 個位元組
type string struct { array unsafe.Pointer // 底層陣列的地址 len int }
字典頭的結構體形式
type hmap struct { count int ... buckets unsafe.Pointer// hash桶地址 ... }
6.結構體的引數傳遞
函式呼叫時引數傳遞結構體變數,Go 語言支援值傳遞,也支援指標傳遞。值傳遞涉及到結構體欄位的淺拷貝,指標傳遞會共享結構體內容,只會拷貝指標地址,規則上和賦值是等價的。下面我們使用兩種傳參方式來編寫擴大圓半徑的函式。
package main import "fmt" type Circle struct { x int y int Radius int } func expandByValue(c Circle) { c.Radius *= 2 } func expandByPointer(c *Circle) { c.Radius *= 2 } func main() { var c = Circle {Radius: 50} expandByValue(c) fmt.Println(c) expandByPointer(&c) fmt.Println(c) } --------- {0 0 50} {0 0 100}
從上面的輸出中可以看到通過值傳遞,在函式裡面修改結構體的狀態不會影響到原有結構體的狀態,函式內部的邏輯並沒有產生任何效果。通過指標傳遞就不一樣。
7.結構體方法
Go 語言不是面向物件的語言,它裡面不存在類的概念,結構體正是類的替代品。類可以附加很多成員方法,結構體也可以。
package main import "fmt" import "math" type Circle struct { x int y int Radius int } // 面積 func (c Circle) Area() float64 { return math.Pi * float64(c.Radius) * float64(c.Radius) } // 周長 func (c Circle) Circumference() float64 { return 2 * math.Pi * float64(c.Radius) } func main() { var c = Circle {Radius: 50} fmt.Println(c.Area(), c.Circumference()) // 指標變數呼叫方法形式上是一樣的 var pc = &c fmt.Println(pc.Area(), pc.Circumference()) } ----------- 7853.981633974483 314.1592653589793 7853.981633974483 314.1592653589793
Go 語言不喜歡型別的隱式轉換,所以需要將整形顯示轉換成浮點型,不是很好看,不過這就是 Go 語言的基本規則,顯式的程式碼可能不夠簡潔,但是易於理解。
Go 語言的結構體方法裡面沒有 self 和 this 這樣的關鍵字來指代當前的物件,它是使用者自己定義的變數名稱,通常我們都使用單個字母來表示。
Go 語言的方法名稱也分首字母大小寫,它的許可權規則和欄位一樣,首字母大寫就是公開方法,首字母小寫就是內部方法,只能歸屬於同一個包的程式碼才可以訪問內部方法。
結構體的值型別和指標型別訪問內部欄位和方法在形式上是一樣的。這點不同於 C++ 語言,在 C++ 語言裡,值訪問使用句點 . 操作符,而指標訪問需要使用箭頭 -> 操作符。
8.關於GO如何實現面對物件的繼承、多型,是個有趣的話題。參考 go是面嚮物件語言嗎?