1、异常机制:panic和recover
编程语言一般都会有异常捕获机制,在 Python 中 是使用raise 和 try-except 语句来实现的异常抛出和异常捕获的。
在 Golang 中,有不少常规错误,在编译阶段就能提前告警,比如语法错误或类型错误等,但是有些错误仅能在程序运行后才能发生,比如数组访问越界、空指针引用等,这些运行时错误会引起程序退出。
当然能触发程序宕机退出的,也可以是我们自己,比如经过检查判断,当前环境无法达到我们程序进行的预期条件时(比如一个服务指定监听端口被其他程序占用),可以手动触发 panic,让程序退出停止运行。
- 触发panic
手动触发宕机,是非常简单的一件事,只需要调用 panic 这个内置函数即可func main() { panic("crash") } //运行后,直接报错宕机 $ go run main.go go run main.go panic: crash goroutine 1 [running]: main.main() E:/Go-Code/main.go:4 +0x40 exit status 2
- 捕获 panic
发生了异常,有时候就得捕获,就像 Python 中的except 一样,那 Golang 中是如何做到的呢?
这就不得不引出另外一个内建函数 – recover,它可以让程序在发生宕机后起死回生。
但是 recover 的使用,有一个条件,就是它必须在 defer 函数中才能生效,其他作用域下,它是不工作的。
func set_data(x int) { defer func() { // recover() 可以将捕获到的panic信息打印 if err := recover(); err != nil { fmt.Println(err) } }() // 故意制造数组越界,触发 panic var arr [10]int arr[x] = 88 } func main() { set_data(20) // 如果能执行到这句,说明panic被捕获了 // 后续的程序能继续运行 fmt.Println("everything is ok") } //运行后,输出如下 $ go run main.go runtime error: index out of range [20] with length 10 everything is ok
通常来说,不应该对进入 panic 宕机的程序做任何处理,但有时,需要我们可以从宕机中恢复,至少我们可以在程序崩溃前,做一些操作,举个例子,当 web 服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态,如果 web 服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
从上面的例子,可以看到,即使 panic 会导致整个程序退出,但在退出前,若有 defer 延迟函数,还是得执行完 defer
无法跨协程
defer 在多个协程之间是没有效果,在子协程里触发 panic,只能触发自己协程内的 defer,而不能调用 main 协程里的 defer 函数的。func main() { // 这个 defer 并不会执行 defer fmt.Println("in main") go func() { defer println("in goroutine") panic("") }() time.Sleep(2 * time.Second) } //输出如下 in goroutine panic: goroutine 6 [running]: main.main.func1() E:/Go-Code/main.go:12 +0x7b created by main.main E:/Go-Code/main.go:10 +0xbc exit status 2
总结一下
Golang 异常的抛出与捕获,依赖两个内置函数:
panic:抛出异常,使程序崩溃
recover:捕获异常,恢复程序或做收尾工作
revocer 调用后,抛出的 panic 将会在此处终结,不会再外抛,但是 recover,并不能任意使用,它有强制要求,必须得在 defer 下才能发挥用途。
2、golang中的类型断言
主要功能有两个:
1、检查i是否为nil
2、检查i存储的值是否为某个类型
具体使用方式有两种:
(1)第一种使用方式
t := i.(T)
这个表达式可以断言一个接口对象(i)是不是 nil,并且接口对象(i)存储的值的类型是 T,如果断言成功,就会返回值给 t,如果断言失败,就会触发 panic
func main() { var i interface{} = 10 t1 := i.(int) fmt.Println(t1) fmt.Println("=====分隔线=====") t2 := i.(string) fmt.Println(t2) } //output 10 =====分隔线===== panic: interface conversion: interface {} is int, not string goroutine 1 [running]: main.main() E:/GoPlayer/src/main.go:12 +0x10e exit status 2
如果要断言的接口值是nil,也会触发panic
func main() { var i interface{} // nil var _ = i.(interface{}) } //output panic: interface conversion: interface is nil, not interface {} goroutine 1 [running]: main.main() E:/GoPlayer/src/main.go:5 +0x34 exit status 2
(2)第二种使用方式
t,ok := i.(T)
这个表达式也是可以断言一个接口对象(i)是不是 nil,并且接口对象(i)存储的值的类型是 T,如果断言成功,就会返回其类型给 t,并且此时 ok 的值 为 true,表示断言成功。
如果接口值的类型,并不是我们所断言的 T,就会断言失败,但和第一种表达式不同的事,这个不会触发 panic,而是将 ok 的值设为 false ,表示断言失败,此时t 为 T 的零值。
func main() { var i interface{} = 10 t1, ok := i.(int) fmt.Printf("%d-%t\n", t1, ok) fmt.Println("=====分隔线1=====") t2, ok := i.(string) fmt.Printf("%s-%t\n", t2, ok) fmt.Println("=====分隔线2=====") var k interface{} // nil t3, ok := k.(interface{}) fmt.Println(t3, "-", ok) fmt.Println("=====分隔线3=====") k = 10 t4, ok := k.(interface{}) fmt.Printf("%d-%t\n", t4, ok) t5, ok := k.(int) fmt.Printf("%d-%t\n", t5, ok) } //可以发现在执行第二次断言的时候,虽然失败了,但并没有触发了 panic。 //output 10-true =====分隔线1===== -false =====分隔线2===== <nil> - false =====分隔线3===== 10-true 10-true
第二个断言的输出在-false 之前并不是有没有输出任何 t2 的值,而是由于断言失败,所以 t2 得到的是 string 的零值也是 "" ,它是零长度的,所以你看不到其输出
Type Switch
如果需要区分多种类型,可以使用 type switch 断言,这个将会比一个一个进行类型断言更简单、直接、高效
func findType(i interface{}) { switch x := i.(type) { case int: fmt.Println(x, "is int") case string: fmt.Println(x, "is string") case nil: fmt.Println(x, "is nil") default: fmt.Println(x, "not type matched") } } func main() { findType(10) // int findType("hello") // string var k interface{} // nil findType(k) findType(10.23) //float64 } //output 10 is int hello is string <nil> is nil 10.23 not type matched
额外说明一下:
如果你的值是 nil,那么匹配的是 case nil
如果你的值在 switch-case 里并没有匹配对应的类型,那么匹配的是 default 分支
此外,还有两点需要格外注意:
1、类型断言,仅能对静态类型为空接口(interface{})的对象进行断言,否则会抛出错误,具体内容可以参考:关于接口的三个“潜规则”
2、类型断言完成后,实际上会返回静态类型为你断言的类型的对象,而要清楚原来的静态类型为空接口类型(interface{}),这是 Go 的隐式转换。
3、fmt输入输出
输出
func Printf(format string, a ...interface{}) (n int, err error) 格式化打印
func Print(a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error) 打印后会换行
package main import ( "fmt" ) func main() { a := 100 //int b := 3.14 //float64 c := true // bool d := "Hello World" //string e := `Ruby` //string f := 'A' fmt.Printf("%T,%b\n", a, a) fmt.Printf("%T,%f\n", b, b) fmt.Printf("%T,%t\n", c, c) fmt.Printf("%T,%s\n", d, d) fmt.Printf("%T,%s\n", e, e) fmt.Printf("%T,%d,%c\n", f, f, f) fmt.Println("-----------------------") fmt.Printf("%v\n", a) fmt.Printf("%v\n", b) fmt.Printf("%v\n", c) fmt.Printf("%v\n", d) fmt.Printf("%v\n", e) fmt.Printf("%v\n", f) }
fmt包读取键盘输入
常用方法:
func Scan(a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
func Scanln(a ...interface{}) (n int, err error)
package main import ( "fmt" ) func main() { var x int var y float64 fmt.Println("请输入一个整数,一个浮点类型:") fmt.Scanln(&x,&y)//读取键盘的输入,通过操作地址,赋值给x和y 阻塞式 fmt.Printf("x的数值:%d,y的数值:%f\n",x,y) fmt.Scanf("%d,%f",&x,&y) fmt.Printf("x:%d,y:%f\n",x,y) }
bufio包读取键盘输入
package main import ( "fmt" "os" "bufio" ) func main() { fmt.Println("请输入一个字符串:") reader := bufio.NewReader(os.Stdin) s1, _ := reader.ReadString('\n') fmt.Println("读到的数据:", s1) }