优秀的编程知识分享平台

网站首页 > 技术文章 正文

GO 编程:常见的坑

nanyue 2025-02-27 16:09:04 技术文章 12 ℃

这里列举的Go语言常见坑都是符合Go语言语法的,可以正常的编译,但是可能是运行结果错误,或者是有资源泄漏的风险。

可变参数是空接口类型

当参数的可变参数是空接口类型时,传入空接口的切片时需要注意参数展开的问题。

func main() {
    var a = []interface{}{1, 2, 3}

    fmt.Println(a)
    fmt.Println(a...)
}

不管是否展开,编译器都无法发现错误,但是输出是不同的:

[1 2 3]
1 2 3

数组是值传递

在函数调用参数中,数组是值传递,无法通过修改数组类型的参数返回结果。

func main() {
    x := [3]int{1, 2, 3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr)
    }(x)

    fmt.Println(x)
}

必要时需要使用切片。

map遍历是顺序不固定

map是一种hash表实现,每次遍历的顺序都可能不一样。

func main() {
    m := map[string]string{
        "1": "1",
        "2": "2",
        "3": "3",
    }

    for k, v := range m {
        println(k, v)
    }
}

返回值被屏蔽

在局部作用域中,命名的返回值内同名的局部变量屏蔽:

func Foo() (err error) {
    if err := Bar(); err != nil {
        return
    }
    return
}

recover必须在defer函数中运行

recover捕获的是祖父级调用时的异常,直接调用时无效:

func main() {
    recover()
    panic(1)
}

直接defer调用也是无效:

func main() {
    defer recover()
    panic(1)
}

defer调用时多层嵌套依然无效:

func main() {
    defer func() {
        func() { recover() }()
    }()
    panic(1)
}

必须在defer函数中直接调用才有效:

func main() {
    defer func() {
        recover()
    }()
    panic(1)
}

main函数提前退出

后台Goroutine无法保证完成任务。

func main() {
    go println("hello")
}

通过Sleep来回避并发中的问题

休眠并不能保证输出完整的字符串:

func main() {
    go println("hello")
    time.Sleep(time.Second)
}

类似的还有通过插入调度语句:

func main() {
    go println("hello")
    runtime.Gosched()
}

独占CPU导致其它Goroutine饿死

Goroutine是协作式抢占调度,Goroutine本身不会主动放弃CPU:

func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
    }()

    for {} // 占用CPU
}

解决的方法是在for循环加入runtime.Gosched()调度函数:

func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
    }()

    for {
        runtime.Gosched()
    }
}

或者是通过阻塞的方式避免CPU占用:

func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
        os.Exit(0)
    }()

    select{}
}

不同Goroutine之间不满足顺序一致性内存模型

因为在不同的Goroutine,main函数中无法保证能打印出hello, world:

var msg string
var done bool

func setup() {
    msg = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {
    }
    println(msg)
}

解决的办法是用显式同步:

var msg string
var done = make(chan bool)

func setup() {
    msg = "hello, world"
    done <- true
}

func main() {
    go setup()
    <-done
    println(msg)
}

msg的写入是在channel发送之前,所以能保证打印hello, world

闭包错误引用同一个变量

func main() {
    for i := 0; i < 5; i++ {
        defer func() {
            println(i)
        }()
    }
}

改进的方法是在每轮迭代中生成一个局部变量:

func main() {
    for i := 0; i < 5; i++ {
        i := i
        defer func() {
            println(i)
        }()
    }
}

或者是通过函数参数传入:

func main() {
    for i := 0; i < 5; i++ {
        defer func(i int) {
            println(i)
        }(i)
    }
}

在循环内部执行defer语句

defer在函数退出时才能执行,在for执行defer会导致资源延迟释放:

func main() {
    for i := 0; i < 5; i++ {
        f, err := os.Open("/path/to/file")
        if err != nil {
            log.Fatal(err)
        }
        defer f.Close()
    }
}

解决的方法可以在for中构造一个局部函数,在局部函数内部执行defer:

func main() {
    for i := 0; i < 5; i++ {
        func() {
            f, err := os.Open("/path/to/file")
            if err != nil {
                log.Fatal(err)
            }
            defer f.Close()
        }()
    }
}

切片会导致整个底层数组被锁定

切片会导致整个底层数组被锁定,底层数组无法释放内存。如果底层数组较大会对内存产生很大的压力。

func main() {
    headerMap := make(map[string][]byte)

    for i := 0; i < 5; i++ {
        name := "/path/to/file"
        data, err := ioutil.ReadFile(name)
        if err != nil {
            log.Fatal(err)
        }
        headerMap[name] = data[:1]
    }

    // do some thing
}

解决的方法是将结果克隆一份,这样可以释放底层的数组:

func main() {
    headerMap := make(map[string][]byte)

    for i := 0; i < 5; i++ {
        name := "/path/to/file"
        data, err := ioutil.ReadFile(name)
        if err != nil {
            log.Fatal(err)
        }
        headerMap[name] = append([]byte{}, data[:1]...)
    }

    // do some thing
}

空指针和空接口不等价

比如返回了一个错误指针,但是并不是空的error接口:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Will always return a non-nil error.
}

内存地址会变化

Go语言中对象的地址可能发生变化,因此指针不能从其它非指针类型的值生成:

func main() {
    var x int = 42
    var p uintptr = uintptr(unsafe.Pointer(&x))

    runtime.GC()
    var px *int = (*int)(unsafe.Pointer(p))
    println(*px)
}

当内存发送变化的时候,相关的指针会同步更新,但是非指针类型的uintptr不会做同步更新。

同理CGO中也不能保存Go对象地址。

Goroutine泄露

Go语言是带内存自动回收的特性,因此内存一般不会泄漏。但是Goroutine确存在泄漏的情况,同时泄漏的Goroutine引用的内存同样无法被回收。

func main() {
    ch := func() <-chan int {
        ch := make(chan int)
        go func() {
            for i := 0; ; i++ {
                ch <- i
            }
        } ()
        return ch
    }()

    for v := range ch {
        fmt.Println(v)
        if v == 5 {
            break
        }
    }
}

上面的程序中后台Goroutine向管道输入自然数序列,main函数中输出序列。但是当break跳出for循环的时候,后台Goroutine就处于无法被回收的状态了。

我们可以通过context包来避免这个问题:

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    ch := func(ctx context.Context) <-chan int {
        ch := make(chan int)
        go func() {
            for i := 0; ; i++ {
                select {
                case <- ctx.Done():
                    return
                case ch <- i:
                }
            }
        } ()
        return ch
    }(ctx)

    for v := range ch {
        fmt.Println(v)
        if v == 5 {
            cancel()
            break
        }
    }
}

当main函数在break跳出循环时,通过调用cancel()来通知后台Goroutine退出,这样就避免了Goroutine的泄漏。

Go 箴言

  • 不要通过共享内存进行通信,通过通信共享内存
  • 并发不是并行
  • 管道用于协调;互斥量(锁)用于同步
  • 接口越大,抽象就越弱
  • 利用好零值
  • 空接口 interface{} 没有任何类型约束
  • Gofmt 的风格不是人们最喜欢的,但 gofmt 是每个人的最爱
  • 允许一点点重复比引入一点点依赖更好
  • 系统调用必须始终使用构建标记进行保护
  • 必须始终使用构建标记保护 Cgo
  • Cgo 不是 Go
  • 使用标准库的 unsafe 包,不能保证能如期运行
  • 清晰比聪明更好
  • 反射永远不清晰
  • 错误是值
  • 不要只检查错误,还要优雅地处理它们
  • 设计架构,命名组件,(文档)记录细节
  • 文档是供用户使用的
  • 不要(在生产环境)使用 panic()

Author: Rob Pike
See more: https://go-proverbs.github.io/

