Go风格指南[转]

Go Styleguide

Go Styleguide
本文是对
Effective Go 的补充, 其条目来自于经年累月的会议上得到的经验和灵感。

Table of contents

给错误添加上下文

Don't:

file, err := os.Open("foo.txt")
if err != nil {
    return err
}

这种处理方式会导致错误信息不清晰,因为丢失了错误本来的上下文。

Do:

import "github.com/pkg/errors" // for example

// ...

file, err := os.Open("foo.txt")
if err != nil {
    return errors.Wrap(err, "open foo.txt failed")
}

用自定义的 message 包装错误可以在错误从栈中向上“冒泡”的时候提供错误的上下文。
这么做并不一定总有意义。
如果你不确定一个返回的错误信息是否充分(译注:能够帮助判断问题在哪里),
那么就对 error 进行 wrap。
确保根 error 在 wrap 之后仍然可以访问到,用于 type checking。

依赖管理

使用dep

由于 dep 已经 production ready,并且将来会成为官方的工具链之一
Sam Boyer at GopherCon 2017
因此开始使用 dep 吧。 dep

使用Semantic版本号

由于 dep 可以管理依赖版本,尽量使用 semver 对你的项目打 tag。
Semantic Versioning.

避免使用gopkgin

gopkg.in 是很棒很有用的工具,这个工具会将你的依赖打 tag,但其本来的设计并不是要与 dep 协作。
请直接 import,使用 dep 并在 Gopkg.toml 中指定版本。

结构化的日志

Don't:

log.Printf("Listening on :%d", port)
http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
// 2017/07/29 13:05:50 Listening on :80

Do:

import "github.com/uber-go/zap" // for example

// ...

logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("Server started",
    zap.Int("port", port),
    zap.String("env", env),
)
http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
// {"level":"info","ts":1501326297.511464,"caller":"Desktop/structured.go:17","msg":"Server started","port":80,"env":"production"}

这个例子并不是很有说服力,不过使用结构化的日志可以让你的日志无论是 debug 还是被日志收集 parse 都变得更容易。

避免全局变量

Don't:

var db *sql.DB

func main() {
    db = // ...
    http.HandleFunc("/drop", DropHandler)
    // ...
}

func DropHandler(w http.ResponseWriter, r *http.Request) {
    db.Exec("DROP DATABASE prod")
}

全局变量会使测试难度增加,会使代码的可读性降低,每一个函数都能够访问这些全局变量(即使是那些根本就不需要操作全局变量的函数)。

Do:

func main() {
    db := // ...
    http.HandleFunc("/drop", DropHandler(db))
    // ...
}

func DropHandler(db *sql.DB) http.HandleFunc {
    return func (w http.ResponseWriter, r *http.Request) {
        db.Exec("DROP DATABASE prod")
    }
}

使用高阶函数(high-order function)来按需注入依赖,而不是全局变量。

测试

使用assert库

Don't:

func TestAdd(t *testing.T) {
    actual := 2 + 2
    expected := 4
    if (actual != expected) {
        t.Errorf("Expected %d, but got %d", expected, actual)
    }
}

Do:

import "github.com/stretchr/testify/assert" // for example

func TestAdd(t *testing.T) {
    actual := 2 + 2
    expected := 4
    assert.Equal(t, expected, actual)
}

使用 assert 库使测试代码更可读,节省冗余的代码并提供稳定的错误输出。

使用表驱动的测试

Don't:

func TestAdd(t *testing.T) {
    assert.Equal(t, 1+1, 2)
    assert.Equal(t, 1+-1, 0)
    assert.Equal(t, 1, 0, 1)
    assert.Equal(t, 0, 0, 0)
}

上面的程序看着还算简单,但是想找一个 fail 掉的 case 却非常麻烦,特别是有几百个 test case 的时候尤其如此。

Do:

func TestAdd(t *testing.T) {
    cases := []struct {
        A, B, Expected int
    }{
        {1, 1, 2},
        {1, -1, 0},
        {1, 0, 1},
        {0, 0, 0},
    }

    for _, tc := range cases {
        t.Run(fmt.Sprintf("%d + %d", tc.A, tc.B), func(t *testing.T) {
            assert.Equal(t, t.Expected, tc.A+tc.B)
        })
    }
}

使用表驱动的 tests 结合子测试能够让你直接看到哪些 case 被测试,哪一个 case 失败了。
Mitchell Hashimoto at GopherCon 2017

避免mock

Don't:

func TestRun(t *testing.T) {
    mockConn := new(MockConn)
    run(mockConn)
}

Do:

func TestRun(t *testing.T) {
    ln, err := net.Listen("tcp", "127.0.0.1:0")
    t.AssertNil(t, err)

    var server net.Conn
    go func() {
        defer ln.Close()
        server, err := ln.Accept()
        t.AssertNil(t, err)
    }()

    client, err := net.Dial("tcp", ln.Addr().String())
    t.AssertNil(err)

    run(client)
}

只在没有其它办法的时候才使用 mock,尽量使用真正的实现。
Mitchell Hashimoto at GopherCon 2017

避免DeepEqual

Don't:

type myType struct {
    id         int
    name       string
    irrelevant []byte
}

func TestSomething(t *testing.T) {
    actual := &myType{/* ... */}
    expected := &myType{/* ... */}
    assert.True(t, reflect.DeepEqual(expected, actual))
}

Do:

type myType struct {
    id         int
    name       string
    irrelevant []byte
}

func (m *myType) testString() string {
    return fmt.Sprintf("%d.%s", m.id, m.name)
}

func TestSomething(t *testing.T) {
    actual := &myType{/* ... */}
    expected := &myType{/* ... */}
    if actual.testString() != expected.testString() {
        t.Errorf("Expected '%s', got '%s'", expected.testString(), actual.testString())
    }
    // or assert.Equal(t, actual.testString(), expected.testString())
}

使用 testString() 这种方式来比较 struct,在结构体比较复杂,并且内含有逻辑上不影响相等判断的字段,那么就应该使用这种方式来进行相等判断。
这种方式只在结构体比较大,或者是“类树”的结构体比较中比较有用:
Mitchell Hashimoto at GopherCon 2017

不要测试非导出函数

只对导出的函数进行测试,如果一个函数是 unexported 并且没有办法通过 exported 函数走到其逻辑,说明这个函数很可能会经常变动,没有必要进行测试。

使用linter

使用 linter, (e.g. gometalinter) 在提交你的项目之前先进行 lint 来帮助查找潜在的规范问题和代码错误。

使用gofmt

在提交之前一定要对文件进行 gofmt,使用 -s 参数来简化代码。

避免side-effects

Don't:

func init() {
    someStruct.Load()
}

side-effects
指函数或者代码会改变其作用域外的内容或值的行为。只有在一些特定的情况下 side-effects 是允许的(比如:在命令行中解析 flags)

如果你想不出其它的办法来避免,那么就重新思考并尝试重构吧。

尽量使用纯函数

在计算机程序中,如果一个函数满足下面的几个条件,那么这个函数就是一个纯函数:

  1. 这个函数在相同的参数下一定会产生相同的结果。即函数的返回值不依赖于任何隐藏在函数内的信息或者状态,而这些隐藏的内容在程序的运行期还可能会变化。且函数不应依赖于任何从 I/O 设备中输入的信息。
  2. 对函数的返回结果进行操作不会引起任何语义上的副作用或者输出,比如导致可变对象的变化或者输出数据到 I/O 设备去。

Wikipedia

Don't:

func MarshalAndWrite(some *Thing) error {
    b, err := json.Marshal(some)
    if err != nil {
        return err
    }

    return ioutil.WriteFile("some.thing", b, 0644)
}

Do:

// Marshal is a pure func (even though useless)
func Marshal(some *Thing) ([]bytes, error) {
    return json.Marshal(some)
}

// ...

纯函数并不一定在所有场景下都适用,但保证你用到的函数尽量都是纯函数能够让你的代码更易理解,且更容易 debug。

避免接口臃肿

Don't:

type Server interface {
    Serve() error
    Some() int
    Fields() float64
    That() string
    Are([]byte) error
    Not() []string
    Necessary() error
}

func debug(srv Server) {
    fmt.Println(srv.String())
}

func run(srv Server) {
    srv.Serve()
}

Do:

type Server interface {
    Serve() error
}

