Go 編程語言的 12 條最佳實踐

jopen 11年前發布 | 21K 次閱讀 Go語言 Google Go/Golang開發

本文來自 Google 工程師 Francesc Campoy Flores 分享的幻燈片。內容包括:代碼組織、API、并發最佳實踐和一些推薦的相關資源。

最佳實踐

維基百科的定義是:

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

寫Go代碼的目標就是:

  • 簡潔
  • 可讀性強
  • 可維護性好

樣例代碼

type Gopher struct {
    Name     string
    Age      int32
    FurColor color.Color
}

func (g *Gopher) DumpBinary(w io.Writer) error {
    err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err == nil {
        _, err := w.Write([]byte(g.Name))
        if err == nil {
            err := binary.Write(w, binary.LittleEndian, g.Age)
            if err == nil {
                return binary.Write(w, binary.LittleEndian, g.FurColor)
            }
            return err
        }
        return err
    }
    return err
}

避免嵌套的處理錯誤

func (g *Gopher) DumpBinary(w io.Writer) error {
    err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err != nil {
        return err
    }
    _, err = w.Write([]byte(g.Name))
    if err != nil {
        return err
    }
    err = binary.Write(w, binary.LittleEndian, g.Age)
    if err != nil {
        return err
    }
    return binary.Write(w, binary.LittleEndian, g.FurColor)
}

減少嵌套意味著提高代碼的可讀性

盡可能避免重復

功能單一,代碼更簡潔

type binWriter struct {
    w   io.Writer
    err error
}

// Write writes a value into its writer using little endian.
func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    w.err = binary.Write(w.w, binary.LittleEndian, v)
}

func (g *Gopher) DumpBinary(w io.Writer) error {
    bw := &binWriter{w: w}
    bw.Write(int32(len(g.Name)))
    bw.Write([]byte(g.Name))
    bw.Write(g.Age)
    bw.Write(g.FurColor)
    return bw.err
}

使用類型推斷來處理特殊情況

// Write writes a value into its writer using little endian.
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))
    default:
        w.err = binary.Write(w.w, binary.LittleEndian, v)
    }
}

func (g *Gopher) DumpBinary(w io.Writer) error {
    bw := &binWriter{w: w}
    bw.Write(g.Name)
    bw.Write(g.Age)
    bw.Write(g.FurColor)
    return bw.err
}

類型推斷的變量聲明要短

// Write write the given value into the writer using little endian.
func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch v := v.(type) {
    case string:
        w.Write(int32(len(v)))
        w.Write([]byte(v))
    default:
        w.err = binary.Write(w.w, binary.LittleEndian, v)
    }
}

函數適配器

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
    }
}

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
}

如何組織代碼

 

將重要的代碼放前面

版權信息,構建信息,包說明文檔

Import 聲明,相關的包連起來構成組,組與組之間用空行隔開.。

import (
    "fmt"
    "io"
    "log"

    "code.google.com/p/go.net/websocket"
)

接下來代碼以最重要的類型開始,以工具函數和類型結束。

 

如何編寫文檔

包名之前要寫相關文檔

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

導出的標識符(譯者按:大寫的標識符為導出標識符)會出現在 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) {

生成的文檔示例

Gocode: 文檔化Go代碼

 

越簡潔越好

或者 長代碼往往不是最好的.

試著使用能自解釋的最短的變量名.

  • MarshalIndent ,別用 MarshalWithIndentation.

別忘了包名會出現在你選擇的標識符前面

  • In package encoding/json we find the type Encoder, not JSONEncoder.
  • It is referred as json.Encoder.

 

有多個文件的包

需要將一個包分散到多個文件中嗎?

  • 避免行數非常多的文件

標準庫中 net/http 包有47個文件,共計 15734 行.

  • 拆分代碼并測試

net/http/cookie.gonet/http/cookie_test.go 都是 http 包的一部分.

測試代碼 只有 在測試時才會編譯.

  • 多文件包的文檔編寫

如果一個包中有多個文件, 可以很方便的創建一個 doc.go 文件,包含包文檔信息.

讓包可以”go get”到

一些包將來可能會被復用,另外一些不會.

定義了一些網絡協議的包可能會在開發一個可執行命令時復用.

github.com/bradfitz/camlistore

 

接口

你需要什么

讓我們以之前的Gopher類型為例

type Gopher struct {
    Name     string
    Age      int32
    FurColor color.Color
}

我們可以定義這個方法

func (g *Gopher) DumpToFile(f *os.File) error {

但是使用一個具體的類型會讓代碼難以測試,因此我們使用接口.

func (g *Gopher) DumpToReadWriter(rw io.ReadWriter) error {

進而,由于使用的是接口,我們可以只請求我們需要的.

func (g *Gopher) DumpToWriter(f io.Writer) error {

讓獨立的包彼此獨立

import (
    "code.google.com/p/go.talks/2013/bestpractices/funcdraw/drawer"
    "code.google.com/p/go.talks/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)
    }

解析

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 }

描繪

import (
    "image"

    "code.google.com/p/go.talks/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.

在接口中避免并發

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)
        }
    }
}

如果我們想串行的使用它會怎樣?

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)
        }
    }
}

暴露同步的接口,這樣異步調用這些接口會簡單.

 

并發的最佳實踐

 

使用goroutines管理狀態

使用chan或者有chan的結構體和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()
}

使用帶緩存的chan,來避免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
}
  • goroutine阻塞在chan寫操作
  • goroutine保存了一個chan的引用
  • chan永遠不會垃圾回收
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
}

如果我們不能預測channel的容量呢?

使用quit chan避免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
}

12條最佳實踐

1. 避免嵌套的處理錯誤
2. 盡可能避免重復
3. 將重要的代碼放前面
4. 為代碼編寫文檔
5. 越簡潔越好
6. 講包拆分到多個文件中
7. 讓包”go get”到
8. 按需請求
9. 讓獨立的包彼此獨立
10. 在接口中避免并發
11. 使用goroutine管理狀態
12. 避免goroutine內存泄漏

一些鏈接

資源

其他演講

  • 用go做詞法掃描 video
  • 并發不是并行 video
  • Go并發模式 video
  • Go高級并發模式 video

謝謝

原文鏈接: Francesc Campoy Flores   翻譯: 伯樂在線 - Codefor
譯文鏈接: http://blog.jobbole.com/44608/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!