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

KevinYan發表於2019-12-15

前言

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

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

《Go 語言程式設計》線上閱讀地址:https://yar999.gitbooks.io/gopl-zh/content...

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

(如Summer在評論裡所說 LearnKu 裡有《the-way-to-go》這本書的映象,《Go 入門指南》閱讀體驗和載入速度都要更好一些 )

命名:

  • 函式名、變數名、常量名、型別名、包名等所有的命名,都遵循一個簡單的命名規則:一個名字必須以一個字母(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("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
    r := []rune(s)
    fmt.Printf("%x\n", 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
  • 外層的結構體不僅僅是獲得了匿名成員型別的所有成員,而且也獲得了該型別匯出的全部的方法。這個機制可以用於將一個有簡單行為的物件組合成有複雜行為的物件。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
公眾號:網管叨bi叨 | Golang、Laravel、Docker、K8s等學習經驗分享

相關文章