(六)Go错误处理和资源管理

defer调用

defer调用也是一种流程控制语句,经常用来调用一些资源处理函数。确保调用在函数结束时发生,defer 调用必须出行在函数内,并且在该函数返回之前才会去执行 defer 调用的函数。

简单示例

func testdefer(){
    // defer在函数最后才去执行。
	defer fmt.Println("2")
	fmt.Println("1")
}
 
func main() {
	testdefer()
}
// 执行结果
// 1
// 2

多个 defer 语句时,defer执行顺序相当于栈,采用的是先进后出的顺序

func testdefer() {
    fmt.Println("1")
    defer fmt.Println("5")
    fmt.Println("2")
    defer fmt.Println("4")
    fmt.Println("3")
}
// 函数执行结果
// 1
// 2
// 3
// 4
// 5

方法也可以作为defer的调用

defer 实参取值

defer 调用函数的参数值,并不是在真正调用 defer 时确定的,而是在执行 defer 时确定的。

func testdefer() {
    s := 21
    defer fmt.Println(s)
    s = 20
    fmt.Println(s)
}
// 执行结果
// 20
// 21

package main

import "fmt"


func main() {
	a := 10
	b := 20
	defer func() {
		fmt.Println("匿名函数中a", a)
		fmt.Println("匿名函数中b", b)
	}()
	a = 100
	b = 200
    fmt.Println("main中a", a)
	fmt.Println("main中b", b)
}
// 执行结果
// main中a 100
// main中b 200
// 匿名函数中a 100
// 匿名函数中b 200

func main() {
	a := 10
	b := 20
	defer func(a, b int) {
		fmt.Println("匿名函数中a", a)
		fmt.Println("匿名函数中b", b)
	}(a, b)
	a = 100
	b = 200
    fmt.Println("main中a", a)
	fmt.Println("main中b", b)
}
// 执行结果
// main中a 100
// main中b 200
// 匿名函数中a 10
// 匿名函数中b 20

执行到 defer 语句的时候,s 还是21,并不是在程序后面改变的 20。

案例:

package main
func writeFile(filename string) {
    // 打开文件
    file, err := os.Create(filename)
	if err != nil {
        panic(err)
	}
    // 函数结束时关闭文件
	defer file.Close()

	writer := bufio.NewWriter(file)
    // 函数结束时刷新缓存
	defer writer.Flush()
	
    // 执行斐波那契列函数
	f := fib.Fibonacci()
	for i := 0; i < 20; i++ {
		fmt.Fprintln(writer, f())
	}
}
func main() {
	writeFile("fib.txt")
}

defer 调用保证调用的函数肯定会在函数结束之前被执行,即使程序报了panic中断,defer调用依旧会被执行,因此常用于资源释放defer 常用场景:

  • 打开文件,关闭文件
  • 加锁,解锁
  • 建立连接,释放连接

错误处理概念

Go 的错误使用 error 表示,是一个接口类型,通常跟返回值一起声明。通过判断 err != nil 来判断是否发生错误。

// 仅包含一个方法的 Error() string。所有实现该接口的类型都可以当作一个错误类型。
// Error() 方法给出了错误的描述。这意味着可以给所有数据类型都配备错误类型。
type error interface {
    Error() string
}

// 简单生成一个自定义的错误信息
errors.New("math: square root of negative number")

error类型是一个 Go 语言的内建类型。在这个接口类型的声明中只包含了一个方法Error。这个方法不接受任何参数,但是会返回一个string类型的结果,用于错误描述。所有实现该接口的类型都可以当作一个错误类型。
fmt.Println 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

使用errors.New函数生成error类型值是一种最基本的生成错误值的方式。调用它的时候传入一个由字符串代表的错误信息,它会给返回给我们一个包含了这个错误信息的error类型值。该值的静态类型当然是error,而动态类型则是一个在errors包中的,包级私有的类型*errorString

显然,errorString类型拥有的一个指针方法实现了error接口中的Error方法。这个方法在被调用后,会原封不动地返回我们之前传入的错误信息。实际上,error类型值的Error方法就相当于其他类型值的String方法。

