Go语言圣经 - 第5章 函数 - 5.4 错误

第5章 函数

函数可以让我们将一个语句序列打包成一个单元,然后可以从程序中其他地方多次调用,函数的机制可以让我们把一个大的工作分解成小任务。前面我们已经接触过函数,本章我们将讨论函数的更多特性

5.4 错误

在Go中有一部分函数总能成功的运行,比如strings.Contains 和 strconv.Forma函数,它们对各种可能的输入都做了良好的处理,几乎不会运行失败,除非灾难性的情况,如运行时内存溢出。这种错误比较复杂,恢复的可能性很低

还有一部分函数只要输入的参数满足一定的条件,也能保证运行成功。比如time.Date函数,该函数将年月日等参数构造成time.Time对象,除非最后一个参数(时区)是nil,这种情况会引发Panic异常。Panic来自被调函数的信号,表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常

对于大部分函数来说,永远无法确保能否成功运行。这是因为错误的原因超出了程序员的控制。比如任何进行I/O操作的函数都有可能面临出现错误的可能

在Go的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分,程序运行失败仅被认为是几个预期的结果之一

对于那些将运行失败看作是预期结果之一的函数发,它们会返回一个额外的值,通常是最后一个,用来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个bool值,通常被命名为ok,如cache.Lookup失败的唯一原因是key不存在,代码可以按照下面的方式组织:

func main() {
   value,ok := cache.Lookup(key)
   if !ok {
      //cache(key) does not exit...
   }
}

通常,导致失败的原因不止一种,尤其对于IO操作,用户需要了解更多错误信息,因此,额外的返回值不再是布尔类型,而是error类型

内置的error是接口类型,关于接口类型,我们将在第7章进一步探讨。现在只需要明白error类型是nil或者non-nil即可,nil意味着函数运行成功,non-nil表示失败, 对于non-nil的的error类型,我们可以通过调用error的Error函数或输出函数获得字符串类型的错误信息。

fmt.Println(err)
fmt.Printf("%v“,err)

通常,当函数返回non-nil的error时,其它返回值时未定义的,这些未定义的返回值应该被忽略,但是有少部分函数在发生错误时,仍然会返回一些有用的值,如,当读文件发生错误时,read函数返回可以读取的字节数以及错误信息.对于这种情况,正确的处理方式是先处理这些不完整的数据在处理错误信息,因此对函数的返回值要清晰的说明,以便他人使用

在Go中,函数运行失败是会返回错误信息,这些被认为是一种预期值而非异常。这使Go有别于那些将运行失败看作异常的语言,然而,虽然Go有着各种异常机制,但是这些机制仅被使用在处理那些未被预料到的错误,即bug,而不是那些健壮程序中应该被避免的程序错误。Go的异常机制我们将在5.9节中细致探讨

Go这样设计的原因是由于对某个应该在控制流程中处理的错误而言,将这个错误以异常的形式抛出会混乱对错误的描述,这通常会导致一些糟糕的后果,当某个程序错误被当作异常处理后,这个错误会将堆栈信息返回给终端用户,这些信息复杂而无用,无法帮助定位错误

正因如此,Go使用流程机制(如if和return)处理错误,这使编码人员能够更多的关注错误处理

5.4.1 错误处理策略

当调用函数时,我们应该选择合适的方式处理错误,常见的方式有五种:

传播错误

传播错误是最常见的错误处理方式之一,这意味着函数中某个子程序的失败,会变成函数的失败,把一个函数的错误信息通过另一个函数进行传递

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

我们以findLinks函数作为例子,如果findLinks函数对http.Get函数调用失败,直接打印出Http的错误返回给调用者。

当对html.Parse调用失败时,findLinks不会直接返回html.Parse的错误,因为缺少两条重要的信息:1、错误发生在解析器;2、url已经被解析,这些信息有助于错误的处理,findLinks会构造新的错误信息返回给调用者,既包含了这两项,也包括了底层解析出错的信息

doc, err:= html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil,fmt.Errorf("parising %s as HTML :%v",url,err)
	}

fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回

我们使用该函数添加额外的前缀上下文信息到原始错误信息,当错误信息最终由main函数处理时,错误信息应该清晰的提供因果链(从原因到结果),就像美国宇航做事故调查那样一样

genesis: crashed: no parachute: G-switch failed: bad relay orientation

由于错误信息经常是以链式组合在一起的,所以在错误信息中应该避免大写和换行符。最终的错误信息可能很长骂我们可以通过grep工具处理错误信息

