专栏首页后台全栈之路如何在 Go 中优雅的处理和返回错误(1)——函数内部的错误处理
原创

如何在 Go 中优雅的处理和返回错误(1)——函数内部的错误处理

2860元腾讯云代金券免费领取,付款直接抵现金,立即领取>>>

腾讯云海外服务器1折限时抢购,2核4G云主机768元/1年,立即抢购>>>

腾讯云服务器1折限时抢购,2核4G云主机899元/3年,立即抢购>>>

在使用 Go 开发的后台服务中,对于错误处理,一直以来都有多种不同的方案,本文探讨并提出一种从服务内到服务外的错误传递、返回和回溯的完整方案,还请读者们一起讨论。


问题提出

在后台开发中,针对错误处理,有三个维度的问题需要解决:

  • 函数内部的错误处理: 这指的是一个函数在执行过程中遇到各种错误时的错误处理。这是一个语言级的问题
  • 函数/模块的错误信息返回: 一个函数在操作错误之后,要怎么将这个错误信息优雅地返回,方便调用方(也要优雅地)处理。这也是一个语言级的问题
  • 服务/系统的错误信息返回: 微服务/系统在处理失败时,如何返回一个友好的错误信息,依然是需要让调用方优雅地理解和处理。这是一个服务级的问题,适用于任何语言

针对这三个维度的问题,笔者准备写三篇文章一一说明。首先本文就是第一篇:函数内部的错误处理


高级语言的错误处理机制

  一个面向过程的函数,在不同的处理过程中需要 handle 不同的错误信息;一个面向对象的函数,针对一个操作所返回的不同类型的错误,有可能需要进行不同的处理。此外,在遇到错误时,也可以使用断言的方式,快速中止函数流程,大大提高代码的可读性。

  在许多高级语言中都提供了 try ... catch 的语法,函数内部可以通过这种方案,实现一个统一的错误处理逻辑。而即便是 C 这种 “中级语言”,虽然没有 try catch,但是程序员也可以使用宏定义配合 goto LABEL 的方式,来实现某种程度上的错误断言和处理。


Go 的错误断言

  在 Go 的情况就比较尴尬了。我们先来看断言,我们的目的是,仅使用一行代码就能够检查错误并终止当前函数。

由于没有 throw、没有宏,如果要实现一行断言,有两种方法。

方法一:单行 if + return

第一种是把 if 的错误判断写在一行内,比如:

	if err != nil { return err }

这种方法有值得商榷的点:

  • 虽然符合 Go 的代码规范,但是在实操中,if 语句中的花括号不换行这一点还是非常有争议的,并且笔者在实际代码中也很少见到过
  • 代码不够直观,大致浏览代码的时候,断言代码不显眼,而且在花括号中除了 return 之外也没法别的了,原因是 Go 的规范中强烈不建议使用 ; 来分隔多条语句(if 条件判断除外)

因此,笔者强烈不建议这么做。

方法二:panic + recover

第二种方法是借用 panic 函数,结合 recover 来实现,如以下代码所示:

func SomeProcess() (err error)
	defer func() {
		if e := recover(); e != nil {
			err = e.(error)
		}
	}()

	assert := func(cond bool, e error) {
		if !cond {
			panic(e)
		}
	}

	// ...

	err = DoSomething()
	assert(err == nil, fmt.Errorf("DoSomething() error: %w", err))

	// ...
}

  这种方法好不好呢?我们要分情况看:

  首先,panic 的设计原意,是在当程序或协程遇到严重错误,完全无法继续运行下去的时候,才会调用(比如段错误、共享资源竞争错误)。这相当于 Linux 中 FATAL 级别的错误日志,用这种机制,仅仅用来进行普通的错误处理(ERROR 级别),杀鸡用牛刀了。

  其次,panic 调用本身,相比于普通的业务逻辑的系统开销是比较大的。而错误处理这种事情,可能是常态化逻辑,频繁的 panic - recover 操作,也会大大降低系统的吞吐。

  但是话虽这么说,使用 panic 来断言的方案,虽然在业务逻辑中基本上不用,但在测试场景下则是非常常见的。测试嘛,用牛刀有何不可?稍微大一点的系统开销也没啥问题。对于 Go 来说,非常热门的单元测试框架 goconvey 就是使用 panic 机制来实现单元测试中的断言,用的人都说好。

结论建议

  综上,在 Go 中,对于业务代码,笔者不建议采用断言,遇到错误的时候建议还是老老实实采用这种格式:

