惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Vulnerabilities – Threatpost
S
Secure Thoughts
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Threat Research - Cisco Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
AWS News Blog
AWS News Blog
C
CERT Recently Published Vulnerability Notes
博客园 - 【当耐特】
量子位
有赞技术团队
有赞技术团队
腾讯CDC
P
Proofpoint News Feed
小众软件
小众软件
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
L
LINUX DO - 热门话题
人人都是产品经理
人人都是产品经理
罗磊的独立博客
M
MIT News - Artificial intelligence
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
A
About on SuperTechFans
月光博客
月光博客
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
P
Palo Alto Networks Blog
K
Kaspersky official blog
Latest news
Latest news
C
Cisco Blogs
I
Intezer
L
Lohrmann on Cybersecurity
Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
The Cloudflare Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Martin Fowler
Martin Fowler
Engineering at Meta
Engineering at Meta
P
Privacy International News Feed
Scott Helme
Scott Helme
Google DeepMind News
Google DeepMind News
S
Security @ Cisco Blogs
T
The Blog of Author Tim Ferriss
S
Securelist
PCI Perspectives
PCI Perspectives
V
Visual Studio Blog
Simon Willison's Weblog
Simon Willison's Weblog

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备 快速了解iptables kafka中listener和advertised.listeners的作用 django rest_framework 分页 django后端服务、logstash和flink接入VictoriaMetrics指标监控 python中import原理 docker容器单机网络 手动实现docker容器bridge网络模型 mysql之MVCC原理 mysql之日志 使用java开发logstash的filter插件 使用python实现单例模式的三种方式 redis之缓存 redis之分片集群 redis之哨兵机制 redis之主从库同步 redis之持久化 redis之五种基本数据类型 pod中将代码与运行环境分离 ddt源码分析 python装饰器的使用方法 读书笔记:如何阅读一本书 使用ddt实现unittest的参数化测试 使用kubeadm安装k8s 优化gin表单的错误提示信息 gin中validator模块的源码分析 go简单使用grpc python简单使用grpc k8s之PV、PVC和StorageClass k8s之StatefulSet k8s之DaemonSet k8s之Job和CronJob k8s之ConfigMap和Secret k8s之Service k8s之Pod k8s之Deployment 容器的本质 docker容器 python迭代器与生成器 python元编程 python垃圾回收机制 python上下文管理器 django rest_framework使用jwt django rest_framework异常处理 django rest_framework 自定义文档 django压缩文件下载 django rest_framework使用pytest单元测试 django restframework choice 自定义输出数据 django Filtering 使用 django viewset 和 Router 配合使用时报的错 django model的序列化 django中使用AbStractUser django.core.exceptions.ImproperlyConfigured Application labels aren't unique, duplicates users django 中 media配置 django 外键引用自身和on_delete参数 django 警告 while time zone support is active Flask使用flask_socketio实现websocket flask结合mongo tornado 文件上传 tornado 使用jwt完成用户异步认证 tornado 用户密码 bcrypt加密 tornado 结合wtforms使用表单操作 tornado finish和write区别 tornado 使用peewee-async 完成异步orm数据库操作 pyspark streaming简介 和 消费 kafka示例 使用hue创建ozzie的pyspark action workflow count的性能优化 django rest_framework Authentication django celery 结合使用 网站
go中如何处理error
zhengwenfeng · 2022-11-14 · via 郑文峰的博客

# 0. 前言

go 中的异常处理和其他语言大不相同,像 Java、C++、python 等语言都是通过抛出 Exception 来处理异常,而 go 是通过返回 error 来判定异常,并进行处理。

在 go 中有 panic 的机制,但 panic 意味着程序终止,代码不能继续运行了,不能期望调用者来解决它。而 error 是预期中的异常,希望调用者可以对其进行处理的。

# 1. error 是什么?

举个例子,使用 Open 来打开文件,但是可能该路径的文件不存在,出现异常,在 go 是通过判断 err 是否为 nil 来判定打开文件是否成功。

f, err := os.Open(path)
if err != nil {
    // handle error
}

// do stuff

1
2
3
4
5
6

问题来了,error 是什么?

查看源码会发现,error 是一个包含 Error 方法的接口,返回的是实现了该接口的对象。

type error interface {  
   Error() string  
}

1
2
3

我们一般使用是通过 errors.New()来返回一个实现了 error 接口的对象。这个对象是一个包含了字符串的结构体,然后可以通过 Error 方法来获取字符串。

func New(text string) error {  
   return &errorString{text}  
}  
  
// errorString is a trivial implementation of error.
type errorString struct {  
   s string  
}  
  
func (e *errorString) Error() string {  
   return e.s  
}

1
2
3
4
5
6
7
8
9
10
11
12

我们可以注意到 New 方法返回的是 errorString 的地址,也就是说,在我们将两个 error 比较相等时,比较是地址,是两个 error 是否为同一个对象,而不是其中的错误字符串。

import (
	"errors"
	"fmt"
)

type errorString string

func (e errorString) Error() string {
	return string(e)
}

func New(text string) error {
	return errorString(text)
}

var ErrNamedType = New("EOF")
var ErrStructType = errors.New("EOF")

func main() {

	if ErrNamedType == New("EOF") {
		fmt.Println("Named Type Error")
	}

	if ErrStructType == errors.New("EOF") {
		fmt.Println("Struct Type Error")
	}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

输出:

# 2. 错误类型

# 2.1 Sentinel Error(预定义错误)

其实就是先预定义一些可以预料中的错误,在使用过程中,通过判断 error 是属于哪一种 error 并进行对应的处理。

举个栗子,在 io.EOF 就是一个预定义的错误,它是表示输入流中的结尾。

var EOF = errors.New("EOF")

1

在从流中读取字符的时候,会通过判断 error 是否等于 io.EOF 来判定是否读完。注意这里是判断 error 的指针是否相等。

n, err := reader.Read(p)  
if err != nil {  
   if err == io.EOF {  
      fmt.Println("The resource is read!")  
      break  
   }  
}

1
2
3
4
5
6
7

这种方式不建议使用,原因是:

  • 它会成为你 API 的公共部分

因为公共函数需要返回一个固定的 error,那么这个 error 就必须是公开的,那么就需要文档记录,这会增加 API 的表面积。

  • 增加调用者的耦合性

调用者必须要知道 io.EOF 这个 error ,并在调用的地方使用该 error 判断是否结束。

# 2.2 Error types(自定义错类型)

通过实现 error 接口来创建自定义错误类型。和 Sentinel Error 相比,是通过判断类型来知道是哪种错误,并且可以输出更多的上下文错误信息。

通过自定义 MyError,并实现 error 接口中的 Error 的方法。

type MyError struct {
	Msg  string
	File string
	Line int
}

func (e *MyError) Error() string {
	return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}

1
2
3
4
5
6
7
8
9

test 方法中返回的是自定义的 error,我们通过断言转换 error 成 MyError 类型,然后再输出更多的上下文信息。

func test() error {
	return &MyError{"Something happened", "server.go", 42}
}

func main() {
	err := test()
	switch err := err.(type) {
	case nil:
		// success
	case *MyError:
		fmt.Println("error occured on line:", err.Line)
	default:
		// unknown error
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在标准库 os.PathError 中,自定义了 PathError ,也是相同的用法。

type PathError struct {
	Op   string
	Path string
	Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

1
2
3
4
5
6
7
8

我们也尽可能避免使用 Error types,因为它和 Sentinel Erorr 一样会和调用者产生耦合,会作为 API 的一部分。

# 2.3 Opaque errors(不透明的错误)

Error types 是通过判断 error 的类型来走不同的逻辑,而 Opaque errors 是通过判断 error 的行为来走不同的逻辑。

在 net.Error 中定义如下,除了包含 error 外,还包含 Timeout 和 Temporary 方法。

type Error interface {
	error
	Timeout() bool 
	Temporary() bool
}

1
2
3
4
5

除了判断是否有 error 之外,还可以通过方法来判断是哪种类型的 error 然后进行对应的处理。

if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
	return false
}

if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
	return false
}

1
2
3
4
5
6
7

它不是扩展 error 更多的信息,而是扩展其方法。

# 3. 优雅的处理错误

# 3.1 无错误的正常流程代码

无错误的正常流程代码,将成为一条直线,而不是缩进的代码。

错误的写法:

// no
f, err := os.Open(path)
if err == nil {
    // do stuff
}

// handle error

1
2
3
4
5
6
7

正确的写法:

// ok
f, err := os.Open(path)
if err != nil {
    // handle error
}

// do stuff

1
2
3
4
5
6
7
8

# 3.2 减少不必要的判断

func AuthenticateRequest(r *Request) error {
    err := authenticate(r.User)
    err != nil {
        return err
    }
    return nil
}

1
2
3
4
5
6
7

改为:

func AuthenticateRequest(r *Request) error {
    return authenticate(r.User)
}

1
2
3

# 3.3 将 error 内部存储起来

err 在内部临时储存,在最后在返回出来。

下面例子中,通过循环读 reader 每一行的数据,每次判断 err 是不是 nil 来判断是否读完,如果是则退出循环,再返回。

func CountLines(r io.Reader)  (int, error) {
	var (
		br = bufio.NewReader(r)
		lines int
		err error
	)

	for {
		_, err = br.ReadString('\n')
		lines++
		if err != nil {
			break
		}
	}

	if err != io.EOF {
		return 0, err
	}

	return lines, nil
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

改进版本:

type Scanner struct {
	err          error     // Sticky error.
	...
}

func CountLines(r io.Reader)  (int, error) {
	sc := bufio.NewScanner(r)
	lines := 0

	for sc.Scan() {
		lines++
	}

	return lines, sc.Err()
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

每次循环都会判断 Scan 的返回值,当无内容返回时,则会返回 False,则结束循环,并返回结果。循环中出现的 error 会在 Scan 中通过 s.setErr(err) 保存在对象的 err 属性中。

代码明显简洁了许多。

# 3.4 将重复操作抽离出来

看看下面的代码,里面多次使用 fmt.Fprintf()并判断其返回值是否为 err

type Header struct {
	Key, Value string
}

type Status struct {
	Code   int
	Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
	_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
	if err != nil {
		return err
	}

	for _, h := range headers {
		_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
		if err != nil {
			return err
		}
	}

	if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
		return err
	}

	_, err = io.Copy(w, body)
	return err
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

我们创建一个 errWrite 结构体并实现 Write 方法,也就是在原来的 write 方法中包了一层并做好错误判断,然后在每一个 fmt.Fprintf 使用我们定义的 errWrite 进行写入,这样就达到了复用的效果,代码也好看了许多。

type errWrite struct {
	io.Writer
	err error
}

func (e *errWrite) Write(buf []byte) (int, error) {
	if e.err != nil {
		return 0, e.err
	}

	var n int
	n, e.err = e.Writer.Write(buf)
	return n, e.err
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
	ew := &errWrite{Writer: w}
	fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)

	for _, h := range headers {
		fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
	}

	fmt.Fprintf(w, "\r\n")

	io.Copy(ew, body)
	return ew.err
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 4. Wrap erros

在我们开发中,常常会在错误处理中,记录了日志,并且将错误给返回了。

os.Open 找不到文件时会返回 error,处理 error 时,将 error 的信息打上日志,并且将 err 进行返回,在 main 函数中,拿到 error 后再次打上 error 的日志,这个日志和上面有部分是重复的日志。

在代码调用链多的时候,会打上更多的重复日志,日志中出现非常多的噪音,非常影响排查错误。

func ReadFile(path string) ([]byte, error) {
	f, err := os.Open(path)
	if err != nil {
		log.Printf("could not open file: %v", err)
		return nil, err
	}
	defer f.Close()

	read := bufio.NewReader(f)

	line, _, err := read.ReadLine()
	return line, err
}

func main() {
	_, err := ReadFile("test.txt")
	if err != nil {
		fmt.Println(err)
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

运行输出:

2022/11/05 17:03:16 could not open file: open test.txt: The system cannot find t
he file specified.
2022/11/05 17:03:16 open test.txt: The system cannot find the file specified.

1
2
3

可以使用 fmt.Errorf 来对原始错误进行包装,除了原始错误信息之外,在添加额外得信息并返回。

f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("open file failed: %w", err)
	}

1
2
3
4

输出:

2022/11/05 17:04:43 open file failed: open test.txt: The system cannot find the
file specified.

1
2

fmt.Errorf 返回的是一个新的被包装的 error,errors.Is 可以一层一层的剥开包装来判断是否为原始错误,但是它是做指针判断的,这里 os.Open 返回的原始错误是 os.PathError 但是因为返回的是地址,所以无法用 errors.Is 判断。

func main() {

	_, err := ReadFile("test.txt")
	var pathError *os.PathError

	if errors.Is(err, pathError) {
		fmt.Println("is PathError")
	} else {
		fmt.Println("no PathError")
	}
}

1
2
3
4
5
6
7
8
9
10
11

输出:

这里可以用 errors.As 来判断 err 是否为 os.PathError 类型,即使 err 是地址。

这里判断了是否为 os.PathError 错误,并且将返回的 err 转换成了该错误,我们可以调用其中的属性来获取更多的信息。

func main() {

	_, err := ReadFile("test.txt")
	var pathError *os.PathError

	if errors.As(err, &pathError) {
		fmt.Println(pathError.Path)
	}

}

1
2
3
4
5
6
7
8
9
10
11

输出:

还可以通过 errors.UnWrap 来获取底层错误,将原始错误给解析出来。

func main() {
	_, err := ReadFile("test.txt")
	err = errors.Unwrap(err)
	fmt.Printf("ori err: %v", err)
}

1
2
3
4
5

# 5. pkg/errors

上面介绍的都是原生的 errors 处理模块,现在介绍 pkg/errors 模块,完全兼容原生 errors,并且对其进行增强,主要是添加了保存堆栈的能力。

可以使用 errors.Wrap 进行对 error 的包装,并添加额外的信息。

func ReadFile(path string) ([]byte, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, errors.Wrap(err, "could not open file")
	}
	defer f.Close()

	read := bufio.NewReader(f)

	line, _, err := read.ReadLine()
	return line, err
}

func main() {
	_, err := ReadFile("test.txt")
	if err != nil {
		fmt.Println(err)
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

输出:

could not open file: open test.txt: The system cannot find the file specified.

1

但它还有一个更强的功能是会保存当前的堆栈信息,使用%+v 可以打印出来。

func main() {
	_, err := ReadFile("test.txt")
	if err != nil {
		fmt.Printf("stack track: \n%+v", err)
	}
}

1
2
3
4
5
6

输出:

stack track:
open test.txt: The system cannot find the file specified.
could not open file
main.ReadFile
        D:/code/go_demo/main3.go:13
main.main
        D:/code/go_demo/main3.go:24
runtime.main
        D:/install/go18.3/src/runtime/proc.go:250
runtime.goexit
        D:/install/go18.3/src/runtime/asm_amd64.s:1571

1
2
3
4
5
6
7
8
9
10
11

如果不想保存堆栈信息,只添加额外的信息,可以使用 errors.WithMessage 添加。

	f, err := os.Open(path)
	if err != nil {
		return nil, errors.WithMessage(err, "could not open file")
	}

1
2
3
4

输出:

could not open file: open test.txt: The system cannot find the file specified.

1

还有几个常见的方法

// 生成错误的同时带上堆栈信息
func New(message string) error

// 只附加调用堆栈信息
func WithStack(err error) error

// 获得最根本的错误原因
func Cause(err error) error

1
2
3
4
5
6
7
8

# 6. error 的最佳实践

处理 error 的方式这么多,我们该如何最优的使用它们呢?有以下几个方法:

  • 在自己的应用代码中,使用 errors.New 或者 errors.Errorf 来返回错误
func parseArgs(args []string) error {
	if len(args) < 3 {
		return errors.Errorf("not enough arguments")
	}

	return nil
}

1
2
3
4
5
6
7

  • 如果调用其他包内的函数,通常简单的直接返回。
if err != nil {
	return err
}

1
2
3

  • 如果和其他库进行协作,考虑使用 errors.Wrap 或者 errors.Wrapf 保存堆栈信息。同样适用于和标准库协作的时候。
f, err := os.Open(path)
if err != nil {
	return errors.Wrapf(err, "failed to open %q", path)

1
2
3

  • 直接返回错误,而不是每个错误产生的地方到处打日志。

  • 在程序的顶部或者是工作的 goroutine 顶部(请求入口),使用 %+v 把堆栈详情记录。

func main() {
	err := app.Run()
	if err != nil {
		fmt.Printf("FATAL: %+V\n", err)
		os.Exit(1)
	}
}

1
2
3
4
5
6
7
8

  • 使用 errors.Cause 获取 root error,再进行和 sentinel error 判定。

# 参考资料