Go 之禅

  • 每个 package 实现单一的目的
  • 显式处理错误
  • 尽早返回,而不是使用深嵌套
  • 让调用者处理并发(带来的问题)
  • 在启动一个 goroutine 时,需要知道何时它会停止
  • 避免 package 级别的状态
  • 简单很重要
  • 编写测试以锁定 package API 的行为
  • 如果你觉得慢,先编写 benchmark 来证明
  • 适度是一种美德
  • 可维护性

Author: Dave Cheney
See more: https://the-zen-of-go.netlify.com/

代码

使用go fmt格式化

让团队一起使用官方的 Go 格式工具,不要重新发明轮子。
尝试减少代码复杂度。 这将帮助所有人使代码易于阅读。

多个 if 语句可以折叠成 switch

// NOT BAD
if foo() {
    // ...
} else if bar == baz {
    // ...
} else {
    // ...
}

// BETTER
switch {
case foo():
    // ...
case bar == baz:
    // ...
default:
    // ...
}

用chan struct{}来传递信号,chan bool表达的不够清楚

当你在结构中看到 chan bool 的定义时,有时不容易理解如何使用该值,例如:

type Service struct {
    deleteCh chan bool // what does this bool mean? 
}

但是我们可以将其改为明确的 chan struct {} 来使其更清楚:我们不在乎值(它始终是 struct {}),我们关心可能发生的事件,例如:

type Service struct {
    deleteCh chan struct{} // ok, if event than delete something.
}

30 * time.Second比time.Duration(30) * time.Second更好

你不需要将无类型的常量包装成类型,编译器会找出来。
另外最好将常量移到第一位:

// BAD
delay := time.Second * 60 * 24 * 60

// VERY BAD
delay := 60 * time.Second * 60 * 24

// GOOD
delay := 24 * 60 * 60 * time.Second

用time.Duration代替int64+ 变量名

// BAD
var delayMillis int64 = 15000

// GOOD
var delay time.Duration = 15 * time.Second

按类型分组const声明,按逻辑和/或类型分组var

// BAD
const (
    foo = 1
    bar = 2
    message = "warn message"
)

// MOSTLY BAD
const foo = 1
const bar = 2
const message = "warn message"

// GOOD
const (
    foo = 1
    bar = 2
)

const message = "warn message"

