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
}

看看上面的代码,自己先思索在代码编写方式上怎么更好,我先简单说下代码意思是啥:

  • 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 可获取你的包

当你的包被提供使用时,应该清晰的让使用者知道哪些可复用,哪些不可复用。

所以,当一些包可能会被复用,有些则不会的情况下怎么做?

例如:定义一些网络协议的包可能会复用,而定义一些可执行命令的包则不会。

[图片上传失败...(image-78ab15-1637144890100)]

  • 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容