Go 开发十种常犯错误(上)

简介: Go 开发十种常犯错误

1、未知的枚举值


示例:

type Status uint32
const (
  StatusOpen Status = iota
  StatusClosed
  StatusUnknown
)

示例中使用了 iota 创建了枚举值,其结果就是:

StatusOpen = 0
StatusClosed = 1
StatusUnknown = 2

现在假设上述 Status 类型将会作为 JSON request 的一部分:

type Request struct {
  ID        int    `json:"Id"`
  Timestamp int    `json:"Timestamp"`
  Status    Status `json:"Status"`
}

然后你收到的数据可能是:

{
  "Id": 1234,
  "Timestamp": 1563362390,
  "Status": 0
}

这看起来似乎没有任何问题,status 将会被解码为 StatusOpen 。

但是如果另一个请求的数据是这样:

{
  "Id": 1235,
  "Timestamp": 1563362390
}

这时 status 即使没有传值(也就是 unknown 未知状态),但由于默认零值,其将会被解码为 StatusOpen ,显然不符合业务语义上的 StatusUnknown 。


最佳实践是将未知的枚举值设置为 0 :

type Status uint32
const (
  StatusUnknown Status = iota
  StatusOpen
  StatusClosed
)

2、基准测试


基准测试受到多方面的影响,因此想得到正确的结果比较困难。


最常见的一种错误情况就是被编译器优化了,例如:

func clear(n uint64, i, j uint8) uint64 {
  return (math.MaxUint64<<j | ((1 << i) - 1)) & n
}

这个函数的作用是清除指定范围的 bit 位,基准测试可能会这样写:

func BenchmarkWrong(b *testing.B) {
  for i := 0; i < b.N; i++ {
    clear(1221892080809121, 10, 63)
  }
}

在这个基准测试中,编译器将会注意到这个 clear 是一个 leaf 函数(没有调用其它函数)因此会将其 inline 。一旦这个函数被 inline 了,编译器也会注意到它没有 side-effects(副作用)。因此 clear 函数的调用将会被简单的移除从而导致不准确的结果。


解决这个问题的一种方式是将函数的返回结果设置给一个全局变量:

var result uint64
func BenchmarkCorrect(b *testing.B) {
  var r uint64
  for i := 0; i < b.N; i++ {
    r = clear(1221892080809121, 10, 63)
  }
  result = r
}

此时,编译器不知道这个函数的调用是否会产生 side-effect ,因此基准测试的结果将会是准确的。



3、指针


按值传递变量将会创建此变量的副本(简称“值拷贝”),而通过指针传递则只会复制变量的内存地址。


因此,指针传递总是更快吗?显然不是,尤其是对于小数据而言,值拷贝更快性能更好。


原因与 Go 中的内存管理有关。让我们简单的解释一下。


变量可以被分配到 heap 或者 stack 中:

  • stack 包含了指定 goroutine 中的将会被用到的变量。一旦函数返回,变量将会从 stack 中 pop 移除。
  • heap 包含了需要共享的变量(例如全局变量等)。


示例:

func getFooValue() foo {
  var result foo
  // Do something
  return result
}

这里,result 变量由当前的 goroutine 创建,并且将会被 push 到当前的 stack 中。一旦这个函数返回了,调用者将会收到 result 变量的值拷贝副本,而这个 result 变量本身将会被从 stack 中 pop 移除掉。它仍存在于内存中,直到它被另一个变量擦除,但是它无法被访问到。


现在看下指针的示例:

func getFooPointer() *foo {
  var result foo
  // Do something
  return &result
}

result 变量仍然由当前 goroutine 创建,但是函数的调用者将会接受的是一个指针(result 变量内存地址的副本)。如果 result 变量被从 stack 中 pop 移除,那么函数调用者显然无法再访问它。

在这种情况下,为了正常使用 result 变量,Go 编译器将会把 result 变量 escape(转移)到一个可以共享变量的位置,也就是 heap 中。


传递指针也会有另一种情况,例如:

func main()  {
  p := &foo{}
  f(p)
}

由于我们在相同的 goroutine(main 函数)中调用 f 函数,这里的 p 变量无需被 escape 到 heap 中,它只会被推送到 stack 中,并且 sub-function 也就是这里的 f 函数是可以直接访问到 p 变量的。



stack 为什么更快?主要有两个原因:

  • stack 几乎没有垃圾回收。正如上文所述,一个变量创建后 push 到 stack 中,其函数返回后则从 stack 中 pop 掉。对于未使用的变量无需复杂的过程来回收它们。
  • stack 从属于一个 goroutine ,与 heap 相比,stack 中的变量不需要同步,这也导致了 stack 性能上的优势。


