ZetCode

Go 错误

最后修改时间 2024 年 4 月 11 日

在本文中,我们将展示如何在 Golang 中处理错误。

错误

错误是指程序中发生的异常、意外情况。在编程中,错误通常被称为 bug。查找和解决 bug 的过程称为调试。Go 提供了处理错误的工具。Go 中的错误是普通的数值。因此,错误可以存储在变量中,作为参数传递给函数,或者从函数返回。

错误使用内置的 error 类型表示。

大多数 Go 函数会在其返回值中返回一个错误值。(Go 支持多个返回值。)检查此错误值是我们的责任。nil 值表示没有错误。按照惯例,错误值是返回值中最右边的值。

content, err := ioutil.ReadFile("thermopylae.txt")

if err != nil {
    log.Fatal(err)
}

处理错误的惯用方法是在函数调用后立即检查错误。

panic 是我们无法优雅处理的运行时错误。例如,当我们尝试除以零或访问不存在的数组索引时,可能会发生 panic。

堆栈跟踪 是程序执行过程中某个时间点上活动堆栈帧的报告。panic 时,Go 会将堆栈跟踪打印到控制台,从而帮助我们进行调试。

Go ioutil.ReadFile

ioutil.ReadFile 读取文件并返回其内容。成功调用会将 err 设置为 nil

thermopylae.txt
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.

这是要读取的文本文件。

read_file.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
)

func main() {

    content, err := ioutil.ReadFile("thermopylae.txt")

     if err != nil {
          log.Fatal(err)
     }

    fmt.Println(string(content))
}

读取文件时可能会发生许多事情。例如,我们可能没有足够的权限读取文件,提供了错误的文件路径,或者磁盘可能已满。

if err != nil {
    log.Fatal(err)
}

log.Fatal 函数将错误打印到控制台,并通过调用 os.Exit 来终止程序。

$ go run read_file.go
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.

如果没有错误,我们将获得文件的内容。

$ go run read_file.go
2021/01/25 11:40:26 open thermopyla.txt: no such file or directory
exit status 1

错误的文件名会导致错误。

Go panic 示例

Panic 是一种运行时错误。Panic 通常很难或不可能恢复。

zero_div.go
package main

import "fmt"

func main() {

    var x int = 10
    var y int = 0

    fmt.Println(x / y)
}

零除会导致运行时错误。

$ go run zero_div.go
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.main()
        /root/Documents/prog/golang/errors/zero_div.go:10 +0x11
exit status 2

这种错误必须由程序员修复。他必须始终检查分母,以确保这种情况不会发生。

Go errors.New

可以使用 errors.New 函数创建新错误。它返回一个格式化为给定文本的错误。即使文本相同,每次调用也会返回一个不同的错误值。

new_error.go
package main

import (
    "errors"
    "fmt"
)

func main() {

    err := errors.New("Some error")
    if err != nil {
        fmt.Println(err)
    }
}

该示例使用 errors.New 创建了一个新错误。

zero_div2.go
package main

import (
    "errors"
    "fmt"
    "log"
)

func divide(x int, y int) (int, error) {
    if y == 0 {
        return 0, errors.New("not possible to divide by zero")
    } else {
        return (x / y), nil
    }
}

func main() {

    if res, err := divide(6, 3); err != nil {
        log.Fatal(err)
    } else {
        fmt.Println("The answer is", res)
    }

    if res, err := divide(6, 0); err != nil {
        log.Fatal(err)
    } else {
        fmt.Println("The answer is", res)
    }
}

在此示例中,我们确保分母不为零。

func divide(x int, y int) (int, error) {
    if y == 0 {
        return 0, errors.New("not possible to divide by zero")
    } else {
        return (x / y), nil
    }
}

如果分母为零,我们将使用 errors.New 生成一个新错误。

$ go run zero_div2.go
The answer is 2
2021/01/25 11:58:57 not possible to divide by zero
exit status 1

这次没有 panic。我们自己处理了错误。

Go errors.Is

errors.Is 函数检查错误是否为指定类型。

isfun.go
package main

import (
    "errors"
    "fmt"
    "log"
    "os"
)

func main() {
    if _, err := os.Open("data.txt"); err != nil {
        if errors.Is(err, os.ErrNotExist) {
            log.Fatal("file does not exist\t", err)
        } else if errors.Is(err, os.ErrPermission) {
            log.Fatal("insufficient permissions\t", err)

        } else {
            log.Fatal(err)
        }
    }

    fmt.Println("...")
}

在代码示例中,我们检查错误是否为 os.ErrNotExistos.ErrPermission 类型。

Go fmt.Errorf

使用 fmt.Errorf,我们可以创建一个带有格式化错误消息的新错误。

circle_area.go
package main

import (
    "fmt"
    "math"
    "os"
)

func area(radius float64) (float64, error) {

    if radius < 0 {
        return 0, fmt.Errorf("radius %0.2f is less than zero", radius)
    }

    return math.Pi * radius * radius, nil
}

func main() {

    radius := -7.0
    area, err := area(radius)

    if err != nil {

        fmt.Println(err)
        os.Exit(1)
    }

    fmt.Printf("Area of circle %0.2f", area)
}

在代码示例中,我们计算圆的面积。其中也包含一些错误处理,因为我们无法计算负半径的圆面积。

func area(radius float64) (float64, error) {

area 函数返回两个值:计算出的面积和错误。

if radius < 0 {
    return 0, fmt.Errorf("radius %0.2f is less than zero", radius)
}

对于负的 radius 值,将使用 fmt.Errorf 创建一个新错误。

return math.Pi * radius * radius, nil

对于正确的 radius 值,我们将返回计算出的面积,并将错误设置为 nil

area, err := area(radius)

我们调用 area 函数并获取返回值。

if err != nil {

    fmt.Println(err)
    os.Exit(1)
}

我们检查错误值;如果它不是 nil,我们将打印错误消息并退出程序。

$ go run circle_area.go
radius -7.00 is less than zero
exit status 1

Go 自定义错误类型

要定义自定义错误,我们需要实现 error 接口。

custom_error.go
package main

import (
    "fmt"
    "log"
)

func enterAge(age int) (string, error) {

    if age < 0 || age > 130 {

        return "", &wrongAge{age, "wrong age value"}
    }

    return fmt.Sprintf("processing %d age value", age), nil
}

type wrongAge struct {
    age int
    msg string
}

func (e *wrongAge) Error() string {

    return fmt.Sprintf("%d: %s", e.age, e.msg)
}

func main() {

    var age int = 18
    msg, err := enterAge(age)

    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(msg)

    age = 178
    msg, err = enterAge(age)

    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(msg)
}

在代码示例中,我们实现了一个自定义错误。

func enterAge(age int) (string, error) {

    if age < 0 || age > 130 {

        return "", &wrongAge{age, "wrong age value"}
    }

    return fmt.Sprintf("processing %d age value", age), nil
}

enterAge 函数接受一个年龄值;对于超出预期年龄范围的值,将生成一个错误。

type wrongAge struct {
    age int
    msg string
}

定义了 wrongAge 类型。

func (e *wrongAge) Error() string {

    return fmt.Sprintf("%d: %s", e.age, e.msg)
}

实现了 error 接口。

$ go run custom_error.go 
processing 18 age value
2021/01/25 12:26:29 178: wrong age value
exit status 1

来源

The Go Programming Language Specification

在本文中,我们研究了 Golang 中的错误处理。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。我自 2007 年以来一直撰写编程文章。迄今为止,我已撰写了 1,400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。

列出所有 Go 教程