package errors
// New() 函数初始化 errorString 并返回该结构体地址
func New(text string) error {
    return &errorString{text}
}
// errorString 是仅包含了一个字符串的结构体类型,同时实现了error接口
type errorString struct {
    s string
}
func (e *errorString) Error() string {
    return e.s
}
  • 当我们想通过模板化的方式生成错误信息,并得到错误值时,可以使用fmt.Errorf函数。

    fmt.Errorf() 函数签名:func Errorf(format string, a ...interface{}) error,它利用一个格式化的字符串,利用上述方法,返回一个签名。

    • 该函数所做的其实就是先调用fmt.Sprintf函数,得到确切的错误信息;再调用errors.New函数,得到包含该错误信息的error类型值,最后返回该值。

案例:

package main
import (
    "errors"
    "fmt"
)
// echo函数接受一个string类型的参数request,并会返回两个结果。
// 第一个结果response也是string类型的,它代表了这个函数正常执行后的结果值。
// 第二个结果err就是error类型的,它代表了函数执行出错时的结果值,同时也包含了具体的错误信息。
func echo(request string) (response string, err error) {
    if request == "" {
        // 生成error类型值
        err = errors.New("empty request")
        return
    }
    response = fmt.Sprintf("echo: %s", request)
    return
}

func main() {
    for _, req := range []string{"", "hello!"} {
        fmt.Printf("request: %s\n", req)
        // 每次调用echo函数之后都会把它返回的结果值赋给变量resp和err
        // 并且总是先检查err的值是否“不为nil”,如果是,就打印错误信息,否则就打印常规的响应信息。
        resp, err := echo(req)
        if err != nil {
            fmt.Printf("error: %s\n", err)
            continue
        }
        fmt.Printf("response: %s\n", resp)
    }
}

由于error是一个接口类型,所以即使同为error类型的错误值,它们的实际类型也可能不同。

样判断一个错误值具体代表的是哪一类错误?

这道题的典型回答是这样的:

  1. 对于类型在已知范围内的一系列错误值,一般使用类型断言表达式或类型switch语句来判断;
  2. 对于已有相应变量且类型相同的一系列错误值,一般直接使用判等操作来判断;
  3. 对于没有相应变量且类型未知的一系列错误值,只能使用其错误信息的字符串表示形式来做判断。

判断错误类型

  • 类型在已知范围内的错误值其实是最容易分辨的。

    • 就拿os包中的几个代表错误的类型os.PathErroros.LinkErroros.SyscallErroros/exec.Error来说,它们的指针类型都是error接口的实现类型,同时它们也都包含了一个名叫Err,类型为error接口类型的代表潜在错误的字段。

      func underlyingError(err error) error {
          switch err := err.(type) {
              case *os.PathError:
              	return err.Err
              case *os.LinkError:
              	return err.Err
              case *os.SyscallError:
              	return err.Err
              case *exec.Error:
              	return err.Err
          }
          return err
      }
      
  • 在错误值类型相同的情况下,无法采用上面的方式辨别了。在 Go 语言的标准库中也有不少以相同方式创建的同类型的错误值。

    • os包其中不少的错误值都是通过调用errors.New函数来初始化的,比如:os.ErrClosedos.ErrInvalid以及os.ErrPermission,等等。这几个都是已经定义好的、确切的错误值。
    • 如果我们在操作文件系统的时候得到了一个错误值,并且知道该值的潜在错误值肯定是上述值中的某一个,那么就可以用普通的switch语句去做判断,当然了,用if语句和判等操作符也是可以的。
    printError := func(i int, err error) {
        if err == nil {
            fmt.Println("nil error")
            return
        }
        err = underlyingError(err)
        switch err {
            case os.ErrClosed:
            	fmt.Printf("error(closed)[%d]: %s\n", i, err)
            case os.ErrInvalid:
            	fmt.Printf("error(invalid)[%d]: %s\n", i, err)
            case os.ErrPermission:
            	fmt.Printf("error(permission)[%d]: %s\n", i, err)
        }
    }
    
  • 如果我们对一个错误值可能代表的含义知之甚少,那么就只能通过它拥有的错误信息去做判断了。

    • 通过错误值的Error方法,拿到它的错误信息。其实os包中就有做这种判断的函数,比如:os.IsExistos.IsNotExistos.IsPermission

错误值设置

构建错误值体系的基本方式有两种,即:创建立体的错误类型体系创建扁平的错误值列表

错误类型体系

参考标准库的net代码包,有一个名为Error的接口类型。它算是内建接口类型error的一个扩展接口,因为errornet.Error的嵌入接口。net.Error扩展了 Timeout 和 Temporary 方法。

