《effective Go》讀後記錄

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

一個線上的Go編譯器

如果還沒來得及安裝Go環境,想體驗一下Go語言,可以在Go線上編譯器 上運行Go程式。

格式化

讓所有人都遵循一樣的編碼風格是一種理想,現在Go語言通過gofmt程式,讓機器來處理大部分的格式化問題。gofmt程式是go標準庫提供的一段程式,可以嘗試運行它,它會按照標準風格縮排,對齊,保留注釋,它預設使用定位字元進行縮排。Go標準庫的所有代碼都經過gofmt程式格式化的。

注釋

Go注釋支援C風格的塊注釋/* */和C++風格的行注釋//。塊注釋主要用作包的注釋。Go官方提倡每個包都應包含一段包注釋,即放置在包子句前的一個塊注釋。對於有多個檔案的包,包注釋只需要出現在其中一個檔案即可。

godoc 既是一個程式,又是一個 Web 服務器,它對 Go 的源碼進行處理,並提取包中的文檔內容。 出現在頂級聲明之前,且與該聲明之間沒有空行的注釋,將與該聲明一起被提取出來,作為該條目的說明文檔。

命名

  • Go語言的命名會影響語義:某個名稱在包外是否可見,取決於其首個字元是否為大寫字母。
  • 包:應當以小寫單個單詞來命名,且不應使用底線或駝峰記法。
  • 包名:應為其源碼目錄的基本名稱,例如在 src/pkg/encoding/base64 中的包應作為"encoding/base64" 匯入,其包名應為 base64
  • 擷取器:若有個名為 owner (小寫,未匯出) 的欄位,其擷取器應當名為 Owner(大寫,可匯出) 而非 GetOwner。若要提供設定器方法,可以選擇SetOwner。
  • 介面:只包含一個方法的介面應當以該方法的名稱加上 - er 尾碼來命名
  • 駝峰記法:Go 中約定使用駝峰記法 MixedCaps 或 mixedCaps

分號

  • Go的詞法分析器會用簡單的規則來自動插入分號
  • 如果在一行中寫多個語句,需要用分號隔開
  • 控制結構的左大括弧不能放在下一行,因為根據詞法分析器的規則,會在大括弧前加入一個分號,造成錯誤

初始化

常量必須在定義的時候就進行初始化。常量只能是數字、字元、字串、布爾值等基本類型,定義它們的運算式必須是在編譯期就可以求值的類型。使用const來定義一個常量:

  const LENGTH int = 10  const WIDTH int = 5 

在Go中,枚舉常量使用iota來建立,iota是一個自增長的值:

