《Go 语言编码规范》学习笔记

  1. 所有的提交代码都应该通过 golint 和 go vet 检测

    建议在代码编辑器上面做如下设置:

    • 保存的时候运行 goimports
    • 使用 golint 和 go vet 去做错误检测。
  2. 零值Mutex是有效的

    零值 sync.Mutex 和 sync.RWMutex 是有效的。所以指向 mutex 的指针基本是不必要的。

    Bad

    mu := new(sync.Mutex)
    mu.Lock()

    Good

    var mu sync.Mutex
    mu.Lock()
  3. 在边界处拷贝Slices和Maps

    slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。

    接收Slices和Maps

    请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。

    Bad

    func (d *Driver) SetTrips(trips []Trip) {
      d.trips = trips
    }
    
    trips := ...
    d1.SetTrips(trips)
    
    // 你是要修改 d1.trips 吗?
    trips[0] = ...

    Good

    func (d *Driver) SetTrips(trips []Trip) {
      d.trips = make([]Trip, len(trips))
      copy(d.trips, trips)
    }
    
    trips := ...
    d1.SetTrips(trips)
    
    // 这里我们修改 trips[0],但不会影响到 d1.trips
    trips[0] = ...

    返回slices和maps

    同样,请注意用户对暴露内部状态的 map 或 slice 的修改。

    Bad

    type Stats struct {
      mu sync.Mutex
    
      counters map[string]int
    }
    
    // Snapshot 返回当前状态。
    func (s *Stats) Snapshot() map[string]int {
      s.mu.Lock()
      defer s.mu.Unlock()
    
      return s.counters
    }
    
    // snapshot 不再受互斥锁保护
    // 因此对 snapshot 的任何访问都将受到数据竞争的影响
    // 影响 stats.counters
    snapshot := stats.Snapshot()

    Good

    type Stats struct {
      mu sync.Mutex
    
      counters map[string]int
    }
    
    func (s *Stats) Snapshot() map[string]int {
      s.mu.Lock()
      defer s.mu.Unlock()
    
      result := make(map[string]int, len(s.counters))
      for k, v := range s.counters {
        result[k] = v
      }
      return result
    }
    
    // snapshot 现在是一个拷贝
    snapshot := stats.Snapshot()
  4. 使用time处理时间

    使用 time.Time 表达瞬时时间

    在处理时间的瞬间时使用 time.Time,在比较、添加或减去时间时使用 time.Time 中的方法。

    Bad

    func isActive(now, start, stop int) bool {
      return start <= now && now < stop
    }

    Good

    func isActive(now, start, stop time.Time) bool {
      return (start.Before(now) || start.Equal(now)) && now.Before(stop)
    }

    使用 time.Duration 表达时间段

    Bad

    func poll(delay int) {
      for {
        // ...
        time.Sleep(time.Duration(delay) * time.Millisecond)
      }
    }
    poll(10) // 是几秒钟还是几毫秒?

    Good

    func poll(delay time.Duration) {
      for {
        // ...
        time.Sleep(delay)
      }
    }
    poll(10*time.Second)
  5. 错误类型

    使用 errors.New 表示带有静态字符串的错误。 如果调用者需要匹配并处理此错误,则将此错误导出为变量以支持将其与 errors.Is 匹配。

    无错误匹配

    // package foo
    
    func Open() error {
      return errors.New("could not open")
    }
    
    // package bar
    
    if err := foo.Open(); err != nil {
      // Can't handle the error.
      panic("unknown error")
    }

    有错误匹配

    // package foo
    
    var ErrCouldNotOpen = errors.New("could not open")
    
    func Open() error {
      return ErrCouldNotOpen
    }
    
    // package bar
    
    if err := foo.Open(); err != nil {
      if errors.Is(err, foo.ErrCouldNotOpen) {
        // handle the error
      } else {
        panic("unknown error")
      }
    }

    对于动态字符串的错误, 如果调用者不需要匹配它,则使用 fmt.Errorf, 如果调用者确实需要匹配它,则自定义 error

    无错误匹配

    // package foo
    
    func Open(file string) error {
      return fmt.Errorf("file %q not found", file)
    }
    
    // package bar
    
    if err := foo.Open("testfile.txt"); err != nil {
      // Can't handle the error.
      panic("unknown error")
    }

    有错误匹配

    // package foo
    
    type NotFoundError struct {
      File string
    }
    
    func (e *NotFoundError) Error() string {
      return fmt.Sprintf("file %q not found", e.File)
    }
    
    func Open(file string) error {
      return &NotFoundError{File: file}
    }
    
    
    // package bar
    
    if err := foo.Open("testfile.txt"); err != nil {
      var notFound *NotFoundError
      if errors.As(err, &notFound) {
        // handle the error
      } else {
        panic("unknown error")
      }
    }
  6. 初始化切片和map时指定容量

    make([]T, length, capacity)
    make(map[T1]T2, hint)

    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)
      }
    }
    

      

    BenchmarkBad-4    100000000    2.48s

    同理:使用map的时候也应该指定容量,减少动态扩容,导致的开销

  7. 在序列化结构中使用字段标记

    任何序列化到JSON、YAML、, 或其他支持基于标记的字段命名的格式应使用相关标记进行注释。

    Bad

    type Stock struct {
      Price int
      Name  string
    }
    bytes, err := json.Marshal(Stock{
      Price: 137,
      Name:  "UBER",
    })

    Good

    type Stock struct {
      Price int    `json:"price"`
      Name  string `json:"name"`
      // Safe to rename Name to Symbol.
    }
    bytes, err := json.Marshal(Stock{
      Price: 137,
      Name:  "UBER",
    })
  8. 优先使用strconv而不是fmt

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

    Bad

    for i := 0; i < b.N; i++ {
      s := fmt.Sprint(rand.Int())
    }

    Good

    for i := 0; i < b.N; i++ {
      s := strconv.Itoa(rand.Int())
    
  9. 避免字符串到字节的转换

    Bad

    for i := 0; i < b.N; i++ {
      w.Write([]byte("Hello world"))
    }

    Good

    data := []byte("Hello world")
    for i := 0; i < b.N; i++ {
      w.Write(data)
    }
  10. 避免过长的行

  11. 相似的声明放在一起

    Bad

    const a = 1
    const b = 2
    
    var a = 1
    var b = 2
    
    type Area float64
    type Volume float64

    Good

    const (
      a = 1
      b = 2
    )
    
    var (
      a = 1
      b = 2
    )
    
    type (
      Area float64
      Volume float64
    )
  12. 包名命名规范

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

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

    另请参阅 Go 包命名规则 和 Go 包样式指南.

  13. 函数名命名规范

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

  14. 函数分组与顺序

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

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

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

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

    Bad

    func (s *something) Cost() {
      return calcCost(s.weights)
    }
    
    type something struct{ ... }
    
    func calcCost(n []int) int {...}
    
    func (s *something) Stop() {...}
    
    func newSomething() *something {
        return &something{}
    }  

    Good


    type something struct{ ... }
    
    func newSomething() *something {
        return &something{}
    }
    
    func (s *something) Cost() {
      return calcCost(s.weights)
    }
    
    func (s *something) Stop() {...}
    
    func calcCost(n []int) int {...}
    

      

  15. 减少嵌套

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

    Bad

    for _, v := range data {
      if v.F1 == 1 {
        v = process(v)
        if err := v.Call(); err == nil {
          v.Send()
        } else {
          return err
        }
      } else {
        log.Printf("Invalid v: %v", v)
      }
    }

    Good

    for _, v := range data {
      if v.F1 != 1 {
        log.Printf("Invalid v: %v", v)
        continue
      }
    
      v = process(v)
      if err := v.Call(); err != nil {
        return err
      }
      v.Send()
    }
  16. 不必要的else

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

    Bad

    var a int
    if b {
      a = 100
    } else {
      a = 10
    }

    Good

    a := 10
    if b {
      a = 100
    }
  17. nil是一个有效的slice

    nil 是一个有效的长度为 0 的 slice,这意味着,您不应明确返回长度为零的切片。应该返回nil 来代替。

    Bad

    if x == "" {
      return []int{}
    }

    Good

    if x == "" {
      return nil
    }

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

    Bad

    func isEmpty(s []string) bool {
      return s == nil
    }

    Good

    func isEmpty(s []string) bool {
      return len(s) == 0
    }

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

    Bad

    nums := []int{}
    // or, nums := make([]int)
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }

    Good

    var nums []int
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }

    记住,虽然 nil 切片是有效的切片,但它不等于长度为 0 的切片(一个为 nil,另一个不是),并且在不同的情况下(例如序列化),这两个切片的处理方式可能不同。

  18. 缩小变量作用域

    如果有可能,尽量缩小变量作用范围。除非它与 减少嵌套的规则冲突。

    Bad

    err := os.WriteFile(name, data, 0644)
    if err != nil {
     return err
    }

    Good

    if err := os.WriteFile(name, data, 0644); err != nil {
     return err
    }

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

    Bad

    if data, err := os.ReadFile(name); err == nil {
      err = cfg.Decode(data)
      if err != nil {
        return err
      }
    
      fmt.Println(cfg)
      return nil
    } else {
      return err
    }

    Good

    data, err := os.ReadFile(name)
    if err != nil {
       return err
    }
    
    if err := cfg.Decode(data); err != nil {
      return err
    }
    
    fmt.Println(cfg)
    return nil

参考:

  1. https://learnku.com/go/wikis/38426
  2. 《Go 语言编码规范》中文翻译地址:https://github.com/xxjwxc/uber_go_guide_cn
posted @ 2024-02-29 19:47  cs_wu  阅读(4)  评论(0编辑  收藏  举报