net包中有很多错误类型都实现了net.Error接口,比如:

  1. *net.OpError
  2. *net.AddrError
  3. net.UnknownNetworkError等等。

可以把这看做是一种多层分类的手段。当net包的使用者拿到一个错误值的时候,可以先判断它是否是net.Error类型的,是否是一个网络相关的错误。

如果是,还可以再进一步判断它的类型是哪一个更具体的错误类型,这样就能知道这个网络相关的错误具体是由于操作不当引起的,还是因为网络地址问题引起的,又或是由于网络协议不正确引起的。

这些错误类型的值之间还可以有另外一种关系,即:链式关系。比如说,使用者调用net.DialTCP之类的函数时,net包中的代码可能会返回给他一个*net.OpError类型的错误值,以表示由于他的操作不当造成了一个错误。同时,这些代码还可能会把一个*net.AddrErrornet.UnknownNetworkError类型的值赋给该错误值的Err字段,以表明导致这个错误的潜在原因。如果,此处的潜在错误值的Err字段也有非nil的值,那么将会指明更深层次的错误原因。如此一级又一级就像链条一样最终会指向问题的根源。

用类型建立起树形结构的错误体系,用统一字段建立起可追根溯源的链式错误关联。这是 Go 语言标准库给予我们的优秀范本,非常有借鉴意义

错误值列表

用于想预先创建一些代表已知错误的错误值时候。由于error是接口类型,所以通过errors.New函数生成的错误值只能被赋给变量,而不能赋给常量,又由于这些代表错误的变量需要给包外代码使用,所以其访问权限只能是公开的。

这就带来了一个问题,如果有恶意代码改变了这些公开变量的值,那么程序的功能就必然会受到影响。

解决方案:

  1. 先私有化此类变量,让它们的名称首字母变成小写,然后编写公开的用于获取错误值以及用于判等错误值的函数。

  2. syscall包中有一个类型叫做Errno,该类型代表了系统调用时可能发生的底层错误。这个错误类型是error接口的实现类型,同时也是对内建类型uintptr的再定义类型。

    由于uintptr可以作为常量的类型,所以syscall.Errno自然也可以。syscall包中声明有大量的Errno类型的常量,每个常量都对应一种系统调用错误。syscall包外的代码可以拿到这些代表错误的常量,但却无法改变它们。

panic 和 recover

Go程序异常被叫做 panic,翻译为运行时恐慌。其中的“恐慌”二字是由 panic 直译过来的,运行时表示异常只会在程序运行的时候被抛出来。

在 panic 中,包含了一个runtime.Error接口类型的值。runtime.Error接口内嵌了error接口并做了一点点扩展,runtime包中有不少它的实现类型。此外,panic 详情中一般还会包含与它的引发原因有关的 goroutine 的代码执行信息。

panic: runtime error: index out of range [5] with length 5

goroutine 1 [running]:
main.main()
	D:/Code/Go/go_study/src/article19/q1/demo.go:5 +0x1b

panic 引发程序停止过程

某个函数中的某行代码有意或无意地引发了一个 panic。初始的 panic 详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。

这也意味着,此行代码所属函数的执行随即终止。紧接着,控制权并不会在此有片刻停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。

这里的最外层函数指的是go函数,对于主 goroutine 来说就是main函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。

随后,程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic 详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。

内建函数panicrecover

内建函数panic是专门用于引发 panic 的。panic函数使程序开发者可以在程序运行期间报告异常。注意,这与从函数返回错误值的意义是完全不同的。

panic 详情会在控制权传播的过程中,被逐渐地积累和完善,并且,控制权会一级一级地沿着调用栈的反方向传播至顶端。

panicrecover 是 Go 的两个内置函数,这两个内置函数用于处理 Go 运行时的错误,panic 用于主动抛出错误,recover用来捕获 panic 抛出的错误。panicrecover 与其他语言中的 try-catch-finally 语句类似。

package main
import "fmt"
func main() {
    defer fmt.Println("宕机后要做的事情1")
    defer fmt.Println("宕机后要做的事情2")
    panic("宕机")
}

执行结果

宕机后要做的事情2
宕机后要做的事情1
panic: 宕机

goroutine 1 [running]:
main.main()
    D:/code/main.go:8 +0xf8
