GoLang 异常处理

2018-09-15 Saturday     program , golang , linux

Golang 中的错误处理是一个被大家经常拿出来讨论的话题(另外一个是泛型),这里简单介绍其使用方法。

错误 VS. 异常

错误和异常是两个不同的概念,非常容易混淆,而通常是将其看做错误,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误。

  • 错误和异常如何区分?
  • 错误处理的方式有哪几种?
  • 什么时候需要使用异常终止程序?
  • 什么时候需要捕获异常?

如何区分

错误通常是指业务正常处理流程中出现了问题,例如文件打开失败、如参为空指针等;而异常非意料中的问题出现,例如内存申请失败等。

GoLang 中引入 error 类型作为错误处理的标准模式,类似于 C 中的错误码,可以逐层返回,直到被处理。

示例

例如,在 regexp 包中有两个函数 Compile MustCompile,它们的声明如下:

func Compile(expr string) (*Regexp, error)
func MustCompile(str string) *Regexp

同样的功能,不同的设计:

  • Compile() 基于错误处理设计,适用于用户输入场景,当用户输入的正则表达式不合法时,该函数会返回一个错误。
  • MustCompile() 基于异常处理设计,适用于硬编码场景,调用者明确知道输入不会引起函数错误,当出现异常时则直接触发 Panic 异常。

也就是说,必须要明确什么是错误什么是异常,否则很容易出现一切皆错误或一切皆异常的情况。

常见异常场景

  1. 空指针引用、内存访问越界(含下标越界)、除数为0;对于 C 来说会产生 SEGV 信号。
  2. 不应该出现的分支,比如default;通常用在测试阶段及时发现错误。
  3. 内存不足,对于这一场景其实很难恢复,要么保持原有功能,要么退出。

常见场景

简单介绍常见示例。

失败的原因单一

此时直接通过 bool 标示,而非使用 error

func CheckHostType(hostType string) error {
	switch hostType {
	case "virtual_machine":
		return nil
	case "bare_metal":
		return nil
	}
	return errors.New("CheckHostType ERROR:" + host_type)
}

可以看出,该函数失败的原因只有一个,重构一下代码。

func IsValidHostType(hostType string) bool {
	return hostType == "virtual_machine" || hostType == "bare_metal"
}

当导致失败的原因不止一个时,例如常见的 IO 操作,此时用户需要了解更多的错误信息,可以通过 error 返回。

无失败

常见的一些,例如对象已经申请,只需要设置成员等函数,就没有必要返回错误。

func setTenantId(id int) error {
	self.TenantId = id
	return nil
}

对于上面的函数设计,就会有下面的调用代码。

err := setTenantId(id)
if err != nil {
	// log && free resource
	return errors.New(...)
}

根据正确的姿势,重构一下代码。

func setTenantId(id) {
	self.TenantId = id
}

于是调用代码变为。

setTenantId(id)

错误值统一定义

可以在 Golang 的每个包中增加一个错误对象定义文件,如下所示:

var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")

使用 defer

一般通过判断 error 的值来处理错误,如果当前操作失败,需要将本函数中已经创建的资源删除掉,示例代码如下:

func deferDemo() error {
	err := createResource1()
	if err != nil {
		return ERR_CREATE_RESOURCE1_FAILED
	}

	err = createResource2()
	if err != nil {
		destroyResource1()
		return ERR_CREATE_RESOURCE2_FAILED
	}

	err = createResource3()
	if err != nil {
		destroyResource1()
		destroyResource2()
		return ERR_CREATE_RESOURCE3_FAILED
	}

	err = createResource4()
	if err != nil {
		destroyResource1()
		destroyResource2()
		destroyResource3()
		return ERR_CREATE_RESOURCE4_FAILED
	}
	return nil
}

当 GoLang 的代码执行时,如果遇到 defer 的闭包调用,则压入堆栈;当函数返回时,会按照后进先出的顺序调用闭包。

闭包的参数是值传递,而对于外部变量却是引用传递,所以闭包中的外部变量 err 的值就变成外部函数返回时最新的 err 值。

所以,根据这个结论,重构上面的示例代码。

func deferDemo() error {
	err := createResource1()
	if err != nil {
		return ERR_CREATE_RESOURCE1_FAILED
	}
	defer func() {
		if err != nil {
			destroyResource1()
		}
	}()

	err = createResource2()
	if err != nil {
		return ERR_CREATE_RESOURCE2_FAILED
	}
	defer func() {
		if err != nil {
			destroyResource2()
		}
	}()

	err = createResource3()
	if err != nil {
		return ERR_CREATE_RESOURCE3_FAILED
	}
	defer func() {
		if err != nil {
			destroyResource3()
		}
	}()

	err = createResource4()
	if err != nil {
		return ERR_CREATE_RESOURCE4_FAILED
	}
	return nil
}

其它

比较简单的示例。

error 类型位置

应放在返回值类型列表的最后,对于返回值类型 error,用来传递错误信息。

resp, err := http.Get(url)
if err != nil {
	return nill, err
}

bool 作为返回值类型时也一样。

value, ok := cache.Lookup(key)
if !ok {
	// ...cache[key] does not exist…
}

错误处理

GoLang 通过内置的错误接口提供了非常简单的错误处理机制,其中 error 类型是一个接口,其定义如下。

type error interface {
	Error() string
}

可以通过实现 Error() 返回具体的报错信息,简单示例如下。

package main

import (
        "errors"
        "fmt"
)

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

        return f, nil
}

func main() {
        if res, err := Sqrt(-1); err != nil {
                fmt.Println(err)
        } else {
                fmt.Println(res)
        }
}

在函数中,通过 errors.New() 新建并返回一个错误信息;在调用方,如果返回的结果为 nil 则输出错误,在 fmt 中处理 error 时会调用 Error() 方法输出错误信息。

异常处理

Go 追求的是简洁优雅,没有提供传统的 try ... catch ... finally 这种异常处理方式,引入的是 defer panic recover 。也就是在 Go 中抛出一个 panic 异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。

Go 对待异常 (准确说是panic) 态度是:没有全面否定异常的存在,但极不鼓励多用异常。

package main

import "fmt"

func main() {
        defer func() {
                if err := recover(); err != nil {
                        fmt.Println(err)
                }
                fmt.Println("Process panic done")
        }()

        foobar()
}

func foobar() {
        fmt.Println("Before panic")
        panic("Panicing ...")
        fmt.Println("After panic")
}

通过 go run main.go 执行会输出如下内容。

Before panic
Panicing ...
Process panic done

也就是说,在 Panic 之后的内容不会再执行。

参考



如果喜欢这里的文章,而且又不差钱的话,欢迎打赏个早餐 ^_^


About This Blog

Recent Posts

Categories

Related Links

  • RTEMS
    RTEMS
  • GNU
  • Linux Kernel
  • Arduino

Search


This Site was built by Jin Yang, generated with Jekyll, and hosted on GitHub Pages
©2013-2019 – Jin Yang