Google:12 條 Golang 最佳實踐

這是直接總結好的 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
}

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

如果對 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 方法中,分了兩大部分,增加了靈活性:

函數適配器

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. 命名儘可能簡潔

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

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

不要忘了,在調用包內容時,會先寫包名。

4. 多文件包

是否應該將一個包拆分到多個文件?

標準包 net/http 總共 15734 行代碼,被拆分到 47 個文件中。

net/http/cookie.go 和 net/http/cookie_test.go 文件都放置在 http 包下。

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

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

個人思考:當一個包的說明信息比較多時,可以考慮創建 doc.go 文件。

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

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

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

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

個人思考:如果一個項目中的可執行入口比較多,建議放置在 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 阻塞問題。

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JD5C2XadveECwdcUtIqoog