Go语言之异常处理

  在编写Go语言代码的时候,我们应该习惯使用error类型值来表明非正常的状态。作为惯用法,在Go语言标准库代码包中的很多函数和方法也会以返回error类型值来表明错误状态及其详细信息。

  error是一个预定义标识符,它代表了一个Go语言內建的接口类型。这个接口的类型声明如下:

type error interface{
	Error() string
}

  其中的Error方法声明的意义就在于为方法调用方提供当前错误状态的详细信息。任何数据类型只要实现了这个可以返回string类型值的Error方法就可以成为一个error接口类型的实现。不过在通常情况下,我们并不需要自己去编写一个error的实现类型。Go语言的标准库代码包errors为我们提供了一个用于创建errors类型值的函数New。该方法的声明如下:

func New(text string) error {
	return &errorString{text}
}

  

  errors.New函数接受一个string类型的参数值并可以返回一个error类型值。这个error类型值的动态类型就是errors.errorString类型。New函数的唯一参数被用于初始化那个errors.errorString类型的值。从代表这个实现类型的名称上可以看出,该类型是一个包级私有的类型。它只是errors包的内部实现的一部分,而非公开的API。errors.errorString类型及其方法的声明如下:

type errorString struct {
    s string
}
func (e *errorString) Error() string {
    return e.s
}

  

  传递给errors.New函数的参数值就是当我们调用它的Error方法的时候返回的那个结果值。

  我们可以使用代码包fmt中的打印函数打印出error类型值所代表的错误的详细信息,就像这样:

var err error = errors.New("A normal error.")

  这些打印函数在发现打印的内容是一个error类型值的时候都会调用该值的Error方法并将结果值作为该值的字符串表示形式。因此,我们传递给errors.New的参数值即是其返回的error类型值的字符串表示形式。

  另一个可以生成error类型值的方法是调用fmt包中的Errorf函数。调用它的代码类似于:

err2 := fmt.Errorf("%s\n", "A normal error.")

  与fmt.Printf函数相同,fmt.Errorf函数可以根据格式说明符和后续参数生成一个字符串类型值。但与fmt.Printf函数不同的是,fmt.Errorf函数并不会在标准输出上打印这个生成的字符串类型值,而是用它来初始化一个error类型值并作为该函数的结果值返回给调用方。在fmt.Errorf函数的内部,创建和初始化error类型值的操作正是通过调用errors.New函数来完成。

  在大多数情况下,errors.New函数和fmt.Errorf函数足以满足我们创建error类型值的要求。但是,接口类型error使得我们拥有了很大的扩展空间。我们可以根据需要定义自己的error类型。例如,我们可以使用额外的字段和方法让程序使用方能够获取更多的错误信息。例如,结构体类型os.PathError是一个error接口类型的实现类型。它的声明中包含了3个字段,这使得我们能够从它的Error方法的结果值当中获取到更多的信息。os.PathError类型及其方法的声明如下:

