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. 先处理错误,来避免嵌套(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. 避免重复(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
}

处理特殊类型的数据(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
}

再进一步优化,变量名变短(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. 优先重要的代码(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
也就是说在 encoding/json 里,Encoder 为啥不需要被称作 JSONEncoder呢,是因为调用这个方法的时候,是 json.Encoder,没必要 json.JSONEncoder

6. Packages with multiple files

  • 避免大文件,所以要拆分
  • 测试代码分离
  • package 里包含多个文件时,要写一个 doc.go 文件作为文档说明

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

相比于写一个具体的类型,使用接口(interface{})更方便测试

// 不方便测试
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

用接口而不是一个实际类型的好处也包括方便测试

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

开发同步的API,这样并行调用它们是很简单的。

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
}

上面的问题是:
- the goroutine is blocked on the chan write
- the goroutine holds a reference to the chan
- the chan will never be garbage collected
修正:(给 errc 加了一个长度

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
}

这里还有个问题,如果我们不知道 addrs 的长度,或者说无法预测 errc 的长度,要怎么办?
改进:(引入 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 来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

猜你喜欢

转载自blog.csdn.net/sunzhongyuan888/article/details/80370874