Go 错误处理与单元测试

庆云2年前技术文章745

1、错误处理

1.1 如何定义错误

在 Go 语言中,无论是在类型检查还是编译过程中,都是将错误看做值来对待,和 string 或者 integer 这些类型值并不差别。声明一个 string 类型变量和声明一个 error 类型变量是没什么区别的。


你可以定义接口作为 error 的类型,有关 error 能够提供什么样信息都是由自己决定的,这是 error 在 golang 作为值的好处,不过这样做也自然有其坏处,有关 error 定义好坏就全由其定义开发人员所决定,也就是有关 error 融入过多人为的主观因素。

1.2 错误处理原则

错误处理的原则就是不能丢弃任何有返回 error 的调用,不要使用 _ 丢弃,必须全部处理,接收到错误,要么返回 error ,或者使用 log 记录下来尽早 return。


一旦有错误发生,马上返回,尽量不要使用 panic,除非你知道你在做什么,错误描述如果是英文必须为小写,不需要标点结尾,采用独立的错误流进行处理。

// 错误写法
if err != nil {
	// 错误处理
} else {
	// 正常代码
}

// 正确写法
if err != nil {
	// 错误处理
	return // 或者继续
}
// 正常代码

1.3 defer+recover 机制

defer 是 Go 语言中一种延迟调用机制,defer 后面的函数只有在当前函数执行完毕后才能执行,将延迟的语句按 defer 的逆序进行执行,也就是说先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行,即对 defer 延迟的语句做压栈操作(先进后出),通常用于释放资源。


recover 内建函数允许程序管理恐慌过程中的 Go 进程。在 defer 的函数中,执行 recover 调用会取回传至 panic 调用的错误值,恢复正常运行,停止恐慌过程。


实例:

package main

import "fmt"

func test() {
	// defer+recover捕获异常
	defer func() {
		// 调用recover函数,捕获异常
		// 如果没有捕获异常,返回值为零值(nil)
		err := recover()
		if err != nil {
			fmt.Printf("当前错误已经捕获: %v\n", err)
		}
	}()

	fmt.Println("函数执行第一阶段(第一小节)...")
	n1 := 10
	n2 := 0
	ret := n1 / n2
	fmt.Println(ret)

	fmt.Println("函数执行第一阶段(第二小节)...")
}

func main() {
	fmt.Println("函数执行第一阶段...")
	test()

	fmt.Println("函数执行第二阶段...")
}


/*
执行结果:
函数执行第一阶段...
函数执行第一阶段(第一小节)...
当前错误已经捕获: runtime error: integer divide by zero
函数执行第二阶段...
*/

1.4 自定义错误

在 Go 语言中,有多种创建自定义错误信息的方法,每一种都有自己的优点和缺点。

1.4.1 基于字符串的错误

基于字符串的错误可以用 Go 语言中两个开箱即用方法来自定义错误,适用哪些仅返回描述错误信息的相对来说比较简单的错误。


语法:

// 方法一
err := errors.New("math: divided by zero")

// 方法二
err := fmt.Errorf("math: divided by zero")


实例:

package main

import (
	"errors"
	"fmt"
)

func test() (err error) {
	n1 := 10
	n2 := 0
	if n2 == 0 {
		// 抛出自定义错误
		return errors.New("除数不能为0")
	} else {
		ret := n1 / n2
		fmt.Println(ret)
		// 如果没有错误返回零值
		return nil
	}
}

func main() {
	fmt.Println("函数执行第一阶段...")
	err := test()
	if err != nil {
		fmt.Printf("自定义错误: %v\n", err)
	}

	fmt.Println("函数执行第二阶段...")
}


/*
执行结果:
函数执行第一阶段...
自定义错误: 除数不能为0
函数执行第二阶段...
*/

1.4.2 自定义数据结构的错误

可以通过在你的结构上实现 Error 接口中定义的 Error() 函数来创建自定义的错误类型。


实例:

package main

import (
	"fmt"
)

// 定义一个DivideError结构
type DivideError struct {
	dividee int
	divider int
}

