《Go 語言程式設計》讀書筆記 (一)基礎類型和複合類型

  • 2019 年 12 月 30 日
  • 筆記

前言

最近在讀《Go 語言程式設計》這本書想通過看書鞏固一下自己的基礎知識,把已經積累的點通過看書學習再編織成一個網,這樣看別人寫的優秀程式碼時才能更好理解。當初工作中需要使用 Go開發項目時看了網上不少教程,比如 uknown 翻譯的《the way to go》看完基本上每次使用遇到不會的時候還會再去翻閱,這次把書中的重點還有一些平時容易忽視的Go語言中各種內部結構(類型、函數、方法)的一些行為整理成讀書筆記。

因為《Go 語言程式設計》不是針對初學者的,所以我只摘選最重要的部分並適當補充和調換描述順序力求用最少的篇幅描述清楚每個知識點。

《Go 語言程式設計》在線閱讀地址:https://yar999.gitbooks.io/go…

如果剛接觸 Go建議先去讀 《the-way-to-go》在線閱讀地址:https://github.com/unknwon/th…

命名:

  • 函數名、變數名、常量名、類型名、包名等所有的命名,都遵循一個簡單的命名規則:一個名字必須以一個字母(Unicode字母)或下劃線開頭,後面可以跟任意數量的字母、數字或下劃線。
  • 大寫字母和小寫字母是不同的:heapSort和Heapsort是兩個不同的名字。
  • 關鍵字不可用於命名
  • break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var
  • 推薦駝峰式命名
  • 名字的開頭字母的大小寫決定了名字在包外的可見性。如果一個名字是大寫字母開頭的,那麼它可以被外部的包訪問,包本身的名字一般總是用小寫字母。

聲明:

  • Go語言主要有四種類型的聲明語句:var、const、type和func,分別對應變數、常量、類型和函數。

變數:

  • var聲明語句可以創建一個特定類型的變數,然後給變數附加一個名字,並且設置變數的初始值。變數聲明的一般語法如下: var 變數名字 類型 = 表達式 其中「類型」或「= 表達式」兩個部分可以省略其中的一個。如果省略的是類型資訊,那麼將 根據初始化表達式來推導變數的類型資訊。如果初始化表達式被省略,那麼將用零值初始化該變數。數值類型變數對應的零值是0,布爾類型變數對應的零值是false,字元串類型對應的零值是空字元串,介面或引用類型(包括slice、map、chan和函數)變數對應的零值是nil。數組或結構體等聚合類型對應的零值是每個元素或欄位都是對應該類型的零值。 零值初始化機制可以確保每個聲明的變數總是有一個良好定義的值,因此在Go語言中不存在未初始化的變數。這個特性可以簡化很多程式碼,而且可以在沒有增加額外工作的前提下確保邊界條件下的合理行為。例如: var s string fmt.Println(s) // ""

字元串:

  • 文本字元串通常被解釋為採用UTF8編碼的Unicode碼點(rune)序列。
  • 內置的len函數可以返回一個字元串中的位元組數目(不是rune字元數目),索引操作s[i]返回第i個位元組的位元組值,i必須滿足0 ≤ i< len(s)條件約束。
  • 字元串的值是不可變的:一個字元串包含的位元組序列永遠不會被改變,當然我們也可以給一個字元串變數分配一個新字元串值。可以像下面這樣將一個字元串追加到另一個字元串: s := "left foot" t := s s += ", right foot" 這並不會導致原始的字元串值被改變,但是變數s將因為+=語句持有一個新的字元串值,但是t依然是包含原先的字元串值。 因為字元串是不可修改的,因此嘗試修改字元串內部數據的操作也是被禁止的: s[0] = 'L' // compile error: cannot assign to s[0]
  • 每一個UTF8字元解碼,不管是顯式地調用utf8.DecodeRuneInString解碼或是在range循環中隱式地解碼,如果遇到一個錯誤的UTF8編碼輸入,將生成一個特別的Unicode字元'uFFFD',在印刷中這個符號通常是一個黑色六角或鑽石形狀,裡面包含一個白色的問號"�"。當程式遇到這樣的一個字元,通常是一個危險訊號,說明輸入並不是一個完美沒有錯誤的UTF8字元串。
  • 字元串的各種轉換: string接受到[]rune的類型轉換,可以將一個UTF8編碼的字元串解碼為Unicode字元序列: // "program" in Japanese katakana s := "プログラム" fmt.Printf("% xn", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0" r := []rune(s) fmt.Printf("%xn", r) // "[30d7 30ed 30b0 30e9 30e0]" (在第一個Printf中的% x參數用於在每個十六進位數字前插入一個空格。) 如果是將一個[]rune類型的Unicode字元slice或數組轉為string,則對它們進行UTF8編碼: fmt.Println(string(r)) // "プログラム" 將一個整數轉型為字元串意思是生成只包含對應Unicode碼點字元的UTF8字元串: fmt.Println(string(65)) // "A", not "65" fmt.Println(string(0x4eac)) // "京" 如果對應碼點的字元是無效的,則用'uFFFD'無效字元作為替換: fmt.Println(string(1234567)) // "�"

