跳转至

Golang编码规范之指导原则05

Golang编码规范之指导原则05

一 避免使用内置名称

Go 语言规范 概述了几个内置的, 不应在 Go 项目中使用的 预先声明的标识符

根据上下文的不同,将这些标识符作为名称重复使用, 将在当前作用域(或任何嵌套作用域)中隐藏原始标识符,或者混淆代码。 在最好的情况下,编译器会报错;在最坏的情况下,这样的代码可能会引入潜在的、难以恢复的错误。

  • Bad
var error string
// `error` 作用域隐式覆盖

// or

func handleErrorMessage(error string) {
    // `error` 作用域隐式覆盖
}

type Foo struct {
    // 虽然这些字段在技术上不构成阴影,但`error`或`string`字符串的重映射现在是不明确的。
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` 和 `f.error` 在视觉上是相似的
    return f.error
}

func (f Foo) String() string {
    // `string` and `f.string` 在视觉上是相似的
    return f.string
}
  • Good
var errorMessage string
// `error` 指向内置的非覆盖

// or

func handleErrorMessage(msg string) {
    // `error` 指向内置的非覆盖
}


type Foo struct {
    // `error` and `string` 现在是明确的。
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

注意,编译器在使用预先分隔的标识符时不会生成错误, 但是诸如go vet之类的工具会正确地指出这些和其他情况下的隐式问题。

二 避免使用 init()

尽可能避免使用init()。当init()是不可避免或可取的,代码应先尝试:

  1. 无论程序环境或调用如何,都要完全确定。
  2. 避免依赖于其他init()函数的顺序或副作用。虽然init()顺序是明确的,但代码可以更改, 因此init()函数之间的关系可能会使代码变得脆弱和容易出错。
  3. 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。
  4. 避免I/O,包括文件系统、网络和系统调用。

不能满足这些要求的代码可能属于要作为main()调用的一部分(或程序生命周期中的其他地方), 或者作为main()本身的一部分写入。特别是,打算由其他程序使用的库应该特别注意完全确定性, 而不是执行“init magic”

  • Bad
type Foo struct {
    // ...
}
var _defaultFoo Foo
func init() {
    _defaultFoo = Foo{
        // ...
    }
}

type Config struct {
    // ...
}
var _config Config
func init() {
    // Bad: 基于当前目录
    cwd, _ := os.Getwd()
    // Bad: I/O
    raw, _ := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}
  • Good
var _defaultFoo = Foo{
    // ...
}
// or,为了更好的可测试性:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}

type Config struct {
    // ...
}
func loadConfig() Config {
    cwd, err := os.Getwd()
    // handle err
    raw, err := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // handle err
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}

考虑到上述情况,在某些情况下,init()可能更可取或是必要的,可能包括:

  • 不能表示为单个赋值的复杂表达式。
  • 可插入的钩子,如database/sql、编码类型注册表等。
  • Google Cloud Functions 和其他形式的确定性预计算的优化。

三 追加时优先指定切片容量

追加时优先指定切片容量

在尽可能的情况下,在初始化要追加的切片时为make()提供一个容量值。

  • Bad
for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

BenchmarkBad-4    100000000    2.48s
  • Good
for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

BenchmarkGood-4   100000000    0.21s

四 主函数退出方式 (Exit)

4.1 主函数退出

Go 程序使用 os.Exit 或者 log.Fatal* 立即退出 (使用panic不是退出程序的好方法,请 不要使用 panic。)

仅在main() 中调用其中一个 os.Exit 或者 log.Fatal*。所有其他函数应将错误返回到信号失败中。

  • Bad
func main() {
  body := readFile(path)
  fmt.Println(body)
}
func readFile(path string) string {
  f, err := os.Open(path)
  if err != nil {
    log.Fatal(err)
  }
  b, err := ioutil.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  return string(b)
}
  • Good
func main() {
  body, err := readFile(path)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(body)
}
func readFile(path string) (string, error) {
  f, err := os.Open(path)
  if err != nil {
    return "", err
  }
  b, err := ioutil.ReadAll(f)
  if err != nil {
    return "", err
  }
  return string(b), nil
}

原则上:退出的具有多种功能的程序存在一些问题:

  • 不明显的控制流:任何函数都可以退出程序,因此很难对控制流进行推理。
  • 难以测试:退出程序的函数也将退出调用它的测试。这使得函数很难测试,并引入了跳过 go test 尚未运行的其他测试的风险。
  • 跳过清理:当函数退出程序时,会跳过已经进入defer队列里的函数调用。这增加了跳过重要清理任务的风险。

4.2 一次性退出

如果可能的话,你的main()函数中 最多一次 调用 os.Exit或者log.Fatal。如果有多个错误场景停止程序执行,请将该逻辑放在单独的函数下并从中返回错误。 这会缩短 main() 函数,并将所有关键业务逻辑放入一个单独的、可测试的函数中。

  • Bad
package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("missing file")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // 如果我们调用 log.Fatal 在这条线之后
  // f.Close 将会被执行。
  b, err := ioutil.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}
  • Good
package main
func main() {
  if err := run(); err != nil {
    log.Fatal(err)
  }
}
func run() error {
  args := os.Args[1:]
  if len(args) != 1 {
    return errors.New("missing file")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    return err
  }
  defer f.Close()
  b, err := ioutil.ReadAll(f)
  if err != nil {
    return err
  }
  // ...
}