跳至主要內容

错误处理


一、错误

在Go中有一部分函数总是能成功的运行。比如strings.Contains和strconv.FormatBool函数;对于大部分函数而言,永远无法确保能否成功运行。

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error类型是一个接口类型,这是它的定义:

type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

这里有一个错误处理的例子:

package main

import "errors"
import "fmt"

// 按照惯例,错误通常是最后一个返回值并且是 error 类型,一个内建的接口。
func f1(arg int) (int, error) {
	// errors.New 构造一个使用给定的错误信息的基本error 值。
	if arg == 42 {
		return -1, errors.New("can't work with 42")
	}
	// 返回错误值为 nil 代表没有错误。
	return arg + 3, nil
}

// 通过实现 Error 方法来自定义 error 类型是可以的。
// 这里使用自定义错误类型来表示上面的参数错误。
type argError struct {
	arg  int
	prob string
}

func (e *argError) Error() string {
	return fmt.Sprintf("%d - %s", e.arg, e.prob)
}
func f2(arg int) (int, error) {
	if arg == 42 {
		// 在这个例子中,我们使用 &argError 语法来建立一个新的结构体,并提供了 arg 和 prob 这个两个字段的值。
		return -1, &argError{arg, "can't work with it"}
	}
	return arg + 3, nil
}
func main() {
	// 下面的两个循环测试了各个返回错误的函数。
	//  注意在 if行内的错误检查代码,在 Go 中是一个普遍的用法。
	for _, i := range []int{7, 42} {
		if r, e := f1(i); e != nil {
			fmt.Println("f1 失败:", e)
		} else {
			fmt.Println("f1 工作:", r)
		}
	}
	for _, i := range []int{7, 42} {
		if r, e := f2(i); e != nil {
			fmt.Println("f2 失败:", e)
		} else {
			fmt.Println("f2 工作:", r)
		}
	}
	// 你如果想在程序中使用一个自定义错误类型中的数据,你需要通过类型断言来得到这个错误类型的实例。
	_, e := f2(42)
	if ae, ok := e.(*argError); ok {
		fmt.Println(ae.arg)
		fmt.Println(ae.prob)
	}
}

二、Deferred函数

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。

示例:

package main

import "fmt"
import "os"

func main() {
	// 假设我们想要创建一个文件,向它进行写操作,然后在结束时关闭它。
	// 这里展示了如何通过 defer 来做到这一切。
	f := createFile("D:/defer.txt") // f := createFile("/tmp/defer.txt")
	// 在 closeFile 后得到一个文件对象,我们使用 defer通过 closeFile 来关闭这个文件。这会在封闭函数(main)结束时执行,就是 writeFile 结束后。
	defer closeFile(f)
	writeFile(f)
}
func createFile(p string) *os.File {
	fmt.Println("creating")
	f, err := os.Create(p)
	if err != nil {
		panic(err)
	}
	return f
}
func writeFile(f *os.File) {
	fmt.Println("writing")
	fmt.Fprintln(f, "data")
}
func closeFile(f *os.File) {
	fmt.Println("closing")
	f.Close()
}

三、异常

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。

示例如下:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 我们将在这个网站中使用 panic 来检查预期外的错误。这个是唯一一个为 panic 准备的例子。
	panic("一个异常")

	// panic 的一个基本用法就是在一个函数返回了错误值但是我们并不知道(或者不想)处理时终止运行。
	// 这里是一个在创建一个新文件时返回异常错误时的panic 用法。
	fmt.Println("继续")
	_, err := os.Create("/tmp/file")
	if err != nil {
		panic(err)
	}
	// 运行程序将会引起 panic,输出一个错误消息和 Go 运行时栈信息,并且返回一个非零的状态码。
}

四、捕获异常

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。如果web服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

示例:

package main

import "fmt"

func main() {
	// 这里我们对异常进行了捕获
	defer func() {
		if p := recover(); p != nil {
			err := fmt.Errorf("internal error: %v", p)
			if err != nil {
				fmt.Println(err)
			}
		}
	}()

	// 我们将在这个网站中使用 panic 来检查预期外的错误。这个是唯一一个为 panic 准备的例子。
	panic("一个异常")

}
上次编辑于: