11 Go错误处理
11.1 nil
函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:
package main
import (
"errors"
"fmt"
"math"
)
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}else {
return math.Sqrt(f),nil;
}
}
func main() {
result, err:= Sqrt(-1)
fmt.Println(result,err)
}
在上面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误。
11.2 接口实现
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
由于在9中说过接口实现不需要类似implements的关键字来实现,所以直接实现对于的接口方法即可。
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() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当被除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
11.3 panic与recover
panic 与 recover,一个用于主动抛出错误,一个用于捕获panic抛出的错误。
1.引发panic有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出。
2.发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,直到被recover捕获或运行到最外层函数。
3.panic不但可以在函数正常流程中抛出,在defer逻辑里也可以再次调用panic或抛出panic。defer里面的panic能够被后续执行的defer捕获。
4.recover用来捕获panic,阻止panic继续向上传递。recover()和defer一起使用,但是defer只有在后面的函数体内直接被掉用才能捕获panic来终止异常,否则返回nil,异常继续向外传递。
示例:多个panic只会捕捉最后一个
package main
import "fmt"
func main(){
defer func(){
if err := recover() ; err != nil {
fmt.Println(err)
}
}()//这里的()就是在调用匿名函数
defer func(){
panic("three")
}()
defer func(){
panic("two")
}()
panic("one")
}
输出:由于defer将函数链接后栈式执行,所以捕捉到了最早定义的three。
输出:three
示例二:
package main
import (
"fmt"
)
func main() {
GO()
PHP()
PYTHON()
}
//Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,
// 将异常与控制结构混在一起会很容易使得代码变得混乱。因为开发者很容易滥用异常,甚至一个小小的错误都抛出一个异常。
// 在Go语言中,使用多值返回来返回错误。不要用异常代替错误,更不要用来控制流程。在极个别的情况下,也就是说,遇到真正的异常的
// 情况下(比如除数为0了)。才使用Go中引入的Exception处理:defer, panic, recover。
//Go没有异常机制,但有panic/recover模式来处理错误
//Panic可以在任何地方引发,但recover只有在defer调用的函数中有效
func GO() {
fmt.Println("1. 我是GO,现在没有发生异常,我是正常执行的。")
}
func PHP() {
// 必须要先声明defer,否则不能捕获到panic异常,也就是说要先注册函数,后面有异常了,才可以调用
defer func() {
if err := recover(); err != nil {
fmt.Println("2. 终于捕获到了panic产生的异常:", err) // 这里的err其实就是panic传入的内容
fmt.Println("3. 我是defer里的匿名函数,我捕获到panic的异常了,我要recover,恢复过来了。")
}
}() //注意这个()就是调用该匿名函数的,不写会报expression in defer must be function call
// panic一般会导致程序挂掉(除非recover) 然后Go运行时会打印出调用栈
//但是,关键的一点是,即使函数执行的时候panic了,函数不往下走了,运行时并不是立刻向上传递panic,而是到defer那,
// 等defer的东西都跑完了,panic再向上传递。所以这时候 defer 有点类似 try-catch-finally 中的 finally。
// panic就是这么简单。抛出个真正意义上的异常。
panic("4. 我是PHP,我要抛出一个异常了,等下defer会通过recover捕获这个异常,捕获到我时,在PHP里是不会输出的," +
"会在defer里被捕获输出,然后正常处理,使后续程序正常运行。但是注意的是,在PHP函数里,排在panic后面的代码也不会执行的。")
fmt.Println("5. 我是PHP里panic后面要打印出的内容。但是我是永远也打印不出来了。因为逻辑并不会恢复到panic那个点去," +
"函数还是会在defer之后返回,也就是说执行到defer后,程序直接返回到main()里,接下来开始执行PYTHON()")
}
func PYTHON() {
fmt.Println("6. 我是PYTHON,没有defer来recover捕获panic的异常,我是不会被正常执行的。")
}
输出:
1. 我是GO,现在没有发生异常,我是正常执行的。
2. 终于捕获到了panic产生的异常: 4. 我是PHP,我要抛出一个异常了,等下defer会通过recover捕获这个异常,捕获到我时,在PHP里是不会输出的,会在defer里被捕获输出,然后正常处理,使后续程序正常运行。但是注意的是,在PHP函数里,排在panic后面的代码也不会执行的。
3. 我是defer里的匿名函数,我捕获到panic的异常了,我要recover,恢复过来了。
6. 我是PYTHON,没有defer来recover捕获panic的异常,我是不会被正常执行的。
12 Go并发
12.1 goroutine
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:
go f(x, y, z)
f(x, y, z)
示例:
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
//这里是为了让主协程进行休眠,使得go say()拥有足够的时间得以在主协程退出之前执行。
time.Sleep(100 * time.Millisecond)
fmt.Print(s)
}
}
func main() {
go say("world ")
say("hello ")//两个gouroutine(线程?)在执行
}
12.2 通道
通道见Go语言学习笔记(一)第4节。
12.3 协程
1. 内存消耗方面
每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。
goroutine:2KB
线程:8MB
2. 线程和 goroutine 切换调度开销方面
线程/goroutine 切换开销方面,goroutine 远比线程小
线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新等。
goroutine:只有三个寄存器的值修改 - PC / SP / DX.
12.4 协程并发
协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。
在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃(所以不能将go应用在赋值语句中)。
12.4.1 Go关键字的疑问
示例代码:
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
结果如下:
这里可以看到,屏幕并没有输出Hello world goroutine。
main 函数在单独的协程中运行,这个协程称为主协程。
当创建一个Go协程时,创建这个Go协程的语句立即返回。与函数不同,程序流程不会等待Go协程结束再继续执行。程序流程在开启Go协程后立即返回并开始执行下一行代码,忽略Go协程的任何返回值。
在主协程存在时才能运行其他协程,主协程终止则程序终止,其他协程也将终止。
11.4.2 Channel的解决方法
信道(Channel)可以被认为是协程之间通信的管道。与水流从管道的一端流向另一端一样,数据可以从信道的一端发送并在另一端接收。
特性
通过信道发送和接收数据默认是阻塞的。这是什么意思呢?当数据发送给信道后,程序流程在发送语句处阻塞,直到其他协程从该信道中读取数据。同样地,当从信道读取数据时,程序在读取语句处阻塞,直到其他协程发送数据给该信道。
信道的这种特性使得协程间通信变得高效,而不是向其他编程语言一样,显式的使用锁和条件变量来达到此目的
根据这个特性,我们可以进一步来避免掉有锁编程。
package main
import (
"fmt"
)
func hello(done chan bool) {
fmt.Println("Hello world goroutine")
done <- true
}
func main() {
done := make(chan bool)
go hello(done)
<-done
fmt.Println("main function")
}
在上面的程序中,我们在第 12 行定义了一个 bool 类型的信道 done,然后将它作为参数传递给 hello 协程。在第 14 行,我们从信道 done 中读取数据。程序将在这一行被阻塞直到其他协程向信道 done 里写入数据,在未读取到数据之前程序将在这一行一直等待而不会执行下一行语句。因此这里消除了在原程序中使用 time.Sleep 来阻止主协程退出的必要。
关于Go的基础知识就初步写到这里,没有涉及的点应该在于defer,这个后续有时间再修改吧。最近的工作接触到很多比较有意思的框架和语言,借用Go社区老哥的一句话,不同的语言所做的工作可能是不一样的,比如你搞java,可能更多的是企业级开发,大数据,你搞c可能更多是智能家居,网络通信,你搞php可能是网站建设,你搞go可能是云计算,所以换一门语言有时候不仅仅是换一门语言,换的是你的未来。我了解到Go是因为InfluxDB和docker,InfluxDB确实性能强悍,docker有多火热更不用说。Go的各种特性确实是适合云的,https://cloud.tencent.com/developer/article/1144911,但是也有自己的短板,go对泛型的支持不够,以及缺少像python一样丰富的库,诸位看官仁者见仁,智者见智。