Google:12 條 Golang 最佳實踐

語言: CN / TW / HK

收錄於《談技術》,作者 “老苗”

這是直接總結好的 12 條,詳細的再繼續往下看:

  1. 先處理錯誤避免巢狀
  2. 儘量避免重複
  3. 先寫最重要的程式碼
  4. 給程式碼寫文件註釋
  5. 命名儘可能簡潔
  6. 使用多檔案包
  7. 使用 go get 可獲取你的包
  8. 瞭解自己的需求
  9. 保持包的獨立性
  10. 避免在內部使用併發
  11. 使用 Goroutine 管理狀態
  12. 避免 Goroutine 洩露

最佳實踐

這是一篇翻譯文章,為了使讀者更好的理解,會在原文翻譯的基礎增加一些講解或描述。

來在維基百科:

"A best practice is a method or technique that has consistently shown results superior
to those achieved with other means"

最佳實踐是一種方法或技術,其結果始終優於其他方式。

寫 Go 程式碼時的技術要求:

  • 簡單性
  • 可讀性
  • 可維護性

樣例程式碼

需要優化的程式碼。

type Gopher struct {
    Name     string
    AgeYears int
}

func (g *Gopher) WriteTo(w io.Writer) (size int64, err error) {
    err = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err == nil {
        size += 4
        var n int
        n, err = w.Write([]byte(g.Name))
        size += int64(n)
        if err == nil {
            err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
            if err == nil {
                size += 4
            }
            return
        }
        return
    }
    return
}

看看上面的程式碼,自己先思索在程式碼編寫方式上怎麼更好,我先簡單說下程式碼意思是啥:

  • NameAgeYears 欄位資料存入 io.Writer 型別中。
  • 如果存入的資料是 string[]byte 型別,再追加其長度資料。

如果對 binary 這個標準包不知道怎麼使用,就看看我的另一篇文章《快速瞭解 “小字端” 和 “大字端” 及 Go 語言中的使用》

先處理錯誤避免巢狀

func (g *Gopher) WriteTo(w io.Writer) (size int64, err error) {
    err = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err != nil {
        return
    }
    size += 4
    n, err := w.Write([]byte(g.Name))
    size += int64(n)
    if err != nil {
        return
    }
    err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
    if err == nil {
        size += 4
    }
    return
}

減少判斷錯誤的巢狀,會使讀者看起來更輕鬆。

儘量避免重複

上面程式碼中 WriteTo 方法中的 Write 出現了 3 次,比較重複,精簡後如下:

type binWriter struct {
    w    io.Writer
    size int64
    err  error
}

// Write writes a value to the provided writer in little endian form.
func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
        w.size += int64(binary.Size(v))
    }
}

使用 binWriter 結構體。

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(int32(len(g.Name)))
    bw.Write([]byte(g.Name))
    bw.Write(int64(g.AgeYears))
    return bw.size, bw.err
}

type-switch 處理不同型別

func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch v.(type) {
    case string:
        s := v.(string)
        w.Write(int32(len(s)))
        w.Write([]byte(s))
    case int:
        i := v.(int)
        w.Write(int64(i))
    default:
        if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
            w.size += int64(binary.Size(v))
        }
    }
}

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(g.Name)
    bw.Write(g.AgeYears)
    return bw.size, bw.err
}

type-switch 精簡

摒棄了上面程式碼的 v.(string)v.(int) 型別反射使用。

func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch x := v.(type) {
    case string:
        w.Write(int32(len(x)))
        w.Write([]byte(x))
    case int:
        w.Write(int64(x))
    default:
        if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
            w.size += int64(binary.Size(v))
        }
    }
}

進入不同分支,x 變數對應的就是該分支的型別。

自行決定是否寫入

type binWriter struct {
    w   io.Writer
    buf bytes.Buffer
    err error
}

// Write writes a value to the provided writer in little endian form.
func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch x := v.(type) {
    case string:
        w.Write(int32(len(x)))
        w.Write([]byte(x))
    case int:
        w.Write(int64(x))
    default:
        w.err = binary.Write(&w.buf, binary.LittleEndian, v)
    }
}

// Flush writes any pending values into the writer if no error has occurred.
// If an error has occurred, earlier or with a write by Flush, the error is
// returned.
func (w *binWriter) Flush() (int64, error) {
    if w.err != nil {
        return 0, w.err
    }
    return w.buf.WriteTo(w.w)
}

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(g.Name)
    bw.Write(g.AgeYears)
    return bw.Flush()
}

WriteTo 方法中,分了兩大部分,增加了靈活性:

  • 組裝資訊
  • 呼叫 Flush 方法來決定是否寫入 w

函式介面卡

func init() {
    http.HandleFunc("/", handler)
}

func handler(w http.ResponseWriter, r *http.Request) {
    err := doThis()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        log.Printf("handling %q: %v", r.RequestURI, err)
        return
    }

    err = doThat()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        log.Printf("handling %q: %v", r.RequestURI, err)
        return
    }
}

