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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介:


import "os"
import "fmt"
.....


在这段代码里有几个点需要特别注意。第一个需要注意的是 os.Open()、f.Read() 函数返回了两个值,Go 语言不但允许函数返回两个值,三个值四个值都是可以的,只不过 Go 语言普遍没有使用多返回值的习惯,仅仅是在需要返回错误的时候才会需要两个返回值。除了错误之外,还有一个地方需要两个返回值,那就是字典,通过第二个返回值来告知读取的结果是零值还是根本就不存在。

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 来明确到底读了多少字节。

体验 Redis 的错误处理

上面读文件的例子并没有让读者感受到错误处理的不爽,下面我们要引入 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() 来恢复异常。

f5c0f0d0f47825f95265a1bfe8ad53117352b3f3图片


在 json 序列化过程中,逻辑上需要递归处理 json 内部的各种类型,每一种容器类型内部都可能会遇到不能序列化的类型。如果对每个函数都使用返回错误的方式来编写代码,会显得非常繁琐。所以在内置的 json 包里也使用了 panic,然后在调用的最外层包裹了 recover 函数来进行恢复,最终统一返回一个 error 类型。

a18160d82bfdcf8024b57df8b8cbfad7e9bb6124图片


你可以想象一下,内置 json 包的开发者在设计开发这个包的时候应该也是纠结的焦头烂额,最终还是使用了 panic 和 recover 来让自己的代码变的好看一些。

多个 defer 语句

有时候我们需要在一个函数里使用多次 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 语言最重要的特色功能 —— 通道与协程


原文发布时间为: 2018-11-29
本文作者: 老钱
本文来自云栖社区合作伙伴“ 码洞”,了解相关信息可以关注“ 码洞”。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2天前
|
安全 网络协议 Go
Go语言网络编程
【10月更文挑战第28天】Go语言网络编程
88 65
|
2天前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
22 13
|
3天前
|
Go 开发者
GO语言基础
【10月更文挑战第27天】GO语言基础
20 10
|
2天前
|
网络协议 安全 Go
Go语言的网络编程基础
【10月更文挑战第28天】Go语言的网络编程基础
16 8
|
1天前
|
Go
go语言的复数常量
【10月更文挑战第21天】
12 6
|
3天前
|
缓存 网络协议 Unix
Go语言网络编程技巧
【10月更文挑战第27天】Go语言网络编程技巧
19 8
|
3天前
|
网络协议 Go
Go语言网络编程的实例
【10月更文挑战第27天】Go语言网络编程的实例
11 7
|
1天前
|
Go
go语言的浮点型常量
【10月更文挑战第21天】
8 4
|
1天前
|
编译器 Go
go语言的整型常量
【10月更文挑战第21天】
6 3
|
2天前
|
Go
go语言编译时常量表达式
【10月更文挑战第20天】
10 3