Golang高效實踐之泛談篇


前言

我博客之前的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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM