Golang高效實踐之泛談篇

我是碼客發表於2019-07-29

前言

我部落格之前的Golang高效實踐系列部落格中已經系統的介紹了Golang的一些高效實踐建議,例如:《Golang高效實踐之interface、reflection、json實踐》、《Golang 高效實踐之defer、panic、recover實踐》、《Golang 高效實踐之併發實踐context篇》、《Golang 高效實踐之併發實踐channel篇》,本文將介紹一些零散的Golang高效實踐建議,雖然瑣碎但是比較重要。

建議

1.程式碼格式go fmt工具,開發不用過多關注。

2.支援塊註釋和行註釋,一般包開頭用塊註釋說明,函式用行註釋說明,為了提高辨識度,函式註釋一般以函式名為開頭。例如:

// Compile parses a regular expression and returns, if successful,

// a Regexp that can be used to match against text.

func Compile(str string) (*Regexp, error) {

3.包名儘量簡潔有意義,一般是一個小寫單詞,不需要下劃線或者駝峰命名。不要用點號引進包,除非是為了簡化單元測試。

4.Go不提供getters和setters方法,使用者要自己實現。例如有一個欄位叫owner(小寫,非匯出變數),那麼getter方法應該命名為Owner而不是GetOwner。如果需要setter方法應該命名為SetOwner。例如:

owner := obj.Owner()

if owner != user {

    obj.SetOwner(user)

}

5.介面命名在方法名後加er,例如:Reader,Writer,Formatter,CloseNotifier等等。

6.變數命名用駝峰例如MixedCaps或者mixedCaps,不用下劃線。

7.Go和C一樣是用分號作為語句的結束標記,不同的是Go是詞法分析器自動加上去,不用程式設計師手動新增。詞法分析器新增分號的標記一是行末遇到int或者float64等關鍵字型別,或者出現下面的特殊字元:

break continue fallthrough return ++ -- ) }

所以:

if i < f() {

    g()

}

開括號‘{’要放在‘)’後面,否則詞法分析器會自動在‘)’末尾新增分到導致語法錯誤。所以不能像下面這樣寫:

if i < f()  // wrong!

{           // wrong!

    g()

}

8.非必須的else可以省略,例如:

if err := file.Chmod(0664); err != nil {

    log.Print(err)

    return err

}

9.宣告和重新賦值:

f, err := os.Open(name)

該語句宣告瞭f和err,緊接著:

d, err := f.Stat()

看著像宣告瞭d和err,但實際上是宣告瞭d,err是重新賦值。也就是說f.Stat用了上面已經存在的err,僅僅是重新給該err賦了一個新值。

所以 v:= declaration是宣告還是重新賦值取決於:

1.該宣告作用域已經存在一個已經宣告的v,那麼就是賦值(如果v已經在外面的作用域宣告,那麼這裡會重新生成一個新的變數v)

例如:

package main

 

import (

"errors"

"fmt"

)

 

func main() {

fmt.Println(declareTest())

}

 

func declareTest() (err error){

//declare a new variable err in if statement

if err := hello(); err != nil {

fmt.Println(err)

}

fmt.Println(err)

return

}

 

func hello() error {

return errors.New("hello world")

}

程式輸出:

hello world

<nil>

<nil> 

2.如果是賦值,那麼左邊至少要有一個宣告的新變數,否則會報語法錯誤。

10.for迴圈。Go的for迴圈和C很像,但是不支援while迴圈。有以下三種形式:

// Like a C for

for init; condition; post { }

 

// Like a C while

for condition { }

 

// Like a C for(;;)

for { }

也可以用for迴圈遍歷陣列、切片、字串、map、或者讀channel,例如:

for key, value := range oldMap {

    newMap[key] = value

}

for pos, char := range "iam中國人" {

fmt.Printf("character %#U start at byte position %d\n", char, pos)

}

程式輸出:

character U+0069 'i' start at byte position 0

character U+0061 'a' start at byte position 1

character U+006D 'm' start at byte position 2

character U+4E2D '' start at byte position 3

character U+56FD '' start at byte position 6

character U+4EBA '' start at byte position 9

// Reverse a,翻轉字元切片a

for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {

    a[i], a[j] = a[j], a[i]

} 

11.switch。Go的switch比C靈活,case的表示式不要求一定是常量甚至整數,例如:

func unhex(c byte) byte {

    switch {

    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

}

每個case不會自動順延到下一個case,如果需要順延需要手動fall through。

Switch用於型別判斷:

var t interface{}

t = functionOfSomeType()

switch t := t.(type) {

default:

    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has

case bool:

    fmt.Printf("boolean %t\n", t)             // t has type bool

case int:

    fmt.Printf("integer %d\n", t)             // t has type int

case *bool:

    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool

case *int:

    fmt.Printf("pointer to integer %d\n", *t) // t has type *int

}

12.命名函式返回值。Go函式的返回值可以像輸入函式一樣命名(當然也可以不命名),命名返回值在函式開始時就已經被初始化為型別的零值。如果函式執行return沒有帶返回值,那麼命名函式的當前值就會被返回。例如:

func ReadFull(r Reader, buf []byte) (n int, err error) {

    for len(buf) > 0 && err == nil {

        var nr int

        nr, err = r.Read(buf)

        n += nr

        buf = buf[nr:]

    }

    return

}

13.用defer釋放資源,比如關閉檔案、釋放鎖。這樣做有兩個好處,一是保證不會忘記釋放資源,另外是釋放的程式碼貼近申請的程式碼,更加清楚明瞭。更多defer特性請參考我的《Golang 高效實踐之defer、panic、recover實踐》博文。

14.new(T)分配一個*T型別,指向被賦予零值的一塊記憶體。例如:

type SyncedBuffer struct {

    lock    sync.Mutex

    buffer  bytes.Buffer

}

p := new(SyncedBuffer)  // type *SyncedBuffer,相當於p:= &SyncedBuffer{}

var v SyncedBuffer      // type  SyncedBuffer 

15.建構函式。Go並沒有像C++一樣為每個型別提供預設的建構函式。所以當new(T)分配的零值不能滿足我們要求時,我們需要一個初始化建構函式,一般命名為NewXXX,例如:

func NewFile(fd int, name string) *File {

    if fd < 0 {

        return nil

    }

    f := new(File)

    f.fd = fd

    f.name = name

    f.dirinfo = nil

    f.nepipe = 0

    return f

}

也可以這樣順序初始化成員:

return &File{fd, name, nil, 0}

還可以指定成員初始化:

return &File{fd: fd, name: name}

所以new(File)是等於&File{}

16.用make(T, args)建立切片、map、channel,返回已經初始化的(非零值)T型別(不是*T)。因為這三種資料結構必須在使用前完成初始化,例如切片的零值是nil,直接操作nil是會panic的。

make([]int, 10, 100)

分配一個length為10,capacity為100的切片。而new([]int)返回的值一個執行零值(nil)的切片指標。

下面的示例會清楚的區分new和make的差別:

var p *[]int = new([]int)       // allocates slice structure; *p == nil; rarely useful

var v  []int = make([]int, 100) // the slice v now refers to a new array of 100 ints

 

// Unnecessarily complex:

var p *[]int = new([]int)

*p = make([]int, 100, 100)

 

// Idiomatic:

v := make([]int, 100

記住只有切片、map和channel分配用到make,並且返回的不是指標。

 

17.陣列。和切片不同,陣列的大小是固定的,可以避免重新分配記憶體。和C語言陣列不同的時,Go的陣列是值,賦值時會引發陣列拷貝。當陣列作為引數傳遞給函式時,函式將會接受到陣列的拷貝,而不是陣列的指標。另外陣列的大小也是資料型別的一部分。也就是說[10]int 和 [20]int不是同一種型別。

但是值屬性本身是效率比較低的,如果不能拷貝傳遞可以傳遞陣列的指標,例如:

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)  // Note the explicit address-of operator

但是這樣不符合Go的程式設計習慣。這裡可以用切片避免拷貝傳遞。

 

18.切片。儘量用切片代替陣列。切片本質是陣列的引用,底層的資料結構還是陣列。所以當把切片A賦值給切片B時,A和B指向的是同一個底層陣列。當給函式傳遞切片時,相當於傳遞底層陣列的指標。因此切片通常是更高效和常用。

 

特別需要注意的是,切片的capacity也就是cap函式的返回值是底層陣列的最大長度,當切片超過了改值時將會觸發重新分配,底層的陣列將會擴容,並且將之前的值拷貝到新記憶體中。

 

func Append(slice, data []byte) []byte {

    l := len(slice)

    if l + len(data) > cap(slice) {  // reallocate

        // Allocate double what's needed, for future growth.

        newSlice := make([]byte, (l+len(data))*2)

        // The copy function is predeclared and works for any slice type.

        copy(newSlice, slice)

        slice = newSlice

    }

    slice = slice[0:l+len(data)]

    copy(slice[l:], data)

    return slice

}

Append函式最後要返回切片的值,因為切片(執行時持有指標,length和capacity的資料結構)本身是值傳遞的。

 

19.二維切片。Go的陣列和切片都是一維的,如果需要建立二維的陣列或者切片則需要定義陣列的陣列,或者切片的切片。例如:

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.

type LinesOfText [][]byte     // A slice of byte slices.

因為切片的長度是可變的,所以每個切片元素可以有不同的長度,所以有:

text := LinesOfText{

[]byte("Now is the time"),

[]byte("for all good gophers"),

[]byte("to bring some fun to the party."),

}

需要注意的是,make只會初始化一維,二維的切片需要我們手動初始化,例如:

// Allocate the top-level slice.

picture := make([][]uint8, YSize) // One row per unit of y.

// Loop over the rows, allocating the slice for each row.

for i := range picture {

  picture[i] = make([]uint8, XSize)

}

 

20.map。map的key可以是任意定義了相等操作的型別,例如int,float,complex,字串,指標,interface(只要是concrete type支援相等比較),結構體和陣列。切片不能作為map的key,因為切片的相等沒有定義。

map可以按k-v的方式列舉初始化,例如:

var timeZone = map[string]int{

    "UTC":  0*60*60,

    "EST": -5*60*60,

    "CST": -6*60*60,

    "MST": -7*60*60,

    "PST": -8*60*60,

} 

根據key索引value:

offset := timeZone["EST"]

當key不存在時,將會返回value對應的零值。例如:

tm := make(map[string]bool)

fmt.Println(tm["test"])

將會輸出false。那怎麼區分究竟是key不存在還是key存在且本身value就是零值呢?可以這樣利用“comma,ok”語法:

var seconds int

var ok bool

seconds, ok = timeZone[tz]

當key存在時ok為true,seconds為對應的value。否則ok為false,seconds為對應value的零值。 

可以用delete指定map的key刪除元素:

delete(timeZone, "PDT")  // Now on Standard Time

 

21.Go的格式輸出是C語言風格的,但是比C的printf更高階。所有格式輸出相關的函式在fmt包中,例如:fmt.Printf,fmt.Fprintf,fmt.Sprintf等等。例如:

fmt.Printf("Hello %d\n", 23)

fmt.Fprint(os.Stdout, "Hello ", 23, "\n")

fmt.Println("Hello", 23)

fmt.Println(fmt.Sprint("Hello ", 23))

%v輸出任意值:

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

程式結果:

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

 

又例如:

type T struct {

    a int

    b float64

    c string

}

t := &T{ 7, -2.35, "abc\tdef" }

fmt.Printf("%v\n", t)

fmt.Printf("%+v\n", t)

fmt.Printf("%#v\n", t)

fmt.Printf("%#v\n", timeZone)

程式輸出:

&{7 -2.35 abc   def}

&{a:7 b:-2.35 c:abc     def}

&main.T{a:7, b:-2.35, c:"abc\tdef"}

map[string]int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}

%T輸出型別:

fmt.Printf("%T\n", timeZone)

執行結果:

map[string]int

 

%s呼叫型別的String()方法輸出,所以不能在自定義型別的String()方法中使用%s,否則會死迴圈

type MyString string

func (m MyString) String() string {

    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.

}

修正版本:

type MyString string

func (m MyString) String() string {

    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.

}

22.append。append內建函式定義:

func append(slice []T, elements ...T) []T

T表示佔位符,可以是任意的型別。編譯時由編譯器替換為實際的型別。用法:

x := []int{1,2,3}

x = append(x, 4, 5, 6)

fmt.Println(x)

程式輸出:[1 2 3 4 5 6]. 

如果想將一個切片追加到另外一個切片末尾要怎麼做呢?可以使用…語法,例如:

x := []int{1,2,3}

y := []int{4,5,6}

x = append(x, y...)

fmt.Println(x)

如果沒有…,編譯將會不通過,因為y不是int型別。

總結

文章介紹了22個Golang的高效實踐建議,其中包括一些程式設計規範和一些實踐生產中容易遇到的坑,希望可以幫助到大家

引用

https://golang.org/doc/effective_go.html

 

相關文章