前言

最近写go,不够规范,这里稍微总结下优步的go标准,还没总结完。

正文

指导原则

  1. 使用值接收器的方法既可以通过值调用,也可以通过指针调用,因为可以自动转换

  2. sync.Mutex 和 sync.RWMutex 是有效的。几乎不需要一个指向 mutex 的指针。

    可以使用非指针形式嵌入结构体中

  3. 边界处拷贝 Slices 和 Maps,注意是不是拷贝还是引用

    slices 和 maps 包含了指向底层数据的指针

    1. 当作为参数进行传递时,如果存储的是其引用,其可能是会被改变的。

      正确:

      错误:

    2. 返回值时,注意返回的是拷贝么

      正确:

      错误:

  4. 使用defer作为清理,例如文件和锁

  5. channal的size要么是1,要么是无缓冲的

    对于任何其他的尺寸都需要进行严格的审查

  6. 枚举从1开始

    引入枚举的标准方法是声明一个自定义类型和一个使用了 iota 的 const 组。由于变量的默认值为 0,因此通常应以非零值开头枚举。

    正确:

    有时候当从零值开始时是有意义的

  7. 错误类型

    对于如何处理错误有一下几个选项:

  • errors.New 对于简单静态字符串的错误
  • fmt.Errorf 用于格式化的错误字符串
  • 实现 Error() 方法的自定义类型
  • "pkg/errors".Wrap 的 Wrapped errors

    针对错误处理,需要考虑:

  • 这是一个不需要额外信息的简单错误吗?如果是这样,errors.New 足够了。

  • 客户需要检测并处理此错误吗?如果是这样,则应使用自定义类型并实现该 Error() 方法。
  • 您是否正在传播下游函数返回的错误?如果是这样,请查看本文后面有关错误包装 section on error wrapping 部分的内容。
  • 否则 fmt.Errorf 就可以了。
  1. 如果客户端需要检测错误,并且您已使用创建了一个简单的错误 errors.New,请使用一个错误变量

    错误:

    go
    // package foo

    func Open() error {
    return errors.New("could not open")
    }

    // package bar

    func use() {
    if err := foo.Open(); err != nil {
    if err.Error() == "could not open" {
    // handle
    } else {
    panic("unknown error")
    }
    }
    }

    正确:

    go
    // package foo

    var ErrCouldNotOpen = errors.New("could not open")

    func Open() error {
    return ErrCouldNotOpen
    }

    // package bar

    if err := foo.Open(); err != nil {
    if err == foo.ErrCouldNotOpen {
    // handle
    } else {
    panic("unknown error")
    }
    }

  2. 如果您有可能需要客户端检测的错误,并且想向其中添加更多信息(例如,它不是静态字符串),则应使用自定义类型。

    错误:

    go
    func open(file string) error {
    return fmt.Errorf("file %q not found", file)
    }

    func use() {
    if err := open(); err != nil {
    if strings.Contains(err.Error(), "not found") {
    // handle
    } else {
    panic("unknown error")
    }
    }
    }

    正确:

    go
    type errNotFound struct {
    file string
    }

    func (e errNotFound) Error() string {
    return fmt.Sprintf("file %q not found", e.file)
    }

    func open(file string) error {
    return errNotFound{file: file}
    }

    func use() {
    if err := open(); err != nil {
    if _, ok := err.(errNotFound); ok {
    // handle
    } else {
    panic("unknown error")
    }
    }
    }

  3. 直接导出自定义错误类型时要小心,因为它们已成为程序包公共 API 的一部分。最好公开匹配器功能以检查是否为这个错误。

    go
    // package foo

    type errNotFound struct {
    file string
    }

    func (e errNotFound) Error() string {
    return fmt.Sprintf("file %q not found", e.file)
    }

    func IsNotFoundError(err error) bool {
    _, ok := err.(errNotFound)
    return ok
    }

    func Open(file string) error {
    return errNotFound{file: file}
    }

    // package bar

    if err := foo.Open("foo"); err != nil {
    if foo.IsNotFoundError(err) {
    // handle
    } else {
    panic("unknown error")
    }
    }

    1. 错误包装

    对于如何处理错误包装,并且更有利于处理错误的传播

  • 如果没有要添加的其他上下文,并且您想要维护原始错误类型,则返回原始错误。
  • 添加上下文,使用 "pkg/errors".Wrap 以便错误消息提供更多上下文 ,"pkg/errors".Cause 可用于提取原始错误。
  • 使用 fmt.Errorf ,如果调用者不需要检测或处理的特定错误情况。 specific error case.

    在可能,需要的地方添加更加精确的描述,以使您获得诸如“调用服务 foo:连接被拒绝”之类的更有用的错误,而不是诸如“连接被拒绝”之类的模糊错误。

    在将上下文添加到返回的错误时,请避免使用“failed to”之类的短语来保持上下文简洁,这些短语会陈述明显的内容,并随着错误在堆栈中的渗透而逐渐堆积:

    但是,一旦将错误发送到另一个系统,就应该明确消息是错误消息(例如使用err标记,或在日志中以”Failed”为前缀)。

    另请参见 Don't just check errors, handle them gracefully. 不要只是检查错误,要优雅地处理错误

  1. 处理断言类型

    断言时如果是单值返回形式,当类型错误的时候会直接panic,所以要使用"comma ok"的模式

  2. 不要panic

    在生产环境中运行的代码必须避免出现 panic。panic 是 cascading failures 级联失败的主要根源 。如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。

  3. 使用atomic/go.uber.org