这个模式也适用于 var

  • 每个阻塞或者 IO 函数操作应该是可取消的或者至少是可超时的
  • 为整型常量值实现 Stringer 接口https://godoc.org/golang.org/x/tools/cmd/stringer
  • 检查 defer 中的错误
  defer func() {
      err := ocp.Close()
      if err != nil {
          rerr = err
      }
  }()
  • 不要在 checkErr 函数中使用 panic()os.Exit()
  • 仅仅在很特殊情况下才使用 panic, 你必须要去处理 error
  • 不要给枚举使用别名,因为这打破了类型安全https://play.golang.org/p/MGbeDwtXN3
  package main
  type Status = int
  type Format = int // remove `=` to have type safety

  const A Status = 1
  const B Format = 1

  func main() {
      println(A == B)
  }
  • 如果你想省略返回参数,你最好表示出来
    • _ = f()f() 更好
  • 我们用 a := []T{} 来简单初始化 slice
  • 用 range 循环来进行数组或 slice 的迭代
    • for _, c := range a[3:7] {...}for i := 3; i < 7; i++ {...} 更好
  • 多行字符串用反引号(`)
  • _ 来跳过不用的参数
  func f(a int, _ string) {}
  • 如果你要比较时间戳,请使用 time.Beforetime.After ,不要使用 time.Sub 来获得 duration (持续时间),然后检查它的值。
  • 带有上下文的函数第一个参数名为 ctx,形如:func foo(ctx Context, ...)
  • 几个相同类型的参数定义可以用简短的方式来进行
  func f(a int, b int, s string, p string)
  func f(a, b int, s, p string)
  • 一个 slice 的零值是 nilhttps://play.golang.org/p/pNT0d_Bunqvar s []int fmt.Println(s, len(s), cap(s)) if s == nil { fmt.Println("nil!") } // Output: // [] 0 0 // nil!https://play.golang.org/p/meTInNyxtk
  var a []string
  b := []string{}

  fmt.Println(reflect.DeepEqual(a, []string{}))
  fmt.Println(reflect.DeepEqual(b, []string{}))
  // Output:
  // false
  // true
  • 不要将枚举类型与 <, >, <=>= 进行比较使用确定的值,不要像下面这样做:
  value := reflect.ValueOf(object)
  kind := value.Kind()
  if kind >= reflect.Chan && kind <= reflect.Slice {
    // ...
  }
  • %+v 来打印数据的比较全的信息
  • 注意空结构 struct{}, 看 issue: https://github.com/golang/go/issues/23440more: https://play.golang.org/p/9C0puRUstrP
  func f1() {
    var a, b struct{}
    print(&a, "\n", &b, "\n") // Prints same address
    fmt.Println(&a == &b)     // Comparison returns false
  }

  func f2() {
    var a, b struct{}
    fmt.Printf("%p\n%p\n", &a, &b) // Again, same address
    fmt.Println(&a == &b)          // ...but the comparison returns true
  }
  • 包装错误: http://github.com/pkg/errors
    • 例如: errors.Wrap(err, "additional message to a given error")
  • 在 Go 里面要小心使用 range:
    • for i := range a and for i, v := range &a ,都不是 a 的副本
    • 但是 for i, v := range a 里面的就是 a 的副本
    • 更多: https://play.golang.org/p/4b181zkB1O
  • 从 map 读取一个不存在的 key 将不会 panic
    • value := map["no_key"] 将得到一个 0 值
    • value, ok := map["no_key"] 更好
  • 不要使用原始参数进行文件操作
    • 而不是一个八进制参数 os.MkdirAll(root, 0700)
    • 使用此类型的预定义常量 os.FileMode
  • 不要忘记为 iota 指定一种类型
    • https://play.golang.org/p/mZZdMaI92cI
    const (
      _ = iota
      testvar         // testvar 将是 int 类型
    )

vs

    type myType int
    const (
      _ myType = iota
      testvar         // testvar 将是 myType 类型
    )

不要在你不拥有的结构上使用encoding/gob

在某些时候,结构可能会改变,而你可能会错过这一点。因此,这可能会导致很难找到 bug。

不要依赖于计算顺序,特别是在 return 语句中。

  // BAD
  return res, json.Unmarshal(b, &res)

  // GOOD
  err := json.Unmarshal(b, &res)
  return res, err

防止结构体字段用纯值方式初始化,添加_ struct {}字段:

type Point struct {
  X, Y float64
  _    struct{} // to prevent unkeyed literals
}

对于 Point {X:1,Y:1} 都可以,但是对于 Point {1,1} 则会出现编译错误:

./file.go:1:11: too few values in Point literal

当在你所有的结构体中添加了 _ struct{} 后,使用 go vet 命令进行检查,(原来声明的方式)就会提示没有足够的参数。

为了防止结构比较,添加func类型的空字段

  type Point struct {
    _ [0]func() // unexported, zero-width non-comparable field
    X, Y float64
  }

http.HandlerFunc比http.Handler更好

http.HandlerFunc 你仅需要一个 func,http.Handler 需要一个类型。

移动defer到顶部

这可以提高代码可读性并明确函数结束时调用了什么。

JavaScript 解析整数为浮点数并且你的 int64 可能溢出

json:"id,string" 代替

type Request struct {
  ID int64 `json:"id,string"`
}

并发

  • 以线程安全的方式创建单例(只创建一次)的最好选择是 sync.Once不要用 flags, mutexes, channels or atomics
  • 永远不要使用 select{}, 省略通道, 等待信号
  • 不要关闭一个发送(写入)管道,应该由创建者关闭往一个关闭的 channel 写数据会引起 panic
  • math/rand 中的 func NewSource(seed int64) Source 不是并发安全的,默认的 lockedSource 是并发安全的, see issue: https://github.com/golang/go/issues/3611更多: https://golang.org/pkg/math/rand/
  • 当你需要一个自定义类型的 atomic 值时,可以使用 atomic.Value

性能

  • 不要省略 defer在大多数情况下 200ns 加速可以忽略不计
  • 总是关闭 http body defer r.Body.Close()除非你需要泄露 goroutine
  • 过滤但不分配新内存
  b := a[:0]
  for _, x := range a {
      if f(x) {
        b = append(b, x)
      }
  }

为了帮助编译器删除绑定检查,请参见此模式_ = b [7]

  • time.Time 有指针字段 time.Location 并且这对 go GC 不好只有使用了大量的 time.Time 才(对性能)有意义,否则用 timestamp 代替
  • regexp.MustCompileregexp.Compile 更好在大多数情况下,你的正则表达式是不可变的,所以你最好在 func init 中初始化它
  • 请勿在你的热点代码中过度使用 fmt.Sprintf. 由于维护接口的缓冲池和动态调度,它是很昂贵的。如果你正在使用 fmt.Sprintf("%s%s", var1, var2), 考虑使用简单的字符串连接。如果你正在使用 fmt.Sprintf("%x", var), 考虑使用 hex.EncodeToString or strconv.FormatInt(var, 16)
  • 如果你不需要用它,可以考虑丢弃它,例如io.Copy(ioutil.Discard, resp.Body)HTTP 客户端的传输不会重用连接,直到body被读完和关闭。
  res, _ := client.Do(req)
  io.Copy(ioutil.Discard, res.Body)
  defer res.Body.Close()
  • 不要在循环中使用 defer,否则会导致内存泄露因为这些 defer 会不断地填满你的栈(内存)
  • 不要忘记停止 ticker, 除非你需要泄露 channel
  ticker := time.NewTicker(1 * time.Second)
  defer ticker.Stop()
  • 用自定义的 marshaler 去加速 marshaler 过程但是在使用它之前要进行定制!例如:https://play.golang.org/p/SEm9Hvsi0r
  func (entry Entry) MarshalJSON() ([]byte, error) {
    buffer := bytes.NewBufferString("{")
    first := true
    for key, value := range entry {
        jsonValue, err := json.Marshal(value)
        if err != nil {
            return nil, err
        }
        if !first {
            buffer.WriteString(",")
        }
        first = false
        buffer.WriteString(key + ":" + string(jsonValue))
    }
    buffer.WriteString("}")
    return buffer.Bytes(), nil
  }
  • sync.Map 不是万能的,没有很强的理由就不要使用它。
    • 了解更多: https://github.com/golang/go/blob/master/src/sync/map.go#L12
  • sync.Pool 中分配内存存储非指针数据
    • 了解更多: https://github.com/dominikh/go-tools/blob/master/cmd/staticcheck/docs/checks/SA6002
  • 为了隐藏逃生分析的指针,你可以小心使用这个函数::
    • 来源: https://go-review.googlesource.com/c/go/+/86976
  // noescape hides a pointer from escape analysis.  noescape is
  // the identity function but escape analysis doesn't think the
  // output depends on the input. noescape is inlined and currently
  // compiles down to zero instructions.
  //go:nosplit
  func noescape(p unsafe.Pointer) unsafe.Pointer {
    x := uintptr(p)
    return unsafe.Pointer(x ^ 0)
  }
  • 对于最快的原子交换,你可以使用这个 m := (*map[int]int)(atomic.LoadPointer(&ptr))
  • 如果执行许多顺序读取或写入操作,请使用缓冲 I/O
    • 减少系统调用次数
  • 有 2 种方法清空一个 map:
    • 重用 map 内存 (但是也要注意 m 的回收)
  for k := range m {
    delete(m, k)
  }
  • 分配新的
  m = make(map[int]int)

模块

  • 如果你想在 CI 中测试 go.mod (和 go.sum)是否是最新 https://blog.urth.org/2019/08/13/testing-go-mod-tidiness-in-ci/

构建

  • 用这个命令 go build -ldflags="-s -w" ... 去掉你的二进制文件
  • 拆分构建不同版本的简单方法用 // +build integration 并且运行他们 go test -v --tags integration .
  • 最小的 Go Docker 镜像https://twitter.com/bbrodriges/status/873414658178396160CGO_ENABLED=0 go build -ldflags="-s -w" app.go && tar C app | docker import - myimage:latest
  • run go format on CI and compare diff这将确保一切都是生成的和承诺的
  • 用最新的 Go 运行 Travis-CI,用 travis 1了解更多:https://github.com/travis-ci/travis-build/blob/master/public/version-aliases/go.json
  • 检查代码格式是否有错误 diff -u <(echo -n) <(gofmt -d .)

测试

  • 测试名称 package_testpackage 要好
  • go test -short 允许减少要运行的测试数
  func TestSomething(t *testing.T) {
    if testing.Short() {
      t.Skip("skipping test in short mode.")
    }
  }
  • 根据系统架构跳过测试
  if runtime.GOARM == "arm" {
    t.Skip("this doesn't work under ARM")
  }
  • testing.AllocsPerRun 跟踪你的内存分配https://godoc.org/testing#AllocsPerRun
  • 多次运行你的基准测试可以避免噪音。go test -test.bench=. -count=20

工具

  • 快速替换 gofmt -w -l -r "panic(err) -> log.Error(err)" .
  • go list 允许找到所有直接和传递的依赖关系
    • go list -f '{{ .Imports }}' package
    • go list -f '{{ .Deps }}' package
  • 对于快速基准比较,我们有一个 benchstat 工具。
    • https://godoc.org/golang.org/x/perf/cmd/benchstat
  • go-critic linter 从这个文件中强制执行几条建议
  • go mod why -m 告诉我们为什么特定的模块在 go.mod 文件中。
  • GOGC=off go build ... 应该会加快构建速度 source
  • 内存分析器每 512KB 记录一次分配。你能通过 GODEBUG 环境变量增加比例,来查看你的文件的更多详细信息。
    • 来源:https://twitter.com/bboreham/status/1105036740253937664
  • go mod why -m 告诉我们为什么特定的模块是在 go.mod 文件中。

其他

  • dump goroutines https://stackoverflow.com/a/27398062/433041
  go func() {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGQUIT)
    buf := make([]byte, 1<<20)
    for {
      <-sigs
      stacklen := runtime.Stack(buf, true)
      log.Printf("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end\n"  , buf[:stacklen])
    }
  }()
  • 在编译期检查接口的实现 var _ io.Reader = (*MyFastReader)(nil)
  • len(nil) = 0https://golang.org/pkg/builtin/#len
  • 匿名结构很酷
  var hits struct {
    sync.Mutex
    n int
  }
  hits.Lock()
  hits.n++
  hits.Unlock()
  • httputil.DumpRequest 是非常有用的东西,不要自己创建
    • https://godoc.org/net/http/httputil#DumpRequest
  • 获得调用堆栈,我们可以使用 runtime.Caller
    • https://golang.org/pkg/runtime/#Caller
  • 要 marshal 任意的 JSON, 你可以 marshal 为 map[string]interface{}{}
  • 配置你的 CDPATH 以便你能在任何目录执行 cd github.com/golang/go
    • 添加这一行代码到 bashrc(或者其他类似的) export CDPATH=$CDPATH:$GOPATH/src
  • 从一个 slice 生成简单的随机元素
    • []string{"one", "two", "three"}[rand.Intn(3)]
最近发表
标签列表