函式 handler 包含了業務的邏輯和錯誤處理,下來將錯誤處理單獨寫一個函式處理,程式碼修改如下:

func init() {
    http.HandleFunc("/", errorHandler(betterHandler))
}

func errorHandler(f func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        err := f(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            log.Printf("handling %q: %v", r.RequestURI, err)
        }
    }
}

func betterHandler(w http.ResponseWriter, r *http.Request) error {
    if err := doThis(); err != nil {
        return fmt.Errorf("doing this: %v", err)
    }

    if err := doThat(); err != nil {
        return fmt.Errorf("doing that: %v", err)
    }
    return nil
}

組織你的程式碼

1. 先寫最重要的

許可資訊、構建資訊、包文件。

import 語句:相關聯組使用空行分隔。

import (
    "fmt"
    "io"
    "log"

    "golang.org/x/net/websocket"
)

其餘程式碼,以最重要的型別開始,以輔助函式和型別結尾。

2. 文件註釋

包名前的相關文件。

// Package playground registers an HTTP handler at "/compile" that
// proxies requests to the golang.org playground service.
package playground

Go 語言中的標示符(變數、結構體等等)在 godoc 匯出的文章中應該被正確的記錄下來。

// Author represents the person who wrote and/or is presenting the document.
type Author struct {
    Elem []Elem
}

