『GoLang』错误处理
Go 没有像 Java 和 .NET 那样的 try/catch
异常机制:不能执行抛异常操作。但是有一套 defer-panic-and-recover
机制。
Go 的设计者觉得 try/catch
机制的使用太泛滥了,而且从底层向更高的层级抛异常太耗费资源。他们给 Go 设计的机制也可以 “捕捉” 异常,但是更轻量,并且只应该作为(处理错误的)最后的手段。
Go 是怎么处理普通错误的呢?通过在函数和方法中返回错误对象作为它们的唯一或最后一个返回值——如果返回 nil
,则没有错误发生——并且主调(calling)函数总是应该检查收到的错误。
处理错误并且在函数发生错误的地方给用户返回错误信息:照这样处理就算真的出了问题,你的程序也能继续运行并且通知给用户。panic and recover
是用来处理真正的异常(无法预测的错误)而不是普通的错误。
库函数通常必须返回某种错误提示给主调(calling)函数。
Go 检查和报告错误条件的惯有方式:
- 产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是
nil
就是成功,非nil
就是发生了错误。 - 为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。
if value, err := pack1.Func1(param1); err != nil {
fmt.Printf("Error %s in pack1.Func1 with parameter %v", err.Error(), param1)
return // or: return err
} else {
// Process(value)
}
为了更清晰的代码,应该总是使用包含错误值变量的
if
复合语句
错误处理
Go 有一个预先定义的 error
接口类型
type error interface {
Error() string
}
错误值用来表示异常状态,errors 包中有一个 errorString
结构体实现了 error
接口。当程序处于错误状态时可以用 os.Exit(1)
来中止运行。
定义错误
任何时候当你需要一个新的错误类型,都可以用 errors
(必须先 import)包的 errors.New
函数接收合适的错误信息来创建,像下面这样:
err := errors.New("math - square root of negative number")
示例:
// errors.go
package main
import (
"errors"
"fmt"
)
var errNotFound error = errors.New("Not found error")
func main() {
fmt.Printf("error: %v", errNotFound)
}
// error: Not found error
可以把它用于计算平方根函数的参数测试:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New ("math - square root of negative number")
}
// implementation of Sqrt
}
你可以像下面这样调用 Sqrt 函数:
if f, err := Sqrt(-1); err != nil {
fmt.Printf("Error: %s\n", err)
}
由于
fmt.Printf
会自动调用String()
方法 ,所以错误信息 “Error: math - square root of negative number” 会打印出来。通常(错误信息)都会有像 “Error:” 这样的前缀,所以你的错误信息不要以大写字母开头。
在大部分情况下自定义错误结构类型很有意义的,可以包含除了(低层级的)错误信息以外的其它有用信息,例如,正在进行的操作(打开文件等),全路径或名字。看下面例子中 os.Open
操作触发的 PathError
错误:
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": "+ e.Err.Error()
}
如果有不同错误条件可能发生,那么对实际的错误使用类型断言或类型判断(type-switch)是很有用的,并且可以根据错误场景做一些补救和恢复操作。
// err != nil
if e, ok := err.(*os.PathError); ok {
// remedy situation
}
或:
switch err := err.(type) {
case ParseError:
PrintParseError(err)
case PathError:
PrintPathError(err)
...
default:
fmt.Printf("Not a special error, just %s\n", err)
}
用 fmt 创建错误对象
通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf()
来实现:它和 fmt.Printf()
完全一样,接收一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。
比如在前面的平方根例子中使用:
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
第二个例子:从命令行读取输入时,如果加了 help 标志,我们可以用有用的信息产生一个错误:
if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
err = fmt.Errorf("usage: %s infile.txt outfile.txt", filepath.Base(os.Args[0]))
return
}
运行时异常和 panic
当发生像数组下标越界或类型断言失败这样的运行错误时,Go 运行时会触发运行时 panic,伴随着程序的崩溃抛出一个 runtime.Error
接口类型的值。这个错误值有个 RuntimeError()
方法用于区别普通错误。
panic
可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用 panic
函数产生一个中止程序的运行时错误。panic
接收一个做任意类型的参数,通常是字符串,在程序死亡时被打印出来。Go 运行时负责中止程序并给出调试信息。
package main
import "fmt"
func main() {
fmt.Println("Starting the program")
panic("A severe error occurred: stopping the program!")
fmt.Println("Ending the program")
}
输出如下:
Starting the program
panic: A severe error occurred: stopping the program!
panic PC=0x4f3038
runtime.panic+0x99 /go/src/pkg/runtime/proc.c:1032
runtime.panic(0x442938, 0x4f08e8)
main.main+0xa5 E:/Go/GoBoek/code examples/chapter 13/panic.go:8
main.main()
runtime.mainstart+0xf 386/asm.s:84
runtime.mainstart()
runtime.goexit /go/src/pkg/runtime/proc.c:148
runtime.goexit()
---- Error run E:/Go/GoBoek/code examples/chapter 13/panic.exe with code Crashed
---- Program exited with code -1073741783
一个检查程序是否被已知用户启动的具体例子:
var user = os.Getenv("USER")
func check() {
if user == "" {
panic("Unknown user: no value for $USER")
}
}
可以在导入包的
init()
函数中检查这些。
当发生错误必须中止程序时,panic
可以用于错误处理模式:
if err != nil {
panic("ERROR occurred:" + err.Error())
}
Go panicking:
在多层嵌套的函数调用中调用 panic
,可以马上中止当前函数的执行,所有的 defer
语句都会保证执行并把控制权交还给接收到 panic
的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer
,在栈顶处程序崩溃,并在命令行中用传给 panic
的值报告错误情况:这个终止过程就是 panicking
。
不能随意地用
panic
中止程序,必须尽力补救错误让程序能继续执行。
从 panic 中恢复(Recover)
正如名字一样,这个recover
内建函数被用于从 panic
或 错误场景中恢复:让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行
recover
只能在 defer
修饰的函数中使用:用于取得 panic
调用中传递过来的错误值,如果是正常执行,调用 recover
会返回 nil,且没有其它效果。
panic
会导致栈被展开直到 defer
修饰的 recover()
被调用或者程序中止。
下面例子中的 protect
函数调用函数参数 g
来保护调用者防止从 g
中抛出的运行时 panic
,并展示 panic
中的信息:
func protect(g func()) {
defer func() {
log.Println("done")
// Println executes normally even if there is a panic
if err := recover(); err != nil {
log.Printf("run time panic: %v", err)
}
}()
log.Println("start")
g() // possible runtime-error
}
这是一个展示 panic,defer 和 recover 怎么结合使用的完整例子:
// panic_recover.go
package main
import (
"fmt"
)
func badCall() {
panic("bad end")
}
func test() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("Panicing %s\r\n", e)
}
}()
badCall()
fmt.Printf("After bad call\r\n") // <-- wordt niet bereikt
}
func main() {
fmt.Printf("Calling test\r\n")
test()
fmt.Printf("Test completed\r\n")
}
输出:
Calling test
Panicing bad end
Test completed
直观地看,抛出
panic
后,直接进入defer
函数内,如果有recover()
就继续执行,没有直接抛错
defer-panic-recover
在某种意义上也是一种像if
,for
这样的控制流机制。
自定义包中的错误处理和 panicking
这是所有自定义包实现者应该遵守的最佳实践:
1)在包内部,总是应该从 panic
中 recover
:不允许显式的超出包范围的 panic()
2)向包的调用者返回错误值(而不是 panic
)
在包内部,特别是在非导出函数中有很深层次的嵌套调用时,将
panic
转换成error
来告诉调用方为何出错,是很实用的(且提高了代码可读性)。
下面的代码则很好地阐述了这一点。
// parse.go
package parse
import (
"fmt"
"strings"
"strconv"
)
// A ParseError indicates an error in converting a word into an integer.
type ParseError struct {
Index int // The index into the space-separated list of words.
Word string // The word that generated the parse error.
Err error // The raw error that precipitated this error, if any.
}
// String returns a human-readable error message.
func (e *ParseError) String() string {
return fmt.Sprintf("pkg parse: error parsing %q as int", e.Word)
}
// Parse parses the space-separated words in in put as integers.
func Parse(input string) (numbers []int, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("pkg: %v", r)
}
}
}()
fields := strings.Fields(input)
numbers = fields2numbers(fields)
return
}
func fields2numbers(fields []string) (numbers []int) {
if len(fields) == 0 {
panic("no words to parse")
}
for idx, field := range fields {
num, err := strconv.Atoi(field)
if err != nil {
panic(&ParseError{idx, field, err})
}
numbers = append(numbers, num)
}
return
}
// panic_package.go
package main
import (
"fmt"
"./parse/parse"
)
func main() {
var examples = []string{
"1 2 3 4 5",
"100 50 25 12.5 6.25",
"2 + 2 = 4",
"1st class",
"",
}
for _, ex := range examples {
fmt.Printf("Parsing %q:\n ", ex)
nums, err := parse.Parse(ex)
if err != nil {
fmt.Println(err) // here String() method from ParseError is used
continue
}
fmt.Println(nums)
}
}
输出:
Parsing "1 2 3 4 5":
[1 2 3 4 5]
Parsing "100 50 25 12.5 6.25":
pkg: pkg parse: error parsing "12.5" as int
Parsing "2 + 2 = 4":
pkg: pkg parse: error parsing "+" as int
Parsing "1st class":
pkg: pkg parse: error parsing "1st" as int
Parsing "":
pkg: no words to parse
『GoLang』错误处理的更多相关文章
- 『Golang』Martini框架入门
本文介绍golang中的优秀web开发框架martini! 序 Martini框架是使用Go语言作为开发语言的一个强力的快速构建模块化web应用与服务的开发框架.Martini是一个专门用来处理Web ...
- 『Golang』跨平台TUI(基于文字的用户界面)库Terbox-Go文档翻译
原文 package termbox import "github.com/nsf/termbox-go" termbox-go 是一个用于创建跨平台TUI(基于文本的用户界面)的 ...
- 『Golang』MongoDB在Golang中的使用(mgo包)
有关在Golang中使用mho进行MongoDB操作的最简单的例子.
- 『Golang』在Golang中使用json
由于要开发一个小型的web应用,而web应用大部分都会使用json作为数据传输的格式,所以有了这篇文章. 包引用 import ( "encoding/json" "gi ...
- 『Golang』—— 标准库之 os
Golang 的 os 库基本承袭 Unix 下 C 语言的用法 path 库: func Base(path string) string //取文件名,不含目录部分 func Dir(path s ...
- 『GoLang』fmt包的使用
目录 1. fmt 包初识 2. 格式化 verb 应用 2.1 通用 2.2 布尔值 2.3 整数 2.4 浮点数与复数 2.5 字符串和 []byte 2.6 指针 2.7 其他 flag 2.8 ...
- 『GoLang』string及其相关操作
目录 1. 字符串简介 2. 字符串的拼接 3. 有关 string 的常用处理 3.1 strings 包 3.1.1 判断两个 utf-8 编码字符串是否相同 3.1.2 判断字符串 str 是否 ...
- 『GoLang』面向对象
我们总结一下前面看到的:Go 没有类,而是松耦合的类型.方法对接口的实现. 面向对象语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢? Go实现面向对象的两个关键是stru ...
- 『GoLang』接口
接口是什么 Go 语言不是一种 "传统" 的面向对象编程语言:它里面没有类和继承的概念. 但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性.接口提供了一 ...
随机推荐
- 常用正则表达式最强汇总(含Python代码举例讲解+爬虫实战)
大家好,我是辰哥~ 本文带大家学习正则表达式,并通过python代码举例讲解常用的正则表达式 最后实战爬取小说网页:重点在于爬取的网页通过正则表达式进行解析. 正则表达式语法 Python的re模块( ...
- wpf 滚动文字 跑马灯
有时候也会有用,比如我的软件界面 放不下全长的文字时.或者状态栏显示一些时间,地点,温度,湿度等等这些东西 代码链接 https://gitee.com/csszbb/wpfnet5 这属于WPF ...
- 根据当前设备的宽度,动态计算出rem的换算比例,实现页面中元素的等比缩放
~function anonymous(window){ //根据当前设备的宽度,动态计算出rem的换算比例,实现页面中元素的等比缩放 let computedREM = function compu ...
- Linux中的静态库与动态库
什么是库文件? 库文件是事先编译好的方法的合集.比如:我们提前写好一些数据公式的实现,将其打包成库文件,以后使用只需要库文件就可以,不需要重新编写. Linux系统中: 1.静态库的扩展名为.a:2. ...
- C#多线程---I/O线程实现异步请求
一.场景 利用I/O线程来模拟浏览器对服务器请求的异步操作. 二.例子 1 using System; 2 using System.Collections.Generic; 3 using Syst ...
- 关于Ubuntu18.04 linux系统下使用Tim QQ 微信
先配上张图 步骤: 1.1 :需要安装环境deepin-wine 1.1:(你把他理解为jdk就好,没有jdk无法运行java程序,同理没有deepin-wine环境无法运行腾讯产品) 1.2 :去哪 ...
- C# 启动 Flask for Python
概览 最近有个需求是通过c#代码来启动 python 脚本.嘿~嘿!!! 突发奇想~~既然可以启动 python 脚本,那也能启动 flask,于是开始着手操作. 先看gif图 准备 因为使用的是.N ...
- 工作多年后再来聊聊IO
IO模型 IO是Input/Output的缩写.Linix网络编程中有五种IO模型: blocking IO(阻塞IO) nonblocking IO(非阻塞IO) IO multiplexing(多 ...
- validity属性返回对象中的属性值
- 利用元数据提高 SQLFlow 血缘分析结果准确率
利用元数据提高 SQLFlow 血缘分析结果准确率 一.SQLFlow--数据治理专家的一把利器 数据血缘属于数据治理中的一个概念,是在数据溯源的过程中找到相关数据之间的联系,它是一个逻辑概念.数据治 ...