// 实现error接口
func (de *DivideError) Error() string {
	strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
	`
	return fmt.Sprintf(strFormat, de.dividee)
}

// 定义int类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
	if varDivider == 0 {
		dData := DivideError{
			dividee: varDividee,
			divider: varDivider,
		}
		errorMsg = dData.Error()
		return
	} else {
		return varDividee / varDivider, ""
	}

}

func main() {
	// 正常情况
	result, errorMsg1 := Divide(100, 10)
	if errorMsg1 == "" {
		fmt.Println("100/10 = ", result)
	}
	// 当除数为零的时候会返回错误信息
	_, errorMsg2 := Divide(100, 0)
	if errorMsg2 != "" {
		fmt.Println("errorMsg is: ", errorMsg2)
	}
}


/*
执行结果:
100/10 =  10
errorMsg is:  
    Cannot proceed, the divider is zero.
    dividee: 100
    divider: 0
*/

1.5 panic

可以借助 builtin 包下的内建 panic 函数实现:程序出现错误后,后续代码没必要执行了,即程序中断、退出程序。


注意: 一旦有错误发生,马上返回,尽量不要使用 panic,除非你知道你在做什么。


实例:

package main

import (
	"errors"
	"fmt"
)

func test() (err error) {
	n1 := 10
	n2 := 0
	if n2 == 0 {
		// 抛出自定义错误
		return errors.New("除数不能为0")
	} else {
		ret := n1 / n2
		fmt.Println(ret)
		// 如果没有错误返回零值
		return nil
	}
}

func main() {
	fmt.Println("函数执行第一阶段...")
	err := test()
	if err != nil {
		fmt.Printf("自定义错误: %v\n", err)
		// panic
		panic(err)
	}

	fmt.Println("函数执行第二阶段...")
}


/*
执行结果:
函数执行第一阶段...
自定义错误: 除数不能为0
panic: 除数不能为0

goroutine 1 [running]:
main.main()
        /Users/zhangyy/Documents/Gitlab/yannic/golang/zhangyydev.com/golang-basics/main/demo.go:28 +0x114
exit status 2
*/


2、单元测试

单元测试文件名命名规范 example_test.go 测试用例函数名称必须以 Test 开头,例如:TestExample。每个重要的函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。


返回列表

上一篇:lru_cache 缓存

下一篇:HAProxy

相关文章

k8s集群内的DNS原理与配置

背景:最近公司有个需求,要在POD应用容器里面能够访问到一些外部域名,这些域名都在一台自建的DNS服务器上做了解析绑定。如果直接在Pod容器里的/etc/hosts文件中设置域名解析,或修改/etc/...

Redis 持久化机制 RDB

Redis 持久化机制 RDB

前言Redis 有两种持久化机制,分别是 RDB 与 AOF 本篇文章将介绍 RDB 的执行过程与应用。1. RDB 简介RDB 持久化是把当前进程数据生成快照保存到硬盘的过程,触发 RDB 持久化过...

xargs-管道命令符

有时候我们的脚本却需要 echo '516' | kill 这样的效果,例如 ps -ef | grep 'ddd' | kill 这样的效果,筛选出符合某条件的进程pid然后结束。这种需求对于我们来...

Python 调用阿里云 OpenAPI 巡检到期云资源

Python 调用阿里云 OpenAPI 巡检到期云资源

前言本篇文章介绍我写的一个程序,通过调用阿里云 OpenAPI 巡检即将到期的云资源。https://github.com/COOH-791/cloud_instance_sentry1. 用途说到云...

REPMGR-PG高可用搭建(二)

REPMGR-PG高可用搭建(二)

REPMGR搭建步骤一、介绍repmgr是第二象限开源的一套流复制集群管理工具,用于管理PostgreSQL服务器群集中的复制和故障转移。 支持故障自动转移和手动切换;支持分布式管理集群节点,易扩展,...

InnoDB秘籍:MVCC机制与行锁的深度探索

InnoDB秘籍:MVCC机制与行锁的深度探索

前言事务的起源可以追溯到 6000 年以前,当时苏美尔人(Sumerians)就发明了事务处理和记录的方法。已知最早的记录是写在土块上的,上面写了皇家的税收、土地、谷物、牲畜、奴隶和黄金,明确地记下了...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。