// TextElem returns the first text elements of the author details.
// This is used to display the author' name, job title, and company
// without the contact details.
func (p *Author) TextElem() (elems []Elem) {

擴充套件

使用 godoc 工具在網頁上檢視 go 專案文件。

# 安裝
go get golang.org/x/tools/cmd/godoc

# 啟動服務
godoc -http=:6060

直接在本地訪問 localhost:6060 檢視文件。

3. 命名儘可能簡潔

或者說,長命名不一定好。

儘可能找到一個可以清晰表達的簡短命名,例如:

  • MarshalIndentMarshalWithIndentation 好。

不要忘了,在呼叫包內容時,會先寫包名。

  • encoding/json 包內,有一個結構體 Encoder,不要寫成 JSONEncoder
  • 這樣被使用 json.Encoder

4. 多檔案包

是否應該將一個包拆分到多個檔案?

  • 應避免程式碼太長

標準包 net/http 總共 15734 行程式碼,被拆分到 47 個檔案中。

  • 拆分程式碼和測試。

net/http/cookie.go 和 net/http/cookie_test.go 檔案都放置在 http 包下。

測試程式碼只有在測試時才被編譯。

  • 拆分包文件

當在一個包內有多個檔案時,按照慣例,建立一個 doc.go 檔案編寫包的文件描述。

個人思考:當一個包的說明資訊比較多時,可以考慮建立 doc.go 檔案。

5. 使用 go get 可獲取你的包

當你的包被提供使用時,應該清晰的讓使用者知道哪些可複用,哪些不可複用。

所以,當一些包可能會被複用,有些則不會的情況下怎麼做?

例如:定義一些網路協議的包可能會複用,而定義一些可執行命令的包則不會。

  • cmd 可執行命令的包,不提供複用
  • pkg 可複用的包

個人思考:如果一個專案中的可執行入口比較多,建議放置在 cmd 目錄中,而對於 pkg 目錄目前是不太建議,所以不用借鑑。

API

1. 瞭解自己的需求

我們繼續使用之前的 Gopher 型別。

type Gopher struct {
    Name     string
    AgeYears int
}

我們可以定義這個方法。

func (g *Gopher) WriteToFile(f *os.File) (int64, error) {

但方法的引數使用具體的型別時會變得難以測試,因此我們使用介面。

func (g *Gopher) WriteToReadWriter(rw io.ReadWriter) (int64, error) {

並且,當使用了介面後,我們應該只需定義我們所需要的方法。

func (g *Gopher) WriteToWriter(f io.Writer) (int64, error) {

2. 保持包的獨立性

import (
    "golang.org/x/talks/content/2013/bestpractices/funcdraw/drawer"
    "golang.org/x/talks/content/2013/bestpractices/funcdraw/parser"
)
// Parse the text into an executable function.
  f, err := parser.Parse(text)
  if err != nil {
      log.Fatalf("parse %q: %v", text, err)
  }

  // Create an image plotting the function.
  m := drawer.Draw(f, *width, *height, *xmin, *xmax)

  // Encode the image into the standard output.
  err = png.Encode(os.Stdout, m)
  if err != nil {
      log.Fatalf("encode image: %v", err)
  }

程式碼中 Draw 方法接受了 Parse 函式返回的 f 變數,從邏輯上看 drawer 包依賴 parser 包,下來看看如何取消這種依賴性。

parser 包:

type ParsedFunc struct {
    text string
    eval func(float64) float64
}

func Parse(text string) (*ParsedFunc, error) {
    f, err := parse(text)
    if err != nil {
        return nil, err
    }
    return &ParsedFunc{text: text, eval: f}, nil
}

func (f *ParsedFunc) Eval(x float64) float64 { return f.eval(x) }
func (f *ParsedFunc) String() string         { return f.text }

drawer 包:

import (
    "image"

    "golang.org/x/talks/content/2013/bestpractices/funcdraw/parser"
)

// Draw draws an image showing a rendering of the passed ParsedFunc.
func DrawParsedFunc(f parser.ParsedFunc) image.Image {

使用介面型別,避免依賴。

import "image"

// Function represent a drawable mathematical function.
type Function interface {
    Eval(float64) float64
}

// Draw draws an image showing a rendering of the passed Function.
func Draw(f Function) image.Image {

測試:介面型別比具體型別更容易測試。

package drawer

import (
    "math"
    "testing"
)

type TestFunc func(float64) float64

func (f TestFunc) Eval(x float64) float64 { return f(x) }

var (
    ident = TestFunc(func(x float64) float64 { return x })
    sin   = TestFunc(math.Sin)
)

func TestDraw_Ident(t *testing.T) {
    m := Draw(ident)
    // Verify obtained image.

4. 避免在內部使用併發

func doConcurrently(job string, err chan error) {
    go func() {
        fmt.Println("doing job", job)
        time.Sleep(1 * time.Second)
        err <- errors.New("something went wrong!")
    }()
}

func main() {
    jobs := []string{"one", "two", "three"}

    errc := make(chan error)
    for _, job := range jobs {
        doConcurrently(job, errc)
    }
    for _ = range jobs {
        if err := <-errc; err != nil {
            fmt.Println(err)
        }
    }
}

如果這樣做,那如果我們想同步呼叫 doConcurrently 該如何做?

func do(job string) error {
    fmt.Println("doing job", job)
    time.Sleep(1 * time.Second)
    return errors.New("something went wrong!")
}

func main() {
    jobs := []string{"one", "two", "three"}

    errc := make(chan error)
    for _, job := range jobs {
        go func(job string) {
            errc <- do(job)
        }(job)
    }
    for _ = range jobs {
        if err := <-errc; err != nil {
            fmt.Println(err)
        }
    }
}

對外暴露同步的函式,這樣併發呼叫時也是容易的,同樣也滿足同步呼叫。

最佳的併發實踐

1. 使用 Goroutine 管理狀態

Goroutine 之間使用一個 “通道” 或帶有通道欄位的 “結構體” 來通訊。

type Server struct{ quit chan bool }

func NewServer() *Server {
    s := &Server{make(chan bool)}
    go s.run()
    return s
}

func (s *Server) run() {
    for {
        select {
        case <-s.quit:
            fmt.Println("finishing task")
            time.Sleep(time.Second)
            fmt.Println("task done")
            s.quit <- true
            return
        case <-time.After(time.Second):
            fmt.Println("running task")
        }
    }
}

func (s *Server) Stop() {
    fmt.Println("server stopping")
    s.quit <- true
    <-s.quit
    fmt.Println("server stopped")
}

func main() {
    s := NewServer()
    time.Sleep(2 * time.Second)
    s.Stop()
}

2. 使用帶緩衝的通道避免 Goroutine 洩露

func sendMsg(msg, addr string) error {
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        return err
    }
    defer conn.Close()
    _, err = fmt.Fprint(conn, msg)
    return err
}

func main() {
    addr := []string{"localhost:8080", "http://google.com"}
    err := broadcastMsg("hi", addr)

    time.Sleep(time.Second)

    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("everything went fine")
}

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error)
    for _, addr := range addrs {
        go func(addr string) {
            errc <- sendMsg(msg, addr)
            fmt.Println("done")
        }(addr)
    }

    for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }
    return nil
}

這段程式碼有個問題,如果提前返回了 err 變數,errc 通道將不會被讀取,因此 Goroutine 將會阻塞。

總結

  • 在寫入通道時 Goroutine 被阻塞。
  • Goroutine 持有對通道的引用。
  • 通道不會被 gc 回收。

使用緩衝通道解決 Goroutine 阻塞問題。

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error, len(addrs))
    for _, addr := range addrs {
        go func(addr string) {
            errc <- sendMsg(msg, addr)
            fmt.Println("done")
        }(addr)
    }

    for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }
    return nil
}

如果我們不能預知通道的緩衝大小,也稱容量,那該怎麼辦?

建立一個傳遞退出狀態的通道來避免 Goroutine 的洩露。

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error)
    quit := make(chan struct{})

    defer close(quit)

    for _, addr := range addrs {
        go func(addr string) {
            select {
            case errc <- sendMsg(msg, addr):
                fmt.Println("done")
            case <-quit:
                fmt.Println("quit")
            }
        }(addr)
    }

    for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }
    return nil
}

參考

原文連結:https://talks.golang.org/2013/bestpractices.slide#1

影片連結:https://www.youtube.com/watch?v=8D3Vmm1BGoY