if err := DoSomething(); err != nil {
	// ...
}

  而在单测代码中,则完全可以大大方方地采用类似于 goconvey 之类基于 panic 机制的断言。

Go 的 try ... catch

  众所周知,Go(当前版本 1.17)是没有 try ... catch 的,而且从官方的态度而言,短时间内也没有明确的计划。但是程序员有这个需求呀。这里也催生出了集中解决方案

defer 函数

  笔者采用的方法,是将需要返回的 err 变量在函数内部全局化,然后结合 defer 统一处理:

func SomeProcess() (err error) { // <-- 注意,err 变量必须在这里有定义
	defer func() {
		if err == nil {
			return
		}

		// 这下面的逻辑,就当作 catch 作用了
		if errors.Is(err, somepkg.ErrRecordNotExist) {
			err = nil		// 这里是举一个例子,有可能捕获到某些错误,对于该函数而言不算错误,因此 err = nil
		} else if errors.Like(err, somepkg.ErrConnectionClosed) {
			// ...			// 或者是说遇到连接断开的操作时,可能需要做一些重连操作之类的;甚至乎还可以在这里重连成功之后,重新拉起一次请求
		} else {
			// ...
		}
	}()

	// ...

	if err = DoSomething(); err != nil {
		return
	}

	// ...
}

  这种方案要特别注意变量作用域问题:

  比如前面的 if err = DoSomething(); err != nil { 行,如果我们将 err = ... 改为 err := ...,那么这一行中的 err 变量和函数最前面定义的 (err error) 不是同一个变量,因此即便在此处发生了错误,但是在 defer 函数中无法捕获到 err 变量了。

  在 try ... catch 方面,笔者其实没有特别好的方法来模拟,即便是上面的方法也有一个很让人头疼的问题:defer 写法导致错误处理前置,而正常逻辑后置了。

命名的错误处理函数

  要解决前文提及的 defer 写法导致错误处理前置的问题,有第一种解决方法是比较常规的,那就是将 defer 后面的匿名函数改成一个命名函数,抽象出一个专门的错误处理函数。这个时候我们可以将上一段函数进行这样的改造:

func SomeProcess() error {
	// ...

	if err = DoSomething(); err != nil {
		return unifiedError(err)
	}

	// ...
}

func unifiedError(err error) error {
	if errors.Is(err, somepkg.ErrRecordNotExist) {
		return nil		// 有可能捕获到某些错误,对于该函数而言不算错误,因此 err = nil

	} else if errors.Like(err, somepkg.ErrConnectionClosed) {
		return fmt.Errorf("handle XXX error: %w", err)

	// ...

	} else {
		return err
	}
}

  这样就舒服一些了,至少逻辑前置,错误处理后置。不过读者肯定会发现——这不是什么语言都可以这么搞嘛?诚然,这怎么看都不像是对 try ... catch 的模拟,但这种方法依然很推荐,特别是错误处理代码很长的时候。

goto LABEL

  理论上,我们可以通过 goto 语句,将错误处理后置,比如:

func SomeProcess() error {
	// ...

	if err = DoSomething(); err != nil {
		goto ERR
	}

	// ...

	return nil

ERR:
	// ...
}

  对 C 语言比较熟悉的同学可能会觉得很亲切,因为在 Linux 内核中就有大量这种写法。这种写法呢,笔者其实说不出具体不好的地方,但是这个看起来很像 C 的写法,其实限制很多,反而比起 C 而言,需要注意的地方也更多:

  • 仅限于 ANSI-C 的话,要求所有的局部变量都需要前置声明,这就避免了因为变量作用域而带来的同名变量覆盖;但 Go 需要注意这个问题。
  • C 支持宏定义,配合前文可以实现断言,使得错误处理语句可以做得比较优雅;而 Go 不支持
  • Go 经常有很多匿名函数,匿名函数无法 goto 到外层函数的标签,这也限制了 goto 的使用

  不过笔者倒也不是不支持使用 goto,只是觉得在现有机制下,还是使用前两种模式比较符合 Go 的习惯。


  下一篇文章是《如何在 Go 中优雅的处理和返回错误(2)——函数/模块的错误信息返回》,笔者详细整理了 Go 1.13 之后的 error wrapping 功能,敬请期待~~


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原作者: amc,欢迎转载,但请注明出处。

原文标题:《如何在 Go 中优雅的处理和返回错误(1)——函数内部的错误处理

发布日期:2021-09-18

原文链接:/developer/article/1879728

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 [email protected] 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 拒绝千篇一律,这套Go错误处理的完整解决方案值得一看!

    ? 导语?|?在使用Go开发的后台服务中,对于错误处理,一直以来都有多种不同的方案,本文探讨并提出一种从服务内到服务外的一个统一的传递、返回和回溯的完整方案,...

    腾小云
  • Golang error 的突围

    姗姗来迟的 Go 1.13 修改了 errors 包,增加了几个函数,用于增强 error 的功能,这篇文章介绍 error 相关的用法。

    梦醒人间
  • Go 语言错误及异常处理篇(一):error 接口

    PHP 的错误和异常处理逻辑一直比较混乱,对于错误处理,在 PHP 5 中是通过 error_reporting 函数设置错误报告级别,然后通过 set_err...

    学院君
  • Go进阶笔记关于Error

    其实很多时候是使用的姿势不对,或者说,对于error的用法没有完全理解,这里整理一下关于Go中的error 。

    后场技术
  • Go 语言学习之错误处理

    在 Go 语言中,错误处理方式一致饱受争议。官方推荐的标准错误处理方法是返回 error 状态。在函数中,通常 error 是最后一个返回参数,程序通过 err...

    frank.
  • 写错误也要优雅,必须优雅!go语言nil的漂亮用法

    尽管go有一个简单的错误模型,但乍一看,事情并不像它们应该的那样简单。在这篇文章中,我想提供一个很好的策略来处理错误并克服您在过程中可能遇到的问题。

    程序员小助手
  • Go 语言基础--错误&异常浅析

    如果go是你的第一门语言,go的异常和错误体系可能比较容易接受,但如果你有一定的Java或者c++基础,go的异常和错误体系可能会比较不适应。 go的错误及异...

    邹志全
  • Go语言相关练习_选择题(2)

    go语言中字符串是UTF-8编码并存储的,它语言不定长的字节,所以它不支持下标操作,因为没一个下标操作代表的是固定长度的字节,所以不能对字符串中某个字符单独赋值...

    Zoctopus
  • 如何写出优雅的 Golang 代码

    原文: https://draveness.me/golang-101.html

    sunsky
  • 《快学 Go 语言》第 10 课 —— 错误与异常

    Go 语言的异常处理语法绝对是独树一帜,在我见过的诸多高级语言中,Go 语言的错误处理形式就是一朵奇葩。一方面它鼓励你使用 C 语言的形式将错误通过返回值来进行...

    老钱
  • 比Python还好用的Go语言要出2.0了,你想怎么设计?

    在昨天的 Go contributor 年度峰会上,与会者对错误处理和泛型的设计草案有了一个初步的了解。Go 2 的开发项目是去年宣布的,今天谷歌公布了这一语言...

    机器之心
  • Golang 语言怎么处理错误?

    golang 程序大多数是通过 if err != nil 处理错误,在 golang 社区中,有一部分 golang 程序员对此举是持反对观点,他们认为在 g...

    frank.
  • Go 每日一库之 gotalk

    接下来让我们来编写一个简单的 echo 程序,服务端直接返回收到的客户端信息,不做任何处理。首先是服务端:

    用户7731323
  • 程序员技术选型:写Go还是Java?

    本文作者根据自己的使用体验,详细对比了 Go 和 Java 的使用差异,给了开发者们一个中肯的选用参考。

    深度学习与Python
  • Python 工匠:让函数返回结果的技巧

    毫无疑问,函数是 Python 语言里最重要的概念之一。在编程时,我们将真实世界里的大问题分解为小问题,然后通过一个个函数交出答案。函数即是重复代码的克星,也是...

    崔庆才
  • 论golang是世界上最好的语言

    概述 golang is a better C and a simple C++ golang主要特性 1、语法简单 舍弃语法糖,严格控制关键字 C++语法糖之...

    李海彬
  • ?? Go 有别于其他语言的九个特性 ??

    随着编程语言的发展,Go 还很年轻。它于 2009 年 11 月 10 日首次发布。其创建者Robert Griesemer Rob Pike 和 Ken T...

    海拥
  • 第11节 编码规范

    命名是代码规范中很重要的一部分,统一的命名规则有利于提高的代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。

    小尘哥
  • 深入Go的异常错误处理机制(二)理解

    阿伟

扫码关注云+社区

领取腾讯云代金券

http://www.vxiaotou.com