前言
我部落格之前的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