性能

  1. 优先使用strconv,不是fmt

    将原语转换为字符串或从字符串转换时,strconv速度比fmt快。

  2. 避免从字符串到字节的转换

    不要反复从固定字符串创建字节 slice。相反,请执行一次转换并捕获结果。

规范

  1. 相似的声明要放到一组

  2. import组内的包导入顺序

  • 标准库
  • 其他一切

  1. 包名

    命名包名时,按下面规则选择一个名称:

  • 全部小写。没有大写或下划线。
  • 大多数使用命名导入的情况下,不需要重命名。
  • 简短而简洁。请记住,在每个使用的地方都完整标识了该名称。
  • 不用复数。例如net/url,而不是net/urls
  • 不要用“common”,“util”,“shared”或“lib”。这些是不好的,信息量不足的名称。

    另请参阅 Package NamesGo 包样式指南.

  1. 函数名

    我们遵循 Go 社区关于使用 MixedCaps 作为函数名 的约定。有一个例外,为了对相关的测试用例进行分组,函数名可能包含下划线,如:TestMyFunction_WhatIsBeingTested.

  2. 导入别名

    如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名。

  3. 函数分组和顺序

  • 函数应按粗略的调用顺序排序。
  • 同一文件中的函数应按接收者分组。

    因此,导出的函数应先出现在文件中,放在struct, const, var定义的后面。

    在定义类型之后,但在接收者的其余方法之前,可能会出现一个 newXYZ()/NewXYZ()

    由于函数是按接收者分组的,因此普通工具函数应在文件末尾出现。

  1. 减少嵌套

    代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码的代码量。

  2. 不必要的else

    如果在 if 的两个分支中都设置了变量,则可以将其替换为单个 if。

  3. 顶层变量声明

  4. 对于未导出的顶级常量和变量,使用_作为前缀

    例外:未导出的错误值,应以err开头。

    基本依据:顶级变量和常量具有包范围作用域。使用通用名称可能很容易在其他文件中意外使用错误的值。

  5. 结构体中的嵌入

    嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。

  6. 使用字段名初始化结构体,初始化结构体时应该指定字段名称。

    例外:如果有 3 个或更少的字段,则可以在测试表中省略字段名称。

  7. 本地变量声明

    如果将变量明确设置为某个值,则应使用短变量声明形式 (:=)。

    BadGood
    var s = "foo"s := "foo"

    但是,在某些情况下,var 使用关键字时默认值会更清晰。例如,声明空切片。

  8. nil是一个有效的切片

    nil 是一个有效的长度为 0 的 slice,这意味着

    1. 不应明确返回长度为零的切片。应该返回nil 来代替。

    2. 要检查切片是否为空,请始终使用len(s) == 0。而非 nil

    3. 零值切片(用var声明的切片)可立即使用,无需调用make()创建。

  9. 缩小变量作用域

    如果有可能,尽量缩小变量作用范围。除非它与 减少嵌套的规则冲突。(将声明放在if里面)

    如果需要在 if 之外使用函数调用的结果,则不应尝试缩小范围。

  10. 避免参量语义不明确

    函数调用中的意义不明确的参数可能会损害可读性。当参数名称的含义不明显时,请为参数添加 C 样式注释 (/* ... */)

    BadGood
    // func printInfo(name string, isLocal, done bool) printInfo("foo", true, true)// func printInfo(name string, isLocal, done bool) printInfo("foo", true /* isLocal */, true /* done */)

    对于上面的示例代码,还有一种更好的处理方式是将上面的 bool 类型换成自定义类型。将来,该参数可以支持不仅仅局限于两个状态(true/false)。

  11. 使用原始字符串字面值,避免转义

    Go 支持使用 原始字符串字面值,也就是 " ` " 来表示原生字符串,在需要转义的场景下,我们应该尽量使用这种方案来替换。

    可以跨越多行并包含引号。使用这些字符串可以避免更难阅读的手工转义的字符串。

  12. 初始化Struct引用

    在初始化结构引用时,请使用&T{}代替new(T),以使其与结构体初始化一致。

  13. 字符串string format

    如果你为Printf-style 函数声明格式字符串,请将格式化字符串放在外面,并将其设置为const常量。

    这有助于go vet对格式字符串执行静态分析。

  14. 命名Printf样式的函数

    声明Printf-style 函数时,请确保go vet可以检测到它并检查格式字符串。

    这意味着您应尽可能使用预定义的Printf-style 函数名称。go vet将默认检查这些。有关更多信息,请参见 Printf 系列

    如果不能使用预定义的名称,请以 f 结束选择的名称:Wrapf,而不是Wrapgo vet可以要求检查特定的 Printf 样式名称,但名称必须以f结尾。

    另请参阅 go vet: Printf family check.

编程模式

  1. 表驱动测试

    当测试逻辑是重复的时候,通过 subtests 使用 table 驱动的方式编写 case 代码看上去会更简洁。

    错误:

    正确:

    很明显,使用 test table 的方式在代码逻辑扩展的时候,比如新增 test case,都会显得更加的清晰。

    我们遵循这样的约定:将结构体切片称为tests。 每个测试用例称为tt。此外,我们鼓励使用givewant前缀说明每个测试用例的输入和输出值。

  2. 功能选项

    功能选项是一种模式,您可以在其中声明一个不透明 Option 类型,该类型在某些内部结构中记录信息。您接受这些选项的可变编号,并根据内部结构上的选项记录的全部信息采取行动。

    将此模式用于您需要扩展的构造函数和其他公共 API 中的可选参数,尤其是在这些功能上已经具有三个或更多参数的情况下。

    错误:

    正确:

总结

代码写的不够多,要多写点了,继续加油吧。