跟我一起来学golang之《错误》

384 阅读6分钟

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

什么是错误

错误指出程序中的异常情况。假设我们正在尝试打开一个文件,文件系统中不存在这个文件。这是一个异常情况,它表示为一个错误。

Go中的错误也是一种类型。错误用内置的error 类型表示。就像其他类型的,如int,float64,。错误值可以存储在变量中,从函数中返回,等等。

如何使用错误

让我们从一个示例程序开始,这个程序尝试打开一个不存在的文件。

示例代码:

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err := os.Open("/test.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
  //根据f进行文件的读或写
    fmt.Println(f.Name(), "opened successfully")
}

在os包中有打开文件的功能函数:

func Open(name string) (file *File, err error)

如果文件已经成功打开,那么Open函数将返回文件处理。如果在打开文件时出现错误,将返回一个非nil错误。

如果一个函数或方法返回一个错误,那么按照惯例,它必须是函数返回的最后一个值。因此,Open 函数返回的值是最后一个值。

处理错误的惯用方法是将返回的错误与nil进行比较。nil值表示没有发生错误,而非nil值表示出现错误。在我们的例子中,我们检查错误是否为nil。如果它不是nil,我们只需打印错误并从主函数返回。

运行结果:

open /test.txt: No such file or directory

我们得到一个错误,说明该文件不存在。

示例代码:

package main

import (
    "fmt"
    "errors"
)

func checkAge(age int) error {
    if age < 0 {
        //return errors.New("年龄不能为负数")
        // fmt包的Errorf函数的用武之地。这个函数根据一个格式说明器格式化错误,并返回一个字符串作为值来满足错误。
        return fmt.Errorf("age是:%d,年龄不能为负数", age)

    }
    fmt.Println("年龄无误,", age)
    return nil

}

func test()(int, int, error)  {
    return 1, 2, errors.New("啦啦啦")
}

func main() {
    /*
    在go中并不是像Java语言那样,有直接的异常处理机制。而是使用error.
    go中的error也是一种类型,内置接口类型的错误是表示错误条件的常规接口,nil值表示没有错误。

     */
    var e1 error // <nil>
    fmt.Println(e1)
    fmt.Printf("%T\n", e1) // <nil>

    //创建一个错误对象
    e2 := errors.New("错误了")
    fmt.Println(e2)        // 错误了
    fmt.Printf("%T\n", e2) // *errors.errorString

    err:=checkAge(-30)
    if err == nil{
        fmt.Println("ok。。。")
    }else{
        fmt.Println("失败,,", err)
    }

    a, b, err:= test()
    fmt.Println(a, b, err)

    p1:= new(int)
    fmt.Printf("%T,\n",p1)
    fmt.Println(*p1)

    p2:=new(string)
    fmt.Printf("%T\n",p2)
    fmt.Println(*p2)

}

运行结果:

<nil>
<nil>
错误了
*errors.errorString
失败,, age是:-30,年龄不能为负数
1 2 啦啦啦
*int,
0
*string

错误类型表示

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

让我们再深入一点,看看如何定义错误类型的构建。错误是一个带有以下定义的接口类型,

type error interface {
    Error() string
}

它包含一个带有Error()字符串的方法。任何实现这个接口的类型都可以作为一个错误使用。这个方法提供了对错误的描述。

当打印错误时,fmt.Println函数在内部调用Error() 方法来获取错误的描述。这就是错误描述是如何在一行中打印出来的。

不要忽略错误

永远不要忽略一个错误。重新编写一个示例,该示例列出了与模式匹配的所有文件的名称,而忽略了错误处理代码。

package main

import (  
    "fmt"
    "path/filepath"
)

func main() {  
    files, _ := filepath.Glob("[")
    fmt.Println("matched files", files)
}

我们从前面的例子中已经知道模式是无效的。我忽略了Glob函数返回的错误,方法是使用行号中的空白标识符。

matched files []

由于我们忽略了这个错误,输出看起来好像没有文件匹配这个模式,但是实际上这个模式本身是畸形的。所以不要忽略错误。

自定义错误

创建自定义错误的最简单方法是使用错误包的新功能。

在使用新函数创建自定义错误之前,让我们了解它是如何实现的。下面提供了错误包中的新功能的实现。

// Package errors implements functions to manipulate errors.
  package errors

  // New returns an error that formats as the given text.
  func New(text string) error {
      return &errorString{text}
  }

  // errorString is a trivial implementation of error.
  type errorString struct {
      s string
  }

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

既然我们知道了新函数是如何工作的,那么就让我们在自己的程序中使用它来创建一个自定义错误。

我们将创建一个简单的程序,计算一个圆的面积,如果半径为负,将返回一个错误。

我们通过fmt包的Errorf函数。这个函数根据一个格式返回一个字符串作为值来满足错误。

使用Errorf函数:

package main

import (  
    "fmt"
    "math"
)

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
    }
    return math.Pi * radius * radius, nil
}

func main() {  
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

运行结果:

Area calculation failed, radius -20.00 is less than zero

Panic和Recover

Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,
将异常与控制结构混在一起会很容易使得代码变得混乱。因为开发者很容易滥用异常,甚至一个小小的错误都抛出一个异常。

在Go语言中,使用多值返回来返回错误。不要用异常代替错误,更不要用来控制流程。
在极个别的情况下,也就是说,遇到真正的异常的情况下(比如除数为0了)。
才使用Go中引入的Exception处理:defer, panic, recover

Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。

Panic是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数(defer)会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。异常错误可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。 Recover是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

下面这个函数演示了如何在过程中使用panic

var user = os.Getenv("USER")
  func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

下面这个函数检查作为其参数的函数在执行时是否会产生panic:

func throwsPanic(f func()) (b bool) {
  defer func() {
    if x := recover(); x != nil {
       b = true
    }
  }()
    f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
    return
}

示例代码:

package main

import "fmt"

func main()  {
    /*
    panic:词意"恐慌"
        内建函数,可以中断原有的控制流程。进入一个恐慌中。
    recover:词意"恢复"
        通过恢复正常的执行并检索传递给panic的调用的错误值来停止恐慌序列

    注意:panic可以在任何地方引发,但是recover只能在defer函数中有效

     */
     testA()
     testB()
     testC()

}

func testA()  {
    fmt.Println("\t函数A。。。")
}

func testB()  {
    defer func() { // 必须是要先声明defer,否则不能捕获到panic异常
        if r:= recover(); r != nil{ // 这里的err其实就是panic传入的内容
            fmt.Println(r,"revocer....")
        }
    }()
    for i:=0;i<10;i++{
        fmt.Println("函数B。。。", i)
        if i == 5{
            panic("panic....")

        }

    }

}

func testC()  {
    fmt.Println("\t函数C...")
}

运行结果:

    函数A。。。
函数B。。。 0
函数B。。。 1
函数B。。。 2
函数B。。。 3
函数B。。。 4
函数B。。。 5
panic.... revocer....
    函数C...