编写错误信息时,我们要确保错误信息对问题的描述是详尽的,尤其要注意错误信息表达的一致性;即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的

以os包为例,OS包确保文件(如os.Open\Read\Close\Write)返回的每个错误的描述不仅包含错误的原因,(如无权限、文件目录不存在)也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息

一般而言,被调函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中返还给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中

重新尝试失败的操作

第二种处理错误的策略是重新尝试失败的操作。如果错误的发生是偶然的,或由不可知的问题导致的,一个明智的选择是重新尝试去失败的操作。但是,再重新尝试时,我们需要限制重新尝试的时间间隔和重新尝试的次数,以防止无限制的重试

func WaitForServer (url string) error {
   const timeout = 1 * time.Minute
   deadline := time.Now().Add(timeout)
   for tries := 0;time.Now().Before(deadline);tries++ {
      _,err := http.Head(url)
      if err == {
         return nil
      }
      log.Printf("server not responding (%s); retrying...",err)
      time.Sleep(time.Second<<uint(tries))
   }
   return fmt.Errorf("server %s failed to respond after %s",url,timeout)
}

如果错误发生后,程序无法继续运行,我们便可以采用第三种策略

输出错误信息并结束程序

但是需要注意的是,这种策略只应在main中执行,对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序

if err := WaitForServer(url);err != nil {
   fmt.Fprintf(os.Stderr,"Site is down: %v\n",err)
   os.Exit(1)
}

调用log.Fatalf可以更简洁的代码达到与上文相同的效果,log中所有函数都会默认在错误信息输出之前输出时间信息

if err := WaitForServer(url);err != nil {
   log.Fatalf("Site is down: %v\n",err)
}

长时间运行的服务器采用默认的时间格式,而交互式工具很少才采用包含如此多的信息的格式

2006/01/02 15:04:05 Site is down :no such domain:
bad.gopl.io

​ 我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名

log.SetPrefix("wait: ")
log.SetFlags(0)

只输出错误信息

第四种策略;有时,我们只需要输出错误信息就最够了,不需要终端程序的运行,我们可以通过log包2提供函数

if err := Ping();err != nil {
   fmt.Printf("ping failed: %v;networking disabled\n",err)
}

或者标准错误流输出错误信息

if err := Ping();err != nil {
   fmt.Fprintf(os.Stderr,"ping failed: %v;networking disabled\n",err)
}

log包中的所有函数会为没有换行符的字符串增加换行符

忽略错误信息

最后一种错误处理策略,我可以直接忽略错误信息

dir,err := iouttil.TempDir("",”scratch")
if err != nil {
   return fmt.Errorf("failed to create temp dir: %v",err)
}
os.RemoveAll(dir)

尽管os.RemoveAll会失败,但是上面的例子并没有做错误处理.这是因为操作系统会定期清理临时目录,

正因如此,程序虽然没有处理错误,但程序的逻辑不会因此受到影响。我们应该在每次函数调用后,都养成错误处理的习惯,当你决定忽略某个错误时,你应该清晰的记录下你的意图

在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的逻辑代码之前,如果某个错误会导致函数返回,那么成功时的逻辑代码不应该放在else语句块中,而应直接放在函数体中,Go大部分函数的代码结构几乎相同,首先是一些列的初始检查,防止错误发生,之后是函数的实际逻辑

5.4.2 文件结尾错误(EOF)

函数经常会返回多种错误,这种对终端用户来说可能会很有趣,但是对程序而言,这使得情况变的复杂。很多时候程序必须根据错误的类型,做出不同的反应。让我们考虑这样一个例子:从文件中读取n个字节,如果n等于文件长度,读取过程中的任何错误都表示失败。如果n小于文件长度,调用者会重复读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因,io包保证任由任何文件结束引起的读取失败都返回同一个错误 —io.EOF,该错误在io包中定义:

package io

import "errors"

var EOF = errors.New("EOF")

调用者只需通过简单的比较,就可以检测出这个错误,下列展示了如何从标准输入中读取字符,以及判断文件结束

in := bufio.NewReader(os.Stdin)
for {
   f,_,err := in.ReadRune()
   if err == io.EOF{
      break
}
if err != nil {
   return fmt.Errorf("read failed:%v",err)
}
}

因为文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息 — ”EOF".对于其它错误,我们可能需要在错误信息中描述错误的类型和数量,这使得我们不能像io.EOF一样采用固定的错误信息。在7.11节中,我们将会提出更系统的方法区分某些固定的错误值

上一篇:分布式ID生成器及redis,etcd分布式锁


下一篇:go-操作mysql数据库go-操作mysql数据库