1、异常机制:panic和recover

编程语言一般都会有异常捕获机制,在 Python 中 是使用raise 和 try-except 语句来实现的异常抛出和异常捕获的。

在 Golang 中,有不少常规错误,在编译阶段就能提前告警,比如语法错误或类型错误等,但是有些错误仅能在程序运行后才能发生,比如数组访问越界、空指针引用等,这些运行时错误会引起程序退出。

当然能触发程序宕机退出的,也可以是我们自己,比如经过检查判断,当前环境无法达到我们程序进行的预期条件时(比如一个服务指定监听端口被其他程序占用),可以手动触发 panic,让程序退出停止运行。

  1. 触发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
  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

  1. 无法跨协程
    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
  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)
​
}

图片说明