Go Error 最佳实践(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: Go 语言由于没有 try...catch 结构屡屡被诟病,Go 中的每一个错误都需要处理,而且错误经常是蹭蹭嵌套的。

使错误信息简洁

合理的错误信息可以通过逐层包装让我们远离冗余信息。

很多人有在下面的事情上打印日志的习惯,加上参数,当前方法的名字,调用方法的名字,这是不必要的。

func Fn(id string) error {
  err := Fn1()
  if err != nil {
    return fmt.Errorf("Call Fn1 failed with id: %s, %w", id, err
  }
  ...
  return nil
}

但是,清晰明了的错误日志只包含当前操作错误的信息、内部参数和动作,以及调用者不知道但调用者不知道的信息,例如当前的方法和参数。这是 Kubernetes 中 endpoints.go 的错误日志,一个非常好的例子,只打印内部 Pod 相关参数和 Unable to get Pod 的失败动作:

func (e *Controller) addPod(obj interface{}) {
  pod := obj.(*v1.Pod)
  services, err := e.serviceSelectorCache.GetPodServiceMemberships(e.serviceLister, pod)
  if err != nil {
    utilruntime.HandleError(fmt.Errorf("Unable to get pod %s/%s's service memberships: %v", pod.Namespace, pod.Name, err))
    return
  }
  for key := range services {
    e.queue.AddAfter(key, e.endpointUpdatesBatchPeriod)
  }
}

处理 error 的黄金五法则

以下介绍作者认为的黄金五法则。

  • errors.Is 优于==

== 比较容易出错,只能比较当前的错误类型而不能解包。因此,errors.Iserrors.As 是更好的选择。

package main
import (
  "errors"
  "fmt"
)
type e1 struct{}
func (e e1) Error() string {
  return "e1 happended"
}
func main() {
  err1 := e1{}
  err2 := e2()
  if err1 == err2 {
    fmt.Println("Equality Operator: Both errors are equal")
  } else {
    fmt.Println("Equality Operator: Both errors are not equal")
  }
  if errors.Is(err2, err1) {
    fmt.Println("Is function: Both errors are equal")
  }
}
func e2() error {
  return fmt.Errorf("e2: %w", e1{})
}
// Output
Equality Operator: Both errors are not equal
Is function: Both errors are equal
  • 打印错误日志,但不打印正常日志
buf, err := json.Marshal(conf)
if err != nil {
  log.Printf(“could not marshal config: %v”, err)
}

新手常犯的错误是使用 log.Printf 打印所有日志,包括错误日志,导致我们无法通过日志级别正确处理日志,调试难度大。我们可以从应用 log.Fatalfdependencycheck.go 中学习正确的方法。

if len(args) != 1 {
  log.Fatalf(“usage: dependencycheck <json-dep-file> (e.g. ‘go list -mod=vendor -test -deps -json ./vendor/…’)”)
}
if *restrict == “” {
  log.Fatalf(“Must specify restricted regex pattern”)
}
depsPattern, err := regexp.Compile(*restrict)
if err != nil {
  log.Fatalf(“Error compiling restricted dependencies regex: %v”, err)
}
  • 永远不要通过错误处理逻辑

这是错误过程的说明。

bytes, err := d.reader.Read()
if err != nil && err != io.EOF {
  return err
}
row := db.QueryRow(“select name from user where id= ?”, 1)
err := row.Scan(&name)
if err != nil && err != sql.ErrNoRows{
  return err
}

可以看到,io.EOFsql.ErrNoRows 这两个 error 都被忽略了,后者是一个典型的用 error 来表示业务逻辑(数据不存在)的例子。 我反对这样的设计但支持大小的优化, err:= row.Scan(&name) if size == 0 {log.Println(“no data”) } ,通过添加返回参数而不是直接抛出错误来提供帮助。

  • bottom 方法返回错误,upper 方法处理错误
func Write(w io.Writer, buf []byte) error {
  _, err := w.Write(buf)
  if err != nil {
    log.Println(“unable to write:”, err)
    return err
  }
  return nil
}

与上面类似的代码有一个明显的问题。如果打印日志后返回错误,则很可能存在重复日志,因为调用者也可能打印日志。

那么如何避免呢?让每个方法只执行一个功能。而这里的一个常见选择是底层方法只返回错误,上层方法处理错误。

  • 包装错误消息并添加有利于故障排除的上下文。

在 Go 中没有原生的 stacktrace 可以依赖,我们只能通过自己实现或第三方库来获取那些异常的堆栈信息。 比如 Kubernetes 实现了一个比较复杂的 klog 包来支持日志打印、堆栈信息和上下文。 如果您开发 Kubernetes 相关的应用程序,例如 Operator,您可以参考 Kubernetes 中的结构化日志记录。 此外,那些第三方错误封装库,如 pkg/errors 非常有名。


结语

Go 设计哲学的初衷是简化,但有时会使事情复杂化。 然而,你永远不能认为 Go 错误处理是没有用的,即使它不是那么用户友好。 至少,逐个错误返回是一个很好的设计,在最高层的调用处统一处理错误。 此外,我们仍然可以期待即将发布的版本中的这些改进将带来更简单的应用程序。

感谢阅读!


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
2月前
|
测试技术 Shell Go
go 语言优雅地处理 error
go 语言优雅地处理 error
|
2月前
|
缓存 监控 Kubernetes
go-zero解读与最佳实践(上)
go-zero解读与最佳实践(上)
|
2月前
|
程序员 测试技术 Go
用 Go 编写简洁代码的最佳实践
用 Go 编写简洁代码的最佳实践
|
2月前
|
存储 缓存 安全
|
2月前
|
存储 NoSQL 测试技术
go最佳实践:如何舒适地编码
go最佳实践:如何舒适地编码
|
2月前
|
存储 缓存 Java
涨姿势啦!Go语言中正则表达式初始化的最佳实践
在Go语言中,正则表达式是处理字符串的强大工具,但其编译过程可能消耗较多性能。本文探讨了正则表达式编译的性能影响因素,包括解析、状态机构建及优化等步骤,并通过示例展示了编译的时间成本。为了优化性能,推荐使用预编译策略,如在包级别初始化正则表达式对象或通过`init`函数进行错误处理。此外,简化正则表达式和分段处理也是有效手段。根据初始化的复杂程度和错误处理需求,开发者可以选择最适合的方法,以提升程序效率与可维护性。
46 0
涨姿势啦!Go语言中正则表达式初始化的最佳实践
|
2月前
|
IDE Go 开发工具
Go Error module declares its path as but was required as解决方案
文章提供了一个解决方案,用于处理在Go工程中将依赖的仓库从A更换为B(即使它们完全相同)时遇到的路径声明错误,建议通过发布新版本来解决此问题。
68 0
|
2月前
|
Kubernetes Go 数据库
go-zero 分布式事务最佳实践
go-zero 分布式事务最佳实践
|
4月前
|
Unix Docker 容器
使用docker 启动naocs 报错出现:standard_init_linux.go:241: exec user process caused "exec format error"
```markdown Error in Docker container startup: &quot;standard_init_linux.go:241: exec user process caused \&quot;exec format error\&quot;&quot;. Occurred at 2024-06-29 09:26:19.910, followed by a failed hook with a syslog delivery error at 09:27:20.193. Seeking solutions from experts. ```
|
5月前
|
安全 Go 调度