Go Error 最佳实践

举报
宇宙之一粟 发表于 2022/04/15 00:19:38 2022/04/15
1.6k+ 0 0
【摘要】 Go 语言由于没有 try...catch 结构屡屡被诟病,Go 中的每一个错误都需要处理,而且错误经常是蹭蹭嵌套的。如下面的结构:a, err := fn()if err != nil { return err}func fn() error { b, err := fn1() if err != nil { … return err } if _, err = f...

Go 语言由于没有 try...catch 结构屡屡被诟病,Go 中的每一个错误都需要处理,而且错误经常是蹭蹭嵌套的。如下面的结构:

a, err := fn()
if err != nil {
  return err
}
func fn() error {
  b, err := fn1()
  if  err != nil {return err
  }
  if _, err = fn2(); err != nil {}
}

Go Error 也是接口

在 Go 语言中,Go Error 也是一个接口:

type error interface {
  Error() string
}

所以,实现、创建或抛出错误,实际上就是实现这个接口。最常见的三种方式是:

  • errors.New
  • fmt.Errof
  • implement error interface
var ErrRecordNotExist   = errors.New("record not exist")
func ErrFileNotExist(filename string) error {
  return fmt.Errorf("%s file not exist", filename)
}
type ErrorCallFailed struct {
  Funcname string
}
func (*ErrorCallFailed) Error() string {
  return fmt.Sprintf(“call %s failed”, funcname)
}
var ErrGetFailed error = &ErrorCallFailed{ Funcname: "getName", }

Go 错误只涉及以下两个逻辑:

  • 抛出错误,这涉及到我们如何定义错误。在实现功能时,异常情况需要返回合理的错误
  • 处理错误。调用函数时,要根据函数的返回实现不同的逻辑,考虑是否有错误,错误是否属于某种类型,是否忽略错误等。
func (d *YAMLToJSONDecoder) Decode(into interface{}) error {
	bytes, err := d.reader.Read()
	if err != nil && err != io.EOF {
		return err
	}

	if len(bytes) != 0 {
		err := yaml.Unmarshal(bytes, into)
		if err != nil {
			return YAMLSyntaxError{err}
		}
	}
	return err
}

type YAMLSyntaxError struct {
	err error
}

func (e YAMLSyntaxError) Error() string {
	return e.err.Error()
}

Kubernetes decode.go 中的这篇文章不仅可以直接返回错误,还可以包装错误,要么返回 YAMLSyntaxError,要么直接忽略 io.EOF

通常,有三种方法可以确定错误类型:

  • 直接通过 == , 例如:if err == ErrRecordNotExist {}
  • 类型推断,if _, ok := err.(*ErrRecordNotExist); ok {}
  • errors.Iserrors.As 方法. 从 Go 1.13 开始添加。 if errors.Is(err, ErrRecordNotExist) 涉及错误换行,解决了定位嵌套错误的麻烦。

遵循的规则

理解了 Go 错误的基本概念之后,是时候讨论可以遵循的规则以进行更好的实践了。让我们从定义开始,然后到错误处理。

定义错误

  • 使用 fmt.Errorf 而不是 errors.New

fmt.Errorf 提供拼接参数功能,并对错误进行包装。虽然我们在处理简单错误时发现这两种方法没有区别,但始终将 fmt.Errorf 设置为您的偏好可以保持代码统一。

封装相同的错误

封装同样的错误,比如上面提到的 ErrorCallFailed,是一种常见的代码优化,结合 errors.Is 或者errors.As 可以解包层,更好的判断错误的真正原因。 至于 errors.Iserrors.As 的区别,前者既需要类型匹配又需要消息匹配,而后者只需要类型匹配。

func fn(n string) error {
  if _, err := get(n); err != nil {
    return ErrorCallFailed("get n")
  }
}

func abc() error {
  _, err = fn("abc")
  if err != nil {
    return fmt.Errorf("handle abc failed, %w", err)
  }
}

func main() {
  _, err := abc()
  if errors.Is(err, ErrorCallFailed){
    log.Fatal("failed to call %v", err)
    os.Exist(1)
  }
}

使用 %w 而不是 %v

一个方法被多处调用时,为了得到完整的调用链,开发者会在返回错误的地方一层一层的包裹起来,通过fmt.Errorf 不断添加当前调用的唯一特征,可以是日志也可以是一个参数。在错误拼接中偶尔使用 %v 而不是 %w 会导致 Go 的错误包装功能在 Go1.13 和之后的版本中失效。正确换行后的错误类型如下

使错误信息简洁

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

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

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 错误处理是没有用的,即使它不是那么用户友好。 至少,逐个错误返回是一个很好的设计,在最高层的调用处统一处理错误。 此外,我们仍然可以期待即将发布的版本中的这些改进将带来更简单的应用程序。

感谢阅读!

引用

https://go.dev/blog/go1.13-errors

https://errnil.substack.com/p/wrapping-errors-the-right-way

https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully

原文链接:Go Error Best Practices. Create and handle your errors the right… | by Stefanie Lai | Level Up Coding (gitconnected.com)

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

作者其他文章

评论(0

抱歉,系统识别当前为高风险访问,暂不支持该操作

    全部回复

    上滑加载中

    设置昵称

    在此一键设置昵称,即可参与社区互动!

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。