複合數據類型:

  • 基本數據類型,它們可以用於構建程式中數據結構,是Go語言的世界的原子。以不同的方式組合基本類型可以構造出複合數據類型。我們主要討論四種類型——數組、slice、map和結構體,數組和結構體都是有固定記憶體大小的數據結構。相比之下,slice和map則是動態的數據結構,它們將根據需要動態增長。

數組:

  • 數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必須是常量表達式,因為數組的長度需要在編譯階段確定。 q := [3]int{1, 2, 3} q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int

Slice:

  • 長度對應slice中元素的數目;長度不能超過容量,容量一般是從slice的開始位置到底層數據的結尾位置。內置的len和cap函數分別返回slice的長度和容量。
  • x[m:n]切片操作對於字元串則生成一個新字元串,如果x是[]byte的話則生成一個新的[]byte。
  • slice並不是一個純粹的引用類型,它實際上是一個類似下面結構體的聚合類型: type IntSlice struct { ptr *int len, cap int }

Map:

  • 在Go語言中,一個map就是一個哈希表的引用,map類型可以寫為map[K]V,其中K和V分別對應key和value。map中所有的key都有相同的類型,所有的value也有著相同的類型,但是key和value之間可以是不同的數據類型。
  • map中的元素並不是一個變數,因此我們不能對map的元素進行取址操作: _ = &ages["bob"] // compile error: cannot take address of map element 禁止對map元素取址的原因是map可能隨著元素數量的增長而重新分配更大的記憶體空間,從而可能導致之前的地址無效。
  • map上的大部分操作,包括查找、刪除、len和range循環都可以安全工作在nil值的map上,它們的行為和一個空的map類似。但是向一個nil值的map存入元素將導致一個panic異常: ages["carol"] = 21 // panic: assignment to entry in nil map 在向map存數據前必須先創建map。
  • 和slice一樣,map之間也不能進行相等比較;唯一的例外是和nil進行比較。要判斷兩個map是否包含相同的key和value,我們必須通過一個循環實現。

結構體:

  • 下面兩個語句聲明了一個叫Employee的命名的結構體類型,並且聲明了一個Employee類型的變數dilbert: type Employee struct { ID int Name string Address string DoB time.Time Position string Salary int ManagerID int } var dilbert Employee dilbert結構體變數的成員可以通過點操作符訪問,比如dilbert.Name和dilbert.DoB。因為dilbert是一個變數,它所有的成員也同樣是變數,我們可以直接對每個成員賦值: dilbert.Salary -= 5000 // demoted, for writing too few lines of code 或者是對成員取地址,然後通過指針訪問: position := &dilbert.Position *position = "Senior " + *position // promoted, for outsourcing to Elbonia
  • 如果結構體成員名字是以大寫字母開頭的,那麼該成員就是導出的;這是Go語言導出規則決定的。一個結構體可能同時包含導出和未導出的成員。未導出的成員只能在包內部訪問,在外部包不可訪問。
  • 結構體類型的零值中每個成員其類型的是零值。通常會將零值作為最合理的默認值。例如,對於bytes.Buffer類型,結構體初始值就是一個隨時可用的空快取,還有sync.Mutex的零值也是有效的未鎖定狀態。有時候這種零值可用的特性是自然獲得的,但是也有些類型需要一些額外的工作。
  • 因為結構體通常通過指針處理,可以用下面的寫法來創建並初始化一個結構體變數,並返回結構體的地址: pp := &Point{1, 2}
  • Go語言有一個特性讓我們只聲明一個成員對應的數據類型而不指名成員的名字;這類成員就叫匿名成員。匿名成員的數據類型必須是命名的類型或指向一個命名的類型的指針。下面的程式碼中,Circle和Wheel各自都有一個匿名成員。我們可以說Point類型被嵌入到了Circle結構體,同時Circle類型被嵌入到了Wheel結構體。 type Circle struct { Point Radius int } type Wheel struct { Circle Spokes int } 得益於匿名嵌入的特性,我們可以直接訪問葉子屬性而不需要給出完整的路徑: var w Wheel w.X = 8 // equivalent to w.Circle.Point.X = 8 w.Y = 8 // equivalent to w.Circle.Point.Y = 8 w.Radius = 5 // equivalent to w.Circle.Radius = 5 w.Spokes = 20
  • 外層的結構體不僅僅是獲得了匿名成員類型的所有成員,而且也獲得了該類型導出的全部的方法。這個機制可以用於將一個有簡單行為的對象組合成有複雜行為的對象。