《effective Go》讀後記錄

分類:IT技術 時間:2017-09-30

一個在線的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 int

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

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

type ByteSize float64

const (
    _          = 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 main
import "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 for
for 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()
three
two
one

關於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) *Type
func 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
} r
eturn
} a
rray := [...]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} //創建一個切片,長度與容量都是5
newSlice := 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 main
import "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:50
r'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:50
r '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 main

import (
    "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:50
after bigger 2 multiple, area :200
geometry 's area:28.274333882308138
after bigger 2 multiple, area :113.09733552923255

類型轉換

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

    func main() {
    var myInt int32 =5
    var myFloat float64 = 6
    fmt.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 int32
type 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)
        }
    }
}

(完)


Tags: 註釋 程序 駝峰 一個 命名 格式化

文章來源:


ads
ads

相關文章
ads

相關文章

ad