总之,当我们创建一个函数时,我们的默认行为应该是使用值而不是指针,只有当我们想用共享变量时才应该使用指针。



如果我们遇到性能问题,一种可能的优化就是检查指针在某些特定情况下是否有帮助。如果你想要知道编译器何时将变量 escape 到 heap ,可以使用以下命令:

go build -gcflags "-m -m"

4、从 for/switch 或 for/select 中 break


例如:

for {
  switch f() {
  case true:
    break
  case false:
    // Do something
  }
}
for {
  select {
  case <-ch:
  // Do something
  case <-ctx.Done():
    break
  }
}

注意,break 将会跳出 switch 或 select ,但不会跳出 for 循环。


为了跳出 for 循环,一种解决方式是使用带标签的 break :

loop:
  for {
    select {
    case <-ch:
    // Do something
    case <-ctx.Done():
      break loop
    }
  }

5、errors 管理


Go 中的错误处理一直以来颇具争议。


推荐使用 https://github.com/pkg/errors 库,这个库遵循如下规则:

An error should be handled only once. Logging an error is handling an error. So an error should either be logged or propagated.

而当前的标准库(只有一个 New 函数)却很难去遵循这一点,因为我们可能希望为错误添加一些上下文并具有某种形式的层次结构。


假设我们在调用某个 REST 请求操作数据库时会碰到以下问题:

unable to server HTTP POST request for customer 1234
 |_ unable to insert customer contract abcd
     |_ unable to commit transaction

通过上述 pkg/errors 库,我们可以处理如下:

func postHandler(customer Customer) Status {
  err := insert(customer.Contract)
  if err != nil {
    log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
    return Status{ok: false}
  }
  return Status{ok: true}
}
func insert(contract Contract) error {
  err := dbQuery(contract)
  if err != nil {
    return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)
  }
  return nil
}
func dbQuery(contract Contract) error {
  // Do something then fail
  return errors.New("unable to commit transaction")
}

最底层通过 errors.New 初始化一个 error ,中间层 insert 函数向其添加更多上下文信息来包装此 error ,然后父级调用者通过记录日志来处理错误,每一层都对错误进行了返回或者处理。


我们可能还想检查错误原因以进行重试。例如我们有一个外部库 db 处理数据库访问,其可能会返回一个 db.DBError 的错误,为了实现重试,我们必须检查具体的错误原因:

func postHandler(customer Customer) Status {
  err := insert(customer.Contract)
  if err != nil {
    switch errors.Cause(err).(type) {
    default:
      log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
      return Status{ok: false}
    case *db.DBError:
      return retry(customer)
    }
  }
  return Status{ok: true}
}
func insert(contract Contract) error {
  err := db.dbQuery(contract)
  if err != nil {
    return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)
  }
  return nil
}

如上所示,通过  pkg/errors 库的 errors.Cause 即可轻松实现。


一种经常会犯的错误是只部分使用  pkg/errors 库,例如:

switch err.(type) {
default:
  log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
  return Status{ok: false}
case *db.DBError:
  return retry(customer)
}

这里直接使用 err.(type) 是无法捕获到 db.DBError 然后进行重试的。


目录
相关文章
|
3月前
|
缓存 弹性计算 API
用 Go 快速开发一个 RESTful API 服务
用 Go 快速开发一个 RESTful API 服务
|
18天前
|
Go 数据安全/隐私保护 开发者
Go语言开发
【10月更文挑战第26天】Go语言开发
32 3
|
19天前
|
Java 程序员 Go
Go语言的开发
【10月更文挑战第25天】Go语言的开发
26 3
|
3月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
131 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
3月前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
3月前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
3月前
|
JSON 编解码 中间件
go-zero代码生成器助你高效开发
go-zero代码生成器助你高效开发
|
3月前
|
Java Go API
我用go-zero开发了第一个线上项目
我用go-zero开发了第一个线上项目
|
3月前
|
监控 Serverless Go
Golang 开发函数计算问题之Go 语言中切片扩容时需要拷贝原数组中的数据如何解决
Golang 开发函数计算问题之Go 语言中切片扩容时需要拷贝原数组中的数据如何解决
|
3月前
|
Go 开发者
Go1.22 新特性:Slices 变更 Concat、Delete、Insert 等函数,对开发挺有帮助!
Go1.22 新特性:Slices 变更 Concat、Delete、Insert 等函数,对开发挺有帮助!