type AudioOutput intconst (    OutMute AudioOutput = iota // 0    OutMono                    // 1    OutStereo                  // 2    _    _    OutSurround                // 5)

iota總是用於increment,但它也可以用於運算式,在《effective Go》展示了一個定義數量級的表示:

type ByteSize float64const (    _          = iota                  // 使用_來忽略iota=0    KB ByteSize = 1 << (10 * iota) // 1 << (10*1)    MB                                  // 1 << (10*2)    GB                                  // 1 << (10*3)    TB                                  // 1 << (10*4)    PB                                  // 1 << (10*5)    EB                                  // 1 << (10*6)    ZB                                  // 1 << (10*7)    YB                                  // 1 << (10*8))

源檔案可以定義無參數init函數,該函數在真正執行函數邏輯之前被自動調用,下面的程式簡單說明這一點:

package mainimport "fmt"func init() {    fmt.Print("執行init函數0\n")}func init() {    fmt.Print("執行init函數1\n")}func init() {    fmt.Print("執行init函數2\n")}func main() {    fmt.Print("執行main函數\n")}//output :執行init函數0執行init函數1執行init函數2執行main函數

可以看到,在執行main函數中的邏輯前,init函數會先被調用,而且同一個源檔案中可以定義多個init函數。init函數通常被用在程式真正執行之前對變數、程式狀態進行校正。它的執行機制是這樣的:

  • 該包中所有的變數都被初始化器求值後,init才會被調用
  • 之後在所有已匯入的包都被初始化之後,init才會被調用

控制結構

Go使用更加通用的for來代替do與while迴圈,for的三種形式為:

// Like a C forfor init ; condition;post {  }//Like a C while for condition{  }//Like a C for(;;)for {}

對於數組、切片、字串、map,或者從通道讀取訊息,可以使用range子句

for key ,value := range oldMap {    newMap[key] = value}

Go的switch要更加靈活通用,當switch後面沒有運算式的時候,它將匹配ture,這也意味著if-else-if-else鏈可以使用switch來實現:

func unhex(c byte) byte {    switch { //switch將匹配true    case '0' <= c && c <= '9':        return c - '0'    case 'a' <= c && c <= 'f':        return c - 'a' + 10    case 'A' <= c && c <= 'F':        return c - 'A' + 10    }    return 0}

函數

Go的函數可以進行多值返回。在C語言中經常有這種笨拙的用法:函數通過傳回值來告知函數的執行情況,例如返回0代表無異常,返回-1表示EOF等,而通過指標實參來傳遞資料給外部。現在使用Go函數的多值返回可以解決解決這個問題。下面是Go標準庫中開啟檔案的File.Write的簽名:

func (file *File) Write(b []byte) (n int, err error)

Write函數返回寫入的位元組數以及一個錯誤。如果正確寫入了,則errnil,否則,err為一個非nilerror錯誤值,這在Go中是一種常見的編碼風格。

Go函數的傳回值可以被命名。Go的傳回值在函數體內可以作為常規的變數來使用,稱為結果“形參”結果“形參”在函數開始執行時被初始化與其類型相應的零值。如果函數執行了不帶參數的return,則把結果形參的當前值返回:

func abs(i int) (result int){    if i < 0{        result = -i   //傳回值result可以直接當成常規變數使用    }    return }

這樣做的好處是函數的簽名即為文檔,傳回值的含義也寫到了函數簽名中,提高了代碼的可讀性。

Go提供defer語句用於順延強制函數。defer語句修飾的函數,在外層函數結束之前被調用。可以這樣來使用defer語句:

func printStr (a string){    fmt.Print(a);}func main() {    defer printStr("one\n")    defer printStr("two\n")    defer printStr("three\n")    fmt.Print("main()\n")}//output :main()threetwoone

關於defer語句

  • 適用於關閉開啟的檔案,避免多個返迴路徑都需要去關閉檔案。
  • 被延遲執行的函數的實參,才延遲執行時就會求值,而不是在調用執行時才求值。
  • 被延遲的函數按照後進先出(LIFO)的順序執行。
  • defer語句是在函數層級的,即使把它寫在大括弧(塊)中,也只會在調用函數結束時才調用被延遲執行的函數。

使用defer語句時還有一些細節需要注意。下面這段代碼:

func main() {    fmt.Print(test())}func test() (r int) {    defer func() {        r = 1        return    }()    r = 2    return 3}//output:1

輸出並不是3,而是1.原因是return的操作實際包括了:

r = 0 //結果“形參”在函數開始執行時被初始化為零值r = 2 r = 1 //defer語句執行return r

記憶體配置

Go提供了兩種分配原語newmake

func new(Type) *Typefunc make(t Type, size ...IntegerType) Type

new(T)用於分配記憶體,它返回一個指標,指向新分配的,類型為T的零值,通過new來申請的記憶體都會被置零。這意味著如果設計了某種資料結構,那麼每種類型的零值就不必進一步初始化了。

make(T,args)的目的不同於new(T),它只用於建立切片(slice)、映射(map)、通道(channel),這三種類型本質上與引用資料類型,它們在使用前必須初始化。make傳回型別為一個類型為T的已初始化的值,而非*T

下面是newmake的對比:

var p *[]int = new([]int) // 分配切片結構;*p == nil;基本沒用var v []int = make([]int, 100) // 切片 v 現在引用了一個具有 100 個 int 元素的新數組// 沒必要的複雜:var p *[]int = new([]int)*p = make([]int, 100, 100)// 習慣用法:v := make([]int, 100)

數組

Go的數組與C語言的數組有很大的區別:

  • 數組是值,把數組傳遞給函數,函數會得到該數組的一個副本,而不是指標。
  • 數組的大小是類型的一部分。[10]int[20]int是兩種類型。

如果想要像C語言那樣傳遞數組指標,需要這樣做:

func Sum(a *[3]float64) (sum float64) {for _, v := range *a {sum += v} return} array := [...]float64{7.0, 8.5, 9.1}x := Sum(&array) // 注意顯式的取址操作

但在Go中通常不會這樣做,而是通過切片來實現引用的傳遞。切片儲存了對底層數組的引用,若你將某個切片賦予另一個切片,它們會引用同一個數組。

切片

切片是一個很小的對象,它對底層數組進行了抽象,並提供相應的操作方法,切片包含3個欄位,其的內部實現為:

可以通過一些方式來定義切片:

var slice0 []type   //通過聲明一個未指定大小的數組來定義切片var slice1 []type = make([]type, len) //通過make來建立切片,長度與容量都是5個元素make([]T, length, capacity) //可以通過make來指定它的容量

聲明的時候,只要在[]運算子裡指定了一個值,那麼建立的就是數組而不是切片,只有不指定值的時候,才會建立切片。
切片之所以稱為切片,是因為建立一個新的切片就是把底層數組切出一部分,例如代碼:

slice := [] int {10,20,30,40,50} //建立一個切片,長度與容量都是5newSlice := slice[1:3] //建立一個新切片,其長度為5,容量為4

對底層數組容量是k的新切片slice[i,j]來說,長度是j-i,容量是k-i,建立的新切片內部實現為:

由於兩個切片共用一部分的底層數組,所以修改newSlice的第2個元素,也將同樣修改了slice的第三個元素
可以使用append來增長切片的長度,這有兩種情況:

  • 當切片的可用容量足夠時,append函數會增加切片的長度,而不會改變容量
  • 當切片的可用容量不足時,append函數會增加切片的容量,增加的策略是:切片容量小於1000時,總是成倍地增加容量;一旦元素個數超過1000個,容量增加因子為1.25,也就是每次會增加25%。

append函數造成切片容量拓展時,該切片將擁有一個全新的底層數組。

映射

映射與切片一樣,也是參考型別。如果通過一個不存在的key來擷取value,將返回與該映射中項的類型對應的零值:

    var map1  map[string] int    map1 = make(map[string]int ,10)    map1["one"]=1    map1["two"]=2    fmt.Print(map1["three"])//output:     0

如果map1["three"]value剛好是0,該怎麼區分呢?可以採用多重賦值的形式來分辨這種情況:

    i, ret := map1["three"]    if ret == true{        fmt.Print("map1[\"three\"]存在,值為:", i)        } else {        fmt.Print("map1[\"three\"] 不存在\n")    }

或者這樣寫更好一些,《effective Go》稱為the “comma ok” idiom ,逗號OK慣用法

    if i, ret := map1["three"] ;ret {        fmt.Print("map1[\"three\"]存在,值為:", i)    } else {        fmt.Print("map1[\"three\"] 不存在\n")    }

如果僅是需要判斷某個key是否存在,可以用空白標識符_來代替value

    if _, ret := map1["three"] ;ret {        fmt.Print("map1[\"three\"]存在\n")    } else {        fmt.Print("map1[\"three\"]不存在,值為:")    }

使用內建函數delete函數來刪除索引值對,即使對應的鍵不在該映射中,delete操作也是安全的

方法

在函數的一節中,我們已經看到了write函數的聲明為:

func (file *File) Write(b []byte) (n int, err error)

我們可以抽象出Go中函數的結構為:

func  [(p mytype)]  funcname([pram type]) [(parm type)] {//body}

其中,函數的(p mytype)為可選部分,具備此部分的函數稱為方法(method),這部分稱為接收者(receiver)。我們可以為任何已命名的類型,包括自己定義的結構體類型,定義方法。通過receiver,把方法綁定到類型上。下面是一個樣本:

package mainimport "fmt"//定義一個矩形類型type rect struct {    width ,height int}//這個方法擴大矩形邊長為multiple倍//這個方法的reciever為*rect//表示這是定義在rect結構體上的方法func (r *rect) bigger(multiple int){    r.height *=multiple    r.height *=multiple}//方法的reciever可以為結構體類型//也可以為結構體指標類型//區別在於當reciever為類型指標時//可以在該方法內部修改結構體成員func (r rect) area() int{    return r.width*r.height}func main(){    r  := rect{width:10,height:5}    fmt.Print("r 's area:",r.area(),"\n")    r.bigger(10)    fmt.Print("r's area:",r.area())}//output:r 's area:50r's area:5000

以指標或值作為reciever的區別在於::

  • 指標可以修改接收者
  • 值方法可通過指標和值調用,而指標方法只能通過指標來調用

值方法可以通過指標和值調用,所以下面語句是合法的:

func main(){    r  := rect{width:10,height:5}        //通過指標調用    fmt.Print("r 's area:",(&r).area(),"\n")        //通過值調用    fmt.Print("r 's area:",r.area(),"\n")}//output:r 's area:50r 's area:50

而對於指標方法只能通過指標來調用,你可能會感到疑惑,因為下面的語句也是合法的:

func main(){    r  := rect{width:10,height:5}    fmt.Print("r 's area:",r.area(),"\n")        //通過值來調用指標方法(為什麼合法?)    r.bigger(10)    fmt.Print("r's area:",r.area())}//output:

其實是這樣的:如果值是可以定址的,那麼Go會自動插入取址操作符來對付一般的通過值調用的指標方法。在這個例子中,r是可定址的,因此r.Bigger(10)將被編譯器改寫為(&r).Bigger

另外,方法也可以"轉換"為函數,這一點便不在這裡詳談。

介面

通過方法與介面,Go語言定義了一種與java/C++等OOP語言截然不同的“繼承”的形態。通過實現介面定義的方法,便可將reciever的類型變數賦值給介面類型變數,通過介面類型變數來調用到reciever類型的方法,用C++來類比,就是通過父類指標來調用到了衍生類別的成員函數(不過Go沒有這些概念)。下面是一個樣本:

package mainimport (    "fmt"    "math")//定義了一個介面geometry表示幾何類型type geometry interface {    area() float64    bigger(float64)}//矩形和圓形要實現這介面的兩個方法type rect struct {    width, height float64}type circle struct {    radius float64}//在Go中,實現介面,只需要實現該介面定義的所有方法即可//矩形的介面方法實現func (r *rect) bigger(multiple float64) {    r.height *= multiple    r.height *= multiple}func (r *rect) area() float64 {    return r.width * r.height}//圓形的介面方法實現func (c *circle) bigger(multiple float64){    c.radius *= multiple}func (c *circle) area() float64 {    return math.Pi * c.radius * c.radius}//可以把rect和circle類型的變數作為實參//傳遞給geometry介面類型的變數func measure (g geometry){    fmt.Print("geometry 's area:",g.area(),"\n")    g.bigger(2)    fmt.Print("after bigger 2 multiple, area :",g.area(),"\n")}func main() {    r := rect{width: 10, height: 5}    c := circle{radius:3}    measure(&r)    measure(&c)}//output:geometry 's area:50after bigger 2 multiple, area :200geometry 's area:28.274333882308138after bigger 2 multiple, area :113.09733552923255

類型轉換

  • 字面量的值,Go編譯器會進行隱式轉換

    func main() {var myInt int32 =5var myFloat float64 = 6fmt.Print(myInt,"\n")fmt.Print(myFloat)}

    這裡的6為整數型別的字面值常量 Integer literals.。它賦值給了float64類型變數,編譯器進行了隱式類型轉換。

  • 底層類型不同的變數,需要顯式類型轉換:

func main() {    var myInt int32 =5    //var myFloat float64 = myInt //error    var myFloat float64 = float64(myInt) //需要顯式轉換    fmt.Print(myInt,"\n")    fmt.Print(myFloat)}

這裡還要區分靜態類型底層類型

type IntA int32type IntB int32 func main() {    var a IntA =1    //var b IntB  = a //error    var b IntB  = IntB(a)        fmt.Print(a,"\n")    fmt.Print(b)}

這裡IntA為變數a的靜態類型,而int32為變數a的底層類型。即使兩個類型的底層類型相同,在相互賦值時還是需要強制類型轉換的。

介面類型變數的類型轉換,有兩種情況:

  1. 普通類型向介面類型的轉換:隱式進行
  2. 介面類型向普通類型的轉換:需要類型斷言

根據Go 官方文檔 所說,所有的類型,都實現了空介面interface{},所以普通類型都可以向interface{}進行類型轉換:

func main() {    var x interface{} = "hello" // 字串常量->interface{}    var y interface{}  = []byte{'w','o','r','l','d'} //[]byte ->interface{}    fmt.Print(x," ")    fmt.Printf("%s",y)}

而介面類型向普通類型的轉換,則需要由Comma-ok斷言switch測試來進行了。

Comma-ok斷言

文法: value,ok := element.(T)

element必須為ingerface類型,宣告失敗,ok為false,否則為true,下面是常式:

func main() {    var vars []interface{} = make([]interface{},5)    vars[0] = "one"    vars[1] = "two"    vars[2] = "three"    vars[3] = 10    vars[4] = []byte{'a', 'b', 'c'}    for index, element := range vars {        if value, ok := element.(int); ok {            fmt.Printf("vars[%d] type is int,value is %d \n",index,value)        }else if value,ok := element.(string);ok{            fmt.Printf("vars[%d] type is string,value is %s \n",index,value)        }else if value,ok := element.([]byte);ok{            fmt.Printf("vars[%d] type is []byte,value is %s \n",index,value)        }    }}//output:vars[0] type is string,value is one vars[1] type is string,value is two vars[2] type is string,value is three vars[3] type is int,value is 10 vars[4] type is []byte,value is abc 

Comma-ok斷言也可以這樣使用:

value := element.(T)

但一旦宣告失敗將產生執行階段錯誤,不推薦使用。

switch測試

switch測試只能在switch語句中使用。將上面的常式改為switch測試:

func main() {    var vars []interface{} = make([]interface{}, 5)    vars[0] = "one"    vars[1] = "two"    vars[2] = "three"    vars[3] = 10    vars[4] = []byte{'a', 'b', 'c'}    for index, element := range vars {        switch value := element.(type) {        case int:            fmt.Printf("vars[%d] type is int,value is %d \n", index, value)        case string:            fmt.Printf("vars[%d] type is string,value is %s \n", index, value)        case []byte:            fmt.Printf("vars[%d] type is []byte,value is %s \n", index, value)        }    }}

(完)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.