// PathError records an error and the operation and file path that caused it.
type PathError struct {
	Op   string
	Path string
	Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

  从os.PathError类型的声明上我们可以获知,它的3个字段都是公开的。因此,在任何位置上我们都可以直接通过选择符访问到它们。但是,在通常情况下,函数或方法中的相关结果声明的类型应该是error类型,而不应该是某一个error类型的实现类型。这也是为了遵循面向接口编程的原则。在这种情况下,我们常常需要先判定获取到的error类型值的动态类型,再依此来进行必要的类型转换和后续操作。例如:

file, err3 := os.Open("/etc/profile")
	if err3 != nil {
		if pe, ok := err3.(*os.PathError); ok {
			fmt.Printf("Path Error: %s (op=%s, path=%s)\n", pe.Err, pe.Op, pe.Path)
		} else {
			fmt.Printf("Unknown Error: %s\n", err3)
		}
	}

  

  我们通过类型断言表达式和if语句来对os.Open函数返回的error类型值进行处理。这与把error类型值作为结果值来表达函数执行的错误状态的做法一样,也属于Go语言中的异常处理的惯用法之一。

  如果os.Open函数在执行过程中没有发生任何错误,那么我们就可以对变量file所代表的文件的内容进行读取了。相关代码如下:

r := bufio.NewReader(file)
	var buf bytes.Buffer

	for {
		byteArray, _, err4 := r.ReadLine()
		if err4 != nil {
			if err4 == io.EOF {
				break
			} else {
				fmt.Printf("Read Error: %s\n", err4)
				break
			}
		} else {
			buf.Write(byteArray)
		}
	}

  io.EOF变量正是由errors.New函数的结果值来初始化的。EOF是文件结束符(End Of File)的缩写。对于文件读取操作来说,它意味着读取器已经读到了文件的末尾。因此,严格来说,EOF并不应该算作一个真正的错误,而仅仅属于一种“错误信号”。

  变量r代表了一个读取器。它的ReadLine方法返回3个结果值。第三个结果值的类型就是error类型的。当读取器读到file所代表的文件的末尾时,ReadLine方法会直接将变量io.EOF的值作为它的第三个结果值返回。如果判断的结果为true,那么我们就可以直接终止那个用于连续读取文件内容的for语句的执行。否则,我们就应该意识到在读取文件内容的过程中有真正的错误发生了,并采取相应的措施。

  注意,只有当两个error类型的变量的值确实为同一个值的时候,使用比较操作符==进行判断时才会得到true。从另一个角度看,我们可以预先声明一些error类型的变量,并把它们作为特殊的“错误信号”来使用。任何需要返回同一类“错误信号”的函数或方法都可以直接把这类预先声明的值拿来使用。这样我们就可以很便捷的使用==来识别这些“错误信号”并进行相应的操作了。

  不过,需要注意的是,这类变量的值必须是不可变的。也就是说,它们的实际类型的声明中不应该包含任何公开的字段,并且附属于这些类型的方法也不应该包含对其字段进行赋值的语句。例如,我们前面提到的os.PathError类型就不适合作为这类变量的值的动态类型,否则很可能会造成不可预知的后果。

  这种通过预先声明error类型的变量为程序使用方提供便利的做法在Go语言标准库代码包中非常常见。

  关于实现error接口类型的另一个技巧是,我们还可以通过把error接口类型嵌入到新的接口类型中对它进行扩展。例如,标准库代码包net中的Error接口类型,其声明如下:

// An Error represents a network error.
type Error interface {
	error
	Timeout() bool   // Is the error a timeout?
	Temporary() bool // Is the error temporary?
}

  一些在net包中声明的函数会返回动态类型为net.Error的error类型值。在使用方,对这种error类型值的动态类型的判定方法与前面提及的基本一致。

  如果变量err的动态类型是net.Error,那么就我们可以根据它的Temporary方法的结果值来判断当前的错误状态是否临时的:

if netErr, ok := err.(net.Error); ok && netErr.Temporary() {	}

  如果是临时的,那么就可以间隔一段时间之后再进行对之前的操作进行重试,否则就记录错误状态的信息并退出。假如我们没有对这个error类型值进行类型断言,也就无法获取到当前错误状态的那个额外属性,更无法决定是否应该进行重试操作了。这种对error类型的无缝扩展方式所带来的益处是显而易见的。

  在Go语言中,对错误的正确处理是非常重要的。语言本身的设计和标准库代码中展示的惯用法鼓励我们对发生的错误进行显式的检查。虽然这会使Go语言代码看起来稍显冗长,但是我们可以使用一些技巧来简化它们。这些技巧大都与通用的编程最佳实践大同小异,或者已经或将要包含在我们所讲的内容(自定义错误类型、单一职责函数等)中,所以这并不是问题。况且,这一点点代价比传统的try-catch方式带来的弊端要小得多。

时间: 2024-10-24 10:50:53

Go语言之异常处理的相关文章

Go语言中异常处理painc()和recover()的用法

Go语言中异常处理painc()和recover()的用法 1.Painc用法是:用于抛出错误.Recover()用法是:将Recover()写在defer中,并且在可能发生panic的地方之前,先调用此defer的东西(让系统方法域结束时,有代码要执行.)当程序遇到panic的时候(当然,也可以正常的调用出现的异常情况),系统将跳过后面的代码,进入defer,如果defer函数中recover(),则返回捕获到的panic的值. 2.代码: package main import "fmt&q

C语言的异常处理

异常:异常(Exception)是可预料的执行分支,bug是不可被预料的执行分支 异常:除以0,数组访问越界. bug: 使用野指针,申请内存没有释放. C语言异常处理方法: int setjump(jmp_buf env)  :将上下文保存在jmp_buf结构体 void longjump(jmp_buf env , int val)  :从jmp_buf结构体中恢复setjump()保存的上下文.最终从setjmp()函数调用点返回,返回值为val. 破坏了C语言的执行顺序 #include

一步一步学习C#语言【异常处理】

1.异常的概念 异常是在运行期间代码中产生的错误,或者由代码调用的函数产生的错误. 示例:    int[] myArray = {1, 2, 3, 4}; myArray[4] = 5; 运行上述代码会产生如下异常信息: Index was outside the bounds of the array. 产生的异常叫做System.IndexOutOfRangeException(数组下标越界异常). 2.异常处理 C#包含结构化异常处理语法. 可以使用try.catch和finally三个

Python语言之异常处理与测试

目录 (一)异常处理 (二)测试 (一)异常处理 1.捕获所有异常 try: x = 5 / 0 except: print('程序有错误') 2.捕获特定异常 try: x = 5 / 0 except ZeroDivisionError as e: print('不能为0',e) except: print('其他错误') else: print('没有错误') finally: print('关闭资源') 3.手动抛出异常 def method(): raise NotImplemente

8、C#语言里面的异常处理

在C#语言里面的异常处理,和Java语言的异常处理,几乎是如出一辙.都是由:try.catch.finally这几个关键词组成. 第一种异常处理是由try和catch组成.举例如下: //在进行除法运算的时候,除数不能为0,否则会发生异常. try { int 除数; System.Console.Write("请输入除数:"); 除数=Convert.ToInt32(System.Console.ReadLine()); int 结果; 结果=12/除数; System.Consol

[R]R语言里的异常处理与错误控制

之前一直只是在写小程序脚本工具,几乎不会对异常和错误进行控制和处理. 随着脚本结构和逻辑更复杂,脚本输出结果的准确性验证困难,同时已发布脚本的维护也变得困难.所以也开始考虑引入异常处理和测试工具的事情. 不过好像R语言的异常处理似乎有些辣鸡?查了下资料和try的文档说明,感觉说的并不清楚. 在网上查了一些资料,对R语言异常处理做了比较详细的说明,留档作为参考.至于测试工具的问题,后续还是再考虑下. 文章链接:R语言-处理异常值或报错的三个示例 原文参考了以下几个网页: http://stacko

C 语言异常处理(五十二)

我们今天来看下异常处理,在看 C++ 的异常处理之前,先来看看 C 语言中的异常处理.那么什么是异常呢?在程序运行过程中可能会产生异常,异常(Exception)与 Bug 的区别是:异常是程序运行时可预料的执行分支,而 Bug 是程序中的错误,是不被预期的运行方式. 下来我们来看看异常和 Bug 的对比:a> 异常比如运行时产生除 0 的情况,需要打开的外部文件不存在,数组访问时越界:b> Bug 是使用野指针,堆数组使用结束后未释放,选择排序无法处理长度为 0 的数组.在 C 语言中的经典

C# 异常和异常处理

C# 语言的异常处理功能可帮助您处理程序运行时出现的任何意外或异常情况. 异常处理使用 try.catch 和 finally 关键字尝试某些操作,以处理失败情况,尽管这些操作有可能失败,但如果您确定需要这样做,且希望在事后清理资源,就可以尝试这样做. 公共语言运行时 (CLR)..NET Framework 或任何第三方库或者应用程序代码都可以生成异常. 异常是使用 throw 关键字创建的.很多情况下,异常可能不是由代码直接调用的方法引发,而是由调用堆栈中位置更靠下的另一个方法所引发. 在这

[每日一题]说说异常处理机制和最佳实践

这个问题仁者见仁智者见智,每个人心中的最佳实践不见得一致,但是你要有想法,这个很关键,如果连思考都没有思考过,那就不太好了. 很多高级语言都提供了异常处理,比如Java.Python.Ruby,比较底层的语言,比如C,没有提供异常机制,最近时兴的Golang,也没有提供通常的try-catch异常机制. 异常机制是必须的么?显然不是,因为我们通常可以用多个返回值来解决,如果语言本身不支持多返回值,那异常机制就是必须的,否则这个语言写起来真的会很痛苦,你想想是不是这样?:) 异常,故名思议,就是不