译文 Google:12 条 Golang 最佳实践

printlove · 2021年10月29日 · 最后由 printlove 回复于 2021年11月02日 · 970 次阅读
本帖已被设为精华帖!

>> 原文链接

这是直接总结好的 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

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
ningxiaofang1o GoCN 每日新闻 (2021-10-30) 中提及了此贴 10月30日 05:37
astaxie 将本帖设为了精华贴 10月30日 05:39

优化代码的例子中,优化前的得出的结果和优化后得出结果不一样吧?

err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
if err == nil {
    // int64 这个位置应该加8个字节吧
    // size += 4
    size +=8
}
cdhraesaemer 回复

我认真查了下英文原文,确定是 size += 4,但你说的这个,代码应该是不对的, size += 8 是对的。😂

@printlove 你的这些文章都不错,可以给 GoCN 公众号开一下白名单吗?GoCN 公众号可以转发宣传一下,公众号 id:golangchina

astaxie 回复

可以呀

astaxie 回复

好了

Orichalcum GoCN 每日新闻 (2021-11-11) 中提及了此贴 11月11日 13:05
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册