2018-03-13 Twelve Go Best Practices

Best practices

From Wikipedia:

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

  • simple
  • readable
  • maintainable

Code sample

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
}

1. 先處理錯(cuò)誤欣舵,來(lái)避免嵌套(avoid nesting by handling errors first)

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

2. 避免重復(fù)(avoid repetition when possible)

type binWriter struct {
    w      io.Writer
    size  int64
    err    error
}
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))
    }
}
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
}

處理特殊類型的數(shù)據(jù)(Type switch to handle special cases)

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
}

再進(jìn)一步優(yōu)化恍风,變量名變短(Type switch with short variable declaration)

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

Write everything or nothing

type binWriter struct {
    w    io.Writer
    buf  bytes.Buffer
    err  error
}
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()
}

適配器(Function adapters)

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
    }
}
Better
func init() {
    http.HandleFunc("/", errorHandler(betterHandler))
}

func errorHandler(f func(http.RequestWriter, *http.Request) error) http.HandleFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        err := f(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            log.Printf("handing %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
}

3. 優(yōu)先重要的代碼(Important code goes first)

  • license information
  • build tags
  • package documentation
  • import statements, related groups separated by blank lines
import (
    "fmt"
    "io"
    "log"

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

4. Document your code

package name

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

exported identifiers appear in godoc, they should be documented correctly

// 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) {

5. Shorter is better

or at least longer is not always better

  • Try to find the shortest name that is self explanatory
    • Prefer MarshaIndent to MarshalWithIndentation
  • Don't forget that the package name will appear before the identifier you chose
    • In package encoding/json we find the type Encoder, not JSONEncoder
    • It is referred as json.Encoder
      也就是說(shuō)在 encoding/json 里,Encoder 為啥不需要被稱作 JSONEncoder呢,是因?yàn)檎{(diào)用這個(gè)方法的時(shí)候,是 json.Encoder,沒(méi)必要 json.JSONEncoder

6. Packages with multiple files

  • 避免大文件萌京,所以要拆分
  • 測(cè)試代碼分離
  • package 里包含多個(gè)文件時(shí)焰轻,要寫一個(gè) doc.go 文件作為文檔說(shuō)明

7. Make your packages "go getable"

example

github.com/peterbourgon/foo/
    circle.yml
    Dockerfile
    cmd/
        foosrv/
            main.go
        foocli/
            main.go
    pkg/
        fs/
            fs.go
            fs_test.go
            mock.go
            mock_test.go
        merge/
            merge.go
            merge_test.go
        api/
            api.go
            api_test.go

8. Ask for what you need

相比于寫一個(gè)具體的類型数苫,使用接口(interface{})更方便測(cè)試

// 不方便測(cè)試
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) {

9. Keep independent packages independent

代碼樣例

import (
    "golang.org/x/talks/2013/bestpractices/funcdraw/drawer"
    "golang.org/x/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)
}

拆分成

Parsing
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 }
Drawing
import (
    "image"
 
   "golang.org/x/talks/2013/bestpractices/funcdraw/parser"
)

// Draw draws an image showing a rendering of the passed Function
func DrawParsedFunc(f parser.ParsedFunc) image.Image {
Avoid dependency by using an interface
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 {
testing

用接口而不是一個(gè)實(shí)際類型的好處也包括方便測(cè)試

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

10. Avoid concurrency in your API

API里就不用并行了
以下是不好的例子

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

開(kāi)發(fā)同步的API聪舒,這樣并行調(diào)用它們是很簡(jiǎn)單的。

11. Use goroutines to manage state

Use a chan or a struct with a chan to communicate with a goroutine
package main
import (
    "fmt"
    "time"
)

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

12. Avoid goroutine leaks

Use a chan or a struct with a chan to communicate with a 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
}

上面的問(wèn)題是:

  • the goroutine is blocked on the chan write
  • the goroutine holds a reference to the chan
  • the chan will never be garbage collected
    修正:(給 errc 加了一個(gè)長(zhǎng)度
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
}

這里還有個(gè)問(wèn)題虐急,如果我們不知道 addrs 的長(zhǎng)度箱残,或者說(shuō)無(wú)法預(yù)測(cè) errc 的長(zhǎng)度,要怎么辦止吁?
改進(jìn):(引入 quit channal)

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
}

這里使用了 select 來(lái)block channel

The select statement lets a goroutine wait on multiple communication operations.
A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.

OVER

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末被辑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子敬惦,更是在濱河造成了極大的恐慌盼理,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俄删,死亡現(xiàn)場(chǎng)離奇詭異宏怔,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)畴椰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門臊诊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人斜脂,你說(shuō)我怎么就攤上這事妨猩。” “怎么了秽褒?”我有些...
    開(kāi)封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)威兜。 經(jīng)常有香客問(wèn)我销斟,道長(zhǎng),這世上最難降的妖魔是什么椒舵? 我笑而不...
    開(kāi)封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任蚂踊,我火速辦了婚禮,結(jié)果婚禮上笔宿,老公的妹妹穿的比我還像新娘犁钟。我一直安慰自己,他們只是感情好泼橘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布涝动。 她就那樣靜靜地躺著,像睡著了一般炬灭。 火紅的嫁衣襯著肌膚如雪醋粟。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音米愿,去河邊找鬼厦凤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛育苟,可吹牛的內(nèi)容都是我干的较鼓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼违柏,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼博烂!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起勇垛,我...
    開(kāi)封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤脖母,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后闲孤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谆级,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年讼积,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肥照。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡勤众,死狀恐怖舆绎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情们颜,我是刑警寧澤吕朵,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站窥突,受9級(jí)特大地震影響努溃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜阻问,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一梧税、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧称近,春花似錦第队、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至衡未,卻和暖如春晾蜘,著一層夾襖步出監(jiān)牢的瞬間邻眷,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工剔交, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肆饶,地道東北人蛹尝。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓候学,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親撒穷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子竭鞍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容