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 然后进行重试的。


目录
相关文章
|
6天前
|
中间件 Go
go语言后端开发学习(三)——基于validator包实现接口校验
go语言后端开发学习(三)——基于validator包实现接口校验
|
15天前
|
存储 前端开发 中间件
Go Web 开发 Demo【用户登录、注册、验证】(3)
Go Web 开发 Demo【用户登录、注册、验证】
|
15天前
|
前端开发 数据库连接 Go
Go Web 开发 Demo【用户登录、注册、验证】(1)
Go Web 开发 Demo【用户登录、注册、验证】
|
6天前
|
存储 Go 开发工具
go语言后端开发学习(二)——基于七牛云实现的资源上传模块
go语言后端开发学习(二)——基于七牛云实现的资源上传模块
|
6天前
|
JSON 算法 Go
go语言后端开发学习(一)——JWT的介绍以及基于JWT实现登录验证
go语言后端开发学习(一)——JWT的介绍以及基于JWT实现登录验证
|
1月前
|
缓存 负载均衡 网络协议
使用Go语言开发高性能服务的深度解析
【5月更文挑战第21天】本文深入探讨了使用Go语言开发高性能服务的技巧,强调了Go的并发性能、内存管理和网络编程优势。关键点包括:1) 利用goroutine和channel进行并发处理,通过goroutine池优化资源;2) 注意内存管理,减少不必要的分配和释放,使用pprof分析;3) 使用非阻塞I/O和连接池提升网络性能,结合HTTP/2和负载均衡技术;4) 通过性能分析、代码优化、缓存和压缩等手段进一步提升服务性能。掌握这些技术能帮助开发者构建更高效稳定的服务。
|
15天前
|
JSON 前端开发 Java
Go Web 开发 Demo【用户登录、注册、验证】(4)
Go Web 开发 Demo【用户登录、注册、验证】
|
15天前
|
Go 数据库
Go Web 开发 Demo【用户登录、注册、验证】(2)
Go Web 开发 Demo【用户登录、注册、验证】
|
1月前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
83 1
|
1月前
|
存储 关系型数据库 Go
【Go语言专栏】基于Go语言的RESTful API开发
【4月更文挑战第30天】本文介绍了使用Go语言开发RESTful API的方法,涵盖了路由、请求处理、数据存储和测试关键点。RESTful API基于HTTP协议,无状态且使用标准方法表示操作。在Go中,通过第三方库如`gorilla/mux`进行路由映射,使用`net/http`处理请求,与数据库交互可选ORM库`gorm`,测试则依赖于Go内置的`testing`框架。Go的简洁性和并发性使得它成为构建高效API的理想选择。