exit status 2
  • 引发panic两种情况:

    • 程序主动调用,
    • 程序产生运行时错误,由运行时检测并退出。
  • 发生panic后,程序立即返回,逐层向上执行函数的defer语句,直到被recover捕获或运行到最外层函数。

  • defer逻辑里还可以再次调用panic或抛出panicdefer里面的panic能够被后续执行的defer捕获。

  • recover用来捕获panic,阻止panic继续向上传递。recover()defer一起使用,但是defer只有在后面的函数体内直接被调用才能捕获panic来终止异常,否则返回nil,异常继续向外传递。

    defer recover()
    defer fmt.Prinntln(recover)
    defer func(){
        recover() // 有效
        func(){
            recover() //无效,嵌套两层
        }()
    }()
    

recover 让程序崩溃时继续执行

package main
import (
    "fmt"
    "runtime"
)
// 崩溃时需要传递的上下文信息
type panicContext struct {
    function string // 所在函数
}
// 保护方式允许一个函数
func ProtectRun(entry func()) {
    // 延迟处理的函数
    defer func() {
        // 发生宕机时,获取panic传递的上下文并打印
        err := recover()
        switch err.(type) {
        case runtime.Error: // 运行时错误
            fmt.Println("runtime error:", err)
        default: // 非运行时错误
            fmt.Println("error:", err)
        }
    }()
    entry()
}
func main() {
    fmt.Println("运行前")
    // 允许一段手动触发的错误
    ProtectRun(func() {
        fmt.Println("手动宕机前")
        // 使用panic传递上下文
        panic(&panicContext{
            "手动触发panic",
        })
        fmt.Println("手动宕机后")
    })
    // 故意造成空指针访问错误
    ProtectRun(func() {
        fmt.Println("赋值宕机前")
        var a *int
        *a = 1
        fmt.Println("赋值宕机后")
    })
    fmt.Println("运行后")
}

执行结果

运行前
手动宕机前
error: &{手动触发panic}
赋值宕机前
runtime error: runtime error: invalid memory address or nil pointer dereference
运行后

使用场景

一般情况下有两种情况用到:

  • 程序遇到无法执行下去的错误时,抛出错误,主动结束运行。
  • 在调试程序时,通过 panic 来打印堆栈,方便定位错误。

统一错误处理案例

新建目录:errorhanding目录下创建 filelistingserver 目录

handler.go ( 位置 errorhandling/filelistingserver/filelisting)

package filelisting
import (
	"io/ioutil"
	"net/http"
	"os"
	"strings"
)

var fib = "fib"
type userError string
func (e userError) Error() string {
	return e.Message()
}
func (e userError) Message() string {
	return string(e)
}

func HandleFileList(writer http.ResponseWriter, request *http.Request) error {
	if strings.Index(request.URL.Path, fib) == -1 {
		return userError("path must contains " + fib)
	}
	path := request.URL.Path[len("/list/"):]
	file, err := os.Open(path)
	if err != nil {
		return err // 发生错误返回错误
	}
	defer file.Close()
	// 读取文件
	all, err := ioutil.ReadAll(file)
	if err != nil {
		return err
	}
	writer.Write(all)
	return nil
}

web.go(位置 errorhandling/filelistingserver)

package main
type appHandler func(writer http.ResponseWriter, request *http.Request) error

func errWrapper(handler appHandler) func(writer http.ResponseWriter, request *http.Request) {
	return func(writer http.ResponseWriter, request *http.Request) {
		defer func() {
			if r := recover(); r != nil {
				log.Print(log.ERROR, "Panic: %v", r)
				http.Error(writer,
					http.StatusText(http.StatusInternalServerError),
					http.StatusInternalServerError)
			}
		}()

		err := handler(writer, request)

		if err != nil {
            // 统一处理错误
			log.Print(log.ERROR, "Error occured handling types: %s", err.Error())

			if userErr, ok := err.(userError); ok {
				http.Error(writer, userErr.Message(), http.StatusBadRequest)
				return
			}

			code := http.StatusOK
			switch {
			case os.IsNotExist(err):
				code = http.StatusNotFound
			case os.IsPermission(err):
				code = http.StatusForbidden
			default:
				code = http.StatusInternalServerError
			}
			http.Error(writer, http.StatusText(code), code)
		}
	}
}

type userError interface {
	error
	Message() string
}

func main() {
	http.HandleFunc("/list/", errWrapper(filelisting.HandleFileList))
	err := http.ListenAndServe(":8888", nil)
	if err != nil {
		panic(err)
	}
}

参考资料

Go错误类型及错误处理

Go语言宕机(panic)——程序终止运行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值