func debug(v fmt.Stringer) {
    fmt.Println(v.String())
}

func run(srv Server) {
    srv.Serve()
}

尽量使用小的 interface,并且在你的函数中只要求传入需要的 interface。

Don't under-package

删除或者合并 package 要比将大的 package 分开容易得多。如果不确定一个包是否可以分开,那么最好去试一试。

处理信号

Don't:

func main() {
    for {
        time.Sleep(1 * time.Second)
        ioutil.WriteFile("foo", []byte("bar"), 0644)
    }
}

Do:

func main() {
    logger := // ...
    sc := make(chan os.Signal, 1)
    done := make(chan bool)

    go func() {
        for {
            select {
            case s := <-sc:
                logger.Info("Received signal, stopping application",
                    zap.String("signal", s.String()))
                done <- true
                return
            default:
                time.Sleep(1 * time.Second)
                ioutil.WriteFile("foo", []byte("bar"), 0644)
            }
        }
    }()

    signal.Notify(sc, os.Interrupt, os.Kill)
    <-done // Wait for go-routine
}

对 os 的信号进行处理能够让我们 gracefully 地停止服务,关闭打开的文件和连接,并且能够防止因为服务的意外关闭而导致文件损坏或其它问题。

分块组织import

Don't:

import (
    "encoding/json"
    "github.com/some/external/pkg"
    "fmt"
    "github.com/this-project/pkg/some-lib"
    "os"
)

Do:

import (
    "encoding/json"
    "fmt"
    "os"

    "github.com/some/external/pkg"

    "github.com/this-project/pkg/some-lib"
)

将 std,外部包和 internal 导入分开写,可以提高可读性。

避免不加修饰的return

Don't:

func run() (n int, err error) {
    // ...
    return
}

Do:

func run() (n int, err error) {
    // ...
    return n, err
}

命名返回的值对文档编写或者生成是有益的,不加任何修饰的 return 会让代码变得难读而易错。

添加包的权威导入路径

Don't:

package sub

Do:

package sub // import "github.com/my-package/pkg/sth/else/sub"

在注释中添加权威导入路径,能够给包添加上下文,也能够帮助用户更容易地导入你的包。

避免空接口

Don't:

func run(foo interface{}) {
    // ...
}

空接口会让代码变得复杂而不清晰,只要能够不使用,就应该在任何时候避免。

main函数先行

Don't:

package main // import "github.com/me/my-project"

func someHelper() int {
    // ...
}

func someOtherHelper() string {
    // ...
}

func Handler(w http.ResponseWriter, r *http.Reqeust) {
    // ...
}

func main() {
    // ...
}

Do:

package main // import "github.com/me/my-project"

func main() {
    // ...
}

func Handler(w http.ResponseWriter, r *http.Reqeust) {
    // ...
}

func someHelper() int {
    // ...
}

func someOtherHelper() string {
    // ...
}

main() 函数放在你文件的最开始,能够让阅读这个文件变得更加轻松。如果有 init() 函数的话,应该再放在 main() 之前。

使用internal包

如果你想创建一个 cmd,考虑将 libraries 移动到 internal/ 包中,而避免这些不稳定可能经常会变化的库被其它项目引用。

避免使用helper/util的文件名、包名

使用清晰的命名,避免创建形如:helper.goutil.go 这样的文件名或者 package。

内嵌二进制数据

为了在部署阶段只有一个二进制文件,使用工具来将 templates 和其它静态内容嵌入到你的二进制文件中
(e.g. github.com/jteeuwen/go-bindata).

使用函数式的配置选项


func main() {
    // ...
    startServer(
        WithPort(8080),
        WithTimeout(1 * time.Second),
    )
}

type Config struct {
    port    int
    timeout time.Duration
}

type ServerOpt func(*Config)

func WithPort(port int) ServerOpt {
    return func(cfg *Config) {
        cfg.port = port
    }
}

func WithTimeout(timeout time.Duration) ServerOpt {
    return func(cfg *Config) {
        cfg.timeout = timeout
    }
}

func startServer(opts ...ServerOpt) {
    cfg := new(Config)
    for _, fn := range opts {
        fn(cfg)
    }

    // ...
}


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

推荐阅读更多精彩内容