Go 语言的异常处理语法绝对是独树一帜,在我见过的诸多高级语言中,Go 语言的错误处理形式就是一朵奇葩。一方面它鼓励你使用 C 语言的形式将错误经过返回值来进行传递,另外一方面它还提供了高级语言通常都有的异常抛出和捕获的形式,可是又不鼓励你使用这个形式。后面咱们统一将返回值形式的称为「错误」,将抛出捕获形式的称为「异常」。git
Go 语言的错误处理在业界饱受批评,不过既然咱们已经入了这个坑,那仍是好好蹲着吧。github
Go 语言规定凡是实现了错误接口的对象都是错误对象,这个错误接口只定义了一个方法。redis
type error interface {
Error() string
}
复制代码
注意这个接口的名称,它是小写的,是内置的全局接口。一般一个名字若是是小写字母开头,那么它在包外就是不可见的,不过 error 是内置的特殊名称,它是全局可见的。json
编写一个错误对象很简单,写一个结构体,而后挂在 Error() 方法就能够了。bash
package main
import "fmt"
type SomeError struct {
Reason string
}
func (s SomeError) Error() string {
return s.Reason
}
func main() {
var err error = SomeError{"something happened"}
fmt.Println(err)
}
---------------
something happened
复制代码
对于上面代码中错误对象的形式很是经常使用,因此 Go 语言内置了一个通用错误类型,在 errors 包里。这个包还提供了一个 New() 函数让咱们方便地建立一个通用错误。app
package errors
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
复制代码
注意这个结构体 errorString 是首字母小写的,意味着咱们没法直接使用这个类型的名字来构造错误对象,而必须使用 New() 函数。函数
var err = errors.New("something happened")
复制代码
若是你的错误字符串须要定制一些参数,可以使用 fmt 包提供了 Errorf 函数性能
var thing = "something"
var err = fmt.Errorf("%s happened", thing)
复制代码
在 Java 语言里,若是遇到 IO 问题一般会抛出 IOException 类型的异常,在 Go 语言里面它不会抛异常,而是以返回值的形式来通知上层逻辑来处理错误。下面咱们经过读文件来尝试一下 Go 语言的错误处理,读文件须要使用内置的 os 包。ui
package main
import "os"
import "fmt"
func main() {
// 打开文件
var f, err = os.Open("main.go")
if err != nil {
// 文件不存在、权限等缘由
fmt.Println("open file failed reason:" + err.Error())
return
}
// 推迟到函数尾部调用,确保文件会关闭
defer f.Close()
// 存储文件内容
var content = []byte{}
// 临时的缓冲,按块读取,一次最多读取 100 字节
var buf = make([]byte, 100)
for {
// 读文件,将读到的内容填充到缓冲
n, err := f.Read(buf)
if n > 0 {
// 将读到的内容聚合起来
content = append(content, buf[:n]...)
}
if err != nil {
// 遇到流结束或者其它错误
break
}
}
// 输出文件内容
fmt.Println(string(content))
}
-------
package main
import "os"
import "fmt"
.....
复制代码
在这段代码里有几个点须要特别注意。第一个须要注意的是 os.Open()、f.Read() 函数返回了两个值,Go 语言不但容许函数返回两个值,三个值四个值都是能够的,只不过 Go 语言广泛没有使用多返回值的习惯,仅仅是在须要返回错误的时候才会须要两个返回值。除了错误以外,还有一个地方须要两个返回值,那就是字典,经过第二个返回值来告知读取的结果是零值仍是根本就不存在。spa
var score, ok := scores["apple"]
复制代码
第二个须要注意的是 defer 关键字,它将文件的关闭调用推迟到当前函数的尾部执行,即便后面的代码抛出了异常,文件关闭也会确保被执行,至关于 Java 语言的 finally 语句块。defer 是 Go 语言很是重要的特性,在平常应用开发中,咱们会常用到它。
第三个须要注意的地方是 append 函数参数中出现了 ... 符号。在切片章节,咱们知道 append 函数能够将单个元素追加到切片中,其实 append 函数能够一次性追加多个元素,它的参数数量是可变的。
var s = []int{1,2,3,4,5}
s = append(s,6,7,8,9)
复制代码
可是读文件的代码中须要将整个切片的内容追加到另外一个切片中,这时候就须要 ... 操做符,它的做用是将切片参数的全部元素展开后传递给 append 函数。你可能会担忧若是切片里有成百上千的元素,展开成元素再传递会不会很是耗费性能。这个没必要担忧,展开只是形式上的展开,在实现上其实并无展开,传递过去的参数本质上仍是切片。
第四个须要注意的地方是读文件操做 f.Read() ,它会将文件的内容往切片里填充,填充的量不会超过切片的长度(注意不是容量)。若是将缓冲改为下面这种形式,就会死循环!
var buf = make([]byte, 0, 100)
复制代码
另外若是遇到文件尾了,切片就不会填满。因此须要经过返回值 n 来明确到底读了多少字节。
上面读文件的例子并无让读者感觉到错误处理的不爽,下面咱们要引入 Go 语言 Redis 的客户端包,来真实体验一下 Go 语言的错误处理有多让人不快。
使用第三方包,须要使用 go get 指令下载这个包,该指令会将第三方包放到 GOPATH 目录下。
go get github.com/go-redis/redis
复制代码
下面我要实现一个小功能,获取 Redis 中两个整数值,而后相乘,再存入 Redis 中
package main
import "fmt"
import "strconv"
import "github.com/go-redis/redis"
func main() {
// 定义客户端对象,内部包含一个链接池
var client = redis.NewClient(&redis.Options {
Addr: "localhost:6379",
})
// 定义三个重要的整数变量值,默认都是零
var val1, val2, val3 int
// 获取第一个值
valstr1, err := client.Get("value1").Result()
if err == nil {
val1, err = strconv.Atoi(valstr1)
if err != nil {
fmt.Println("value1 not a valid integer")
return
}
} else if err != redis.Nil {
fmt.Println("redis access error reason:" + err.Error())
return
}
// 获取第二个值
valstr2, err := client.Get("value2").Result()
if err == nil {
val2, err = strconv.Atoi(valstr2)
if err != nil {
fmt.Println("value1 not a valid integer")
return
}
} else if err != redis.Nil {
fmt.Println("redis access error reason:" + err.Error())
return
}
// 保存第三个值
val3 = val1 * val2
ok, err := client.Set("value3",val3, 0).Result()
if err != nil {
fmt.Println("set value error reason:" + err.Error())
return
}
fmt.Println(ok)
}
------
OK
复制代码
由于 Go 语言中不轻易使用异常语句,因此对于任何可能出错的地方都须要判断返回值的错误信息。上面代码中除了访问 Redis 须要判断以外,字符串转整数也须要判断。
另外还有一个须要特别注意的是由于字符串的零值是空串而不是 nil,你很差从字符串内容自己判断出 Redis 是否存在这个 key 仍是对应 key 的 value 为空串,须要经过返回值的错误信息来判断。代码中的 redis.Nil 就是客户端专门为 key 不存在这种状况而定义的错误对象。
相比于写习惯了 Python 和 Java 程序的朋友们来讲,这样繁琐的错误判断简直太地狱了。不过仍是那句话,习惯了就好。
Go 语言提供了 panic 和 recover 全局函数让咱们能够抛出异常、捕获异常。它相似于其它高级语言里常见的 throw try catch 语句,可是又很不同,好比 panic 函数能够抛出来任意对象。下面咱们看一个使用 panic 的例子
package main
import "fmt"
var negErr = fmt.Errorf("non positive number")
func main() {
fmt.Println(fact(10))
fmt.Println(fact(5))
fmt.Println(fact(-5))
fmt.Println(fact(15))
}
// 让阶乘函数返回错误太不雅观了
// 使用 panic 会合适一些
func fact(a int) int{
if a <= 0 {
panic(negErr)
}
var r = 1
for i :=1;i<=a;i++ {
r *= i
}
return r
}
-------
3628800
120
panic: non positive number
goroutine 1 [running]:
main.fact(0xfffffffffffffffb, 0x1)
/Users/qianwp/go/src/github.com/pyloque/practice/main.go:16 +0x75
main.main()
/Users/qianwp/go/src/github.com/pyloque/practice/main.go:10 +0x122
exit status 2
复制代码
上面的代码抛出了 negErr,直接致使了程序崩溃,程序最后打印了异常堆栈信息。下面咱们使用 recover 函数来保护它,recover 函数须要结合 defer 语句一块儿使用,这样能够确保 recover() 逻辑在程序异常的时候也能够获得调用。
package main
import "fmt"
var negErr = fmt.Errorf("non positive number")
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("error catched", err)
}
}()
fmt.Println(fact(10))
fmt.Println(fact(5))
fmt.Println(fact(-5))
fmt.Println(fact(15))
}
func fact(a int) int{
if a <= 0 {
panic(negErr)
}
var r = 1
for i :=1;i<=a;i++ {
r *= i
}
return r
}
-------
3628800
120
error catched non positive number
复制代码
输出结果中的异常堆栈信息没有了,说明捕获成功了,不过即便程序再也不崩溃,异常点后面的逻辑也不会再继续执行了。上面的代码中须要注意的是咱们使用了匿名函数 func() {...}
defer func() {
if err := recover(); err != nil {
fmt.Println("error catched", err)
}
}()
复制代码
尾部还有个括号是怎么回事,为何还须要这个括号呢?它表示对匿名函数进行了调用。对比一下前面写的文件关闭尾部的括号就能理解了
defer f.Close()
复制代码
还有个值得注意的地方时,panic 抛出的对象未必是错误对象,而 recover() 返回的对象正是 panic 抛出来的对象,因此它也不必定是错误对象。
func panic(v interface{})
func recover() interface{}
复制代码
咱们常常还须要对 recover() 返回的结果进行判断,以挑选出咱们愿意处理的异常对象类型,对于那些不肯意处理的,能够选择再次抛出来,让上层来处理。
defer func() {
if err := recover(); err != nil {
if err == negErr {
fmt.Println("error catched", err)
} else {
panic(err) // rethrow
}
}
}()
复制代码
Go 语言官方表态不要轻易使用 panic recover,除非你真的没法预料中间可能会发生的错误,或者它能很是显著地简化你的代码。简单一点说除非逼不得已,不然不要使用它。
在一个常见的 Web 应用中,不能由于个别 URL 处理器抛出异常而致使整个程序崩溃,就须要在每一个 URL 处理器外面包括一层 recover() 来恢复异常。
知乎最近发表了内部的 Go 语言实践方案,由于忍受不了代码里太多的错误判断语句,它们的业务异常也改用 panic 抛出来,虽然这并非官方的推荐模式。
有时候咱们须要在一个函数里使用屡次 defer 语句。好比拷贝文件,须要同时打开源文件和目标文件,那就须要调用两次 defer f.Close()。
package main
import "fmt"
import "os"
func main() {
fsrc, err := os.Open("source.txt")
if err != nil {
fmt.Println("open source file failed")
return
}
defer fsrc.Close()
fdes, err := os.Open("target.txt")
if err != nil {
fmt.Println("open target file failed")
return
}
defer fdes.Close()
fmt.Println("do something here")
}
复制代码
须要注意的是 defer 语句的执行顺序和代码编写的顺序是反过来的,也就是说最早 defer 的语句最后执行,为了验证这个规则,咱们来改写一下上面的代码
package main
import "fmt"
import "os"
func main() {
fsrc, err := os.Open("source.txt")
if err != nil {
fmt.Println("open source file failed")
return
}
defer func() {
fmt.Println("close source file")
fsrc.Close()
}()
fdes, err := os.Open("target.txt")
if err != nil {
fmt.Println("open target file failed")
return
}
defer func() {
fmt.Println("close target file")
fdes.Close()
}()
fmt.Println("do something here")
}
--------
do something here
close target file
close source file
复制代码
下一节咱们开讲 Go 语言最重要的特点功能 —— 通道与协程
关注公众号「码洞」,回复 go 获取《快学 Go 语言》所有章节