Go从入门到放弃之错误处理

简介: Go从入门到放弃之错误处理

阅读目录

回到顶部

一、error 类型及其使用

Go 语言错误处理机制

Go 语言错误处理机制非常简单明了,不需要学习了解复杂的概念、函数和类型,Go 语言为错误处理定义了一个标准模式,即 error 接口,该接口的定义非常简单:

1

2

3

type error interface {

    Error() string

}

其中只声明了一个 Error() 方法,用于返回字符串类型的错误消息。对于大多数函数或类方法,如果要返回错误,基本都可以定义成如下模式 —— 将错误类型作为第二个参数返回  

1

2

3

func Foo(param int) (n int, err error) {

    // ...

}

然后在调用返回错误信息的函数/方法时,按照如下「卫述语句」模板编写处理代码即可:

1

2

3

4

5

6

7

n, err := Foo(0)

 

if err != nil {

    // 错误处理

else {

    // 使用返回值 n

}

返回错误实例并打印

Go 标准错误包 errors 提供的 New() 方法快速创建一个 error 类型的错误实例

1

2

3

4

5

6

7

8

9

10

func add(a, b int) (c int, err error) {

    if (a < 0 || b < 0) {

        err = errors.New("只支持非负整数相加")

        return

    }

    a *= 2

    b *= 3

    c = a + b

    return

}

参照上面介绍的 Go 错误处理标准模式,调用这个函数并编写错误处理的代码如下

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

func main() {

    if len(os.Args) != 3 {

        fmt.Printf("Usage: %s num1 num2\n", filepath.Base(os.Args[0]))

        return

    }

    x, _ := strconv.Atoi(os.Args[1])

    y, _ := strconv.Atoi(os.Args[2])

    // 通过多返回值捕获函数调用过程中可能的错误信息

    z, err := add(x, y)

    // 通过「卫述语句」处理后续业务逻辑

    if err != nil {

        fmt.Println(err)

    else {

        fmt.Printf("add(%d, %d) = %d\n", x, y, z)

    }

}

为了方便测试,我们将通过命令行参数传递 add 函数的参数,这里我们引入了 os 包读取命令行参数,并通过 strconv 包提供的 Atoi 方法将其转化为整型(命令行读取参数值默认是字符串类型,转化时忽略错误以便简化处理流程),然后分别赋值为 xy 变量,再调用 add 函数进行运算。

注意到我们在打印错误信息时,直接传入了 err 对象实例,因为 Go 底层会自动调用 err 实例上的 Error() 方法返回错误信息并将其打印出来,就像普通类的 String() 方法一样。

我们简单测试下不传递参数、传递错误类型参数和传递正常参数这几种场景,打印结果如下:

 

以上这种错误处理已经能够满足我们日常编写 Go 代码时大部分错误处理的需求了,事实上,Go 底层很多包进行错误处理时就是这样做的。此外,我们还可以通过 fmt.Errorf() 格式化方法返回 error 类型错误,其底层调用的其实也是 errors.New 方法:

1

2

3

func Errorf(format string, a ...interface{}) error {

    return errors.New(Sprintf(format, a...))

}

更复杂的错误类型

系统内置错误类型

除了上面这种最基本的、使用 errors.New() 方法返回包含错误信息的错误实例之外,Go 语言内置的很多包还封装了更复杂的错误类型。

os 包为例,这个包主要负责与操作系统打交道,所以提供了 LinkErrorPathErrorSyscallError 这些实现了 error 接口的错误类型,以 PathError 为例,顾名思义,它主要用于表示路径相关的错误信息,比如文件不存在,其底层类型结构信息如下:

1

2

3

4

5

type PathError struct {

    Op   string

    Path string

    Err  error

}

该错误类型除了组合 error 接口实现 Error() 方法外,还提供了额外的操作类型字段 Op 和文件路径字段 Path 以丰富错误信息,方便定位问题,该类型的 Error() 方法实现如下:  

1

2

3

func (e *PathError) Error() string {

    return e.Op + " " + e.Path + ": " + e.Err.Error()

}

我们可以在调用 os 包方法出错时通过 switch 分支语句判定具体的错误类型,然后进行相应的处理:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

// 获取指定路径文件信息,对应类型是 FileInfo

// 如果文件不存在,则返回 PathError 类型错误

fi, err := os.Stat("test.txt")

if err != nil {

    switch err.(type) {

    case *os.PathError:

        // do something

    case *os.LinkError:

        // dome something

    case *os.SyscallError:

        // dome something

    case *exec.Error:

        // dome something

    }

else {

    // ...

}

自定义错误类型

我们也可以仿照 PathError 的实现自定义一些复杂的错误类型,只需要组合 error 接口并实现 Error() 方法即可,然后按照自己的需要为自定义类型添加一些属性字段,这很简单,就不展开介绍了。

小结

可以看到,Go 语言的错误和其他语言的错误和异常不同,它们就是从函数或者方法中返回的、和其他返回值并没有什么区别的普通 Go 对象而已,如果程序出错,要如何处理程序下一步的动作,是退出程序还是警告后继续执行,决定权完全在开发者手上。

回到顶部

二、defer 语句及其使用

Go 语言中的类没有构造函数和析构函数的概念,处理错误和异常时也没有提供 try...catch...finally 之类的语法,那当我们想要在某个资源使用完毕后将其释放(网络连接、文件句柄等),或者在代码运行过程中抛出错误时执行一段兜底逻辑,要怎么做呢?

通过 defer 关键字声明兜底执行或者释放资源的语句可以轻松解决这个问题。比如我们看 Go 内置的 io/ioutil 包提供的读取文件方法 ReadFile 实现源码,其中就有 defer 语句的使用:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

func ReadFile(filename string) ([]byte, error) {

    f, err := os.Open(filename)

    if err != nil {

        return nil, err

    }

    defer f.Close()

 

    var n int64 = bytes.MinRead

 

    if fi, err := f.Stat(); err == nil {

        if size := fi.Size() + bytes.MinRead; size > n {

            n = size

        }

    }

    return readAll(f, n)

}

defer 修饰的 f.Close() 方法会在函数执行完成后或读取文件过程中抛出错误时执行,以确保已经打开的文件资源被关闭,从而避免内存泄露。如果一条语句干不完清理的工作,也可以在 defer 后加一个匿名函数来执行对应的兜底逻辑:

1

2

3

defer func() {

    //  执行复杂的清理工作...

} ()

另外,一个函数/方法中可以存在多个 defer 语句,defer 语句的调用顺序遵循先进后出的原则,即最后一个 defer 语句将最先被执行,相当于「栈」这个数据结构,如果在循环语句中包含了 defer 语句,则对应的 defer 语句执行顺序依然符合先进后出的规则。

由于 defer 语句的执行时机和调用顺序,所以我们要尽量在函数/方法的前面定义它们,以免在后面编写代码时漏掉,尤其是运行时抛出错误会中断后面代码的执行,也就感知不到后面的 defer 语句。

下面我们看一段简单的 defer 示例代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

package main

 

import "fmt"

 

func printError()  {

    fmt.Println("兜底执行")

}

 

func main()  {

    defer printError()

    defer func() {

        fmt.Println("除数不能是0!")

    }()

 

    var i = 1

    var j = 1

    var k = i / j

 

    fmt.Printf("%d / %d = %d\n", i, j, k)

}

在这段代码中,我们定义了两个 defer 语句,并且是在函数最顶部,以确保异常情况下也能执行。

在函数正常执行的情况下,这两个 defer 语句会在最后一条打印语句执行完成后先执行第二条 defer 语句,再执行第一条 defer 语句

而如果我们把 j 的值设置为 0,则函数会抛出 panic:

 

表示除数不能为零。这个时候,由于 defer 语句定义在抛出 panic 代码的前面,所以依然会被执行,底层的逻辑是在执行 var k = i / j 这条语句时,遇到除数为 0,则抛出 panic,然后立即中断当前函数 main 的执行(后续其他语句都不再执行),并按照先进后出顺序依次执行已经在当前函数中声明过的 defer 语句,最后打印出 panic 日志及错误信息

总结一下,Go 语言的 defer 语句相当于 Java/PHP 中的析构函数和 finally 语句的功效,常用于定义兜底逻辑,在函数执行完毕后或者运行抛出 panic 时执行,如果一个函数定义了多个 defer 语句,则按照先进后出的顺序执行

回到顶部

三、panic 和 recover

Go 语言通过 error 类型统一进行错误处理,但这些错误都是我们在编写代码时就已经预见并返回的,对于某些运行时错误,比如数组越界、除数为0、空指针引用,这些 Go 语言是怎么处理的呢?

panic

除了像面演示的那样由 Go 语言底层抛出 panic,我们还可以在代码中显式抛出 panic,以便对错误和异常信息进行自定义,仍然以上篇教程除数为 0 的示例代码为例,我们可以这样显式返回 panic 中断代码执行:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

package main

 

import "fmt"

 

func main() {

    defer func() {

        fmt.Println("代码清理逻辑")

    }()

 

    var i = 1

    var j = 0

    if j == 0 {

        panic("除数不能为0!")

    }

    k := i / j

    fmt.Printf("%d / %d = %d\n", i, j, k)

}

这样一来,当我们执行这段代码时,就会抛出 panic:

 

panic 函数支持的参数类型是 interface{}

1

func panic(v interface{})

所以可以传入任意类型的参数:

1

2

panic(500)   // 传入数字

panic(errors.New("除数不能为0"))  // 传入 error 类型

无论是 Go 语言底层抛出 panic,还是我们在代码中显式抛出 panic,处理机制都是一样的:当遇到 panic 时,Go 语言会中断当前协程(即 main 函数)后续代码的执行,然后执行在中断代码之前定义的 defer 语句(按照先入后出的顺序),最后程序退出并输出 panic 错误信息,以及出现错误的堆栈跟踪信息,也就是下面红框中的内容

第一行表示出问题的协程,第二行是问题代码所在的包和函数,第三行是问题代码的具体位置,最后一行则是程序的退出状态,通过这些信息,可以帮助你快速定位问题并予以解决。

recover

此外,我们还可以通过 recover() 函数对 panic 进行捕获和处理,从而避免程序崩溃然后直接退出,而是继续可以执行后续代码,实现类似 Java、PHP 中 try...catch 语句的功能。

由于执行到抛出 panic 的问题代码时,会中断后续其他代码的执行,所以,显然这个 panic 的捕获应该放到 defer 语句中完成,才可以在抛出 panic 时通过 recover 函数将其捕获,defer 语句执行完毕后,会退出抛出 panic 的当前函数,回调调用它的地方继续后续代码的执行。

下面我们引入 recover() 函数来重构上述示例代码如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

package main

 

import (

    "fmt"

)

 

func divide() {

    defer func() {

        if err := recover(); err != nil {

            fmt.Printf("Runtime panic caught: %v\n", err)

        }

    }()

 

    var i = 1

    var j = 0

    k := i / j

    fmt.Printf("%d / %d = %d\n", i, j, k)

}

 

func main() {

    divide()

    fmt.Println("divide 方法调用完毕,回到 main 函数")

}

如果没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息:

 

而现在我们在 divide() 函数的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 函数而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行

 

如果在代码执行过程中没有抛出 panic,比如我们把 divide() 函数中的 j 值改为 1,则代码会正常执行到函数末尾,然后调用 defer 语句声明的匿名函数,此时 recover() 函数返回值为 nil,不会执行 if 分支代码,然后退出 divide() 函数回到 main() 函数执行后续代码:

 

这样一来,当程序运行过程中抛出 panic 时我们可以通过 recover() 函数对其进行捕获和处理,如果没有抛出则什么也不做,从而确保了代码的健壮性

相关文章
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
396 33
|
7月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟蒋星熠Jaxonic,Go语言探索者。深耕云计算、微服务与并发编程,以代码为笔,在二进制星河中书写极客诗篇。分享Go核心原理、性能优化与实战架构,助力开发者掌握云原生时代利器。#Go语言 #并发编程 #性能优化
630 43
Go语言深度解析:从入门到精通的完整指南
|
8月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟 蒋星熠Jaxonic,执着的星际旅人,用Go语言编写代码诗篇。🚀 Go语言以简洁、高效、并发为核心,助力云计算与微服务革新。📚 本文详解Go语法、并发模型、性能优化与实战案例,助你掌握现代编程精髓。🌌 从goroutine到channel,从内存优化到高并发架构,全面解析Go的强大力量。🔧 实战构建高性能Web服务,展现Go在云原生时代的无限可能。✨ 附技术对比、最佳实践与生态全景,带你踏上Go语言的星辰征途。#Go语言 #并发编程 #云原生 #性能优化
|
存储 算法 数据可视化
【二叉树遍历入门:从中序遍历到层序与右视图】【LeetCode 热题100】94:二叉树的中序遍历、102:二叉树的层序遍历、199:二叉树的右视图(详细解析)(Go语言版)
本文详细解析了二叉树的三种经典遍历方式:中序遍历(94题)、层序遍历(102题)和右视图(199题)。通过递归与迭代实现中序遍历,深入理解深度优先搜索(DFS);借助队列完成层序遍历和右视图,掌握广度优先搜索(BFS)。文章对比DFS与BFS的思维方式,总结不同遍历的应用场景,为后续构造树结构奠定基础。
606 10
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
426 3
Go 语言入门指南:切片
|
Go C语言
Go语言入门:分支结构
本文介绍了Go语言中的条件语句,包括`if...else`、`if...else if`和`switch`结构,并通过多个练习详细解释了它们的用法。`if...else`用于简单的条件判断;`if...else if`处理多条件分支;`switch`则适用于基于不同值的选择逻辑。特别地,文章还介绍了`fallthrough`关键字,用于优化重复代码。通过实例如判断年龄、奇偶数、公交乘车及成绩等级等,帮助读者更好地理解和应用这些结构。
260 15
|
存储 算法 Go
Go语言实战:错误处理和panic_recover之自定义错误类型
本文深入探讨了Go语言中的错误处理和panic/recover机制,涵盖错误处理的基本概念、自定义错误类型的定义、panic和recover的工作原理及应用场景。通过具体代码示例介绍了如何定义自定义错误类型、检查和处理错误值,并使用panic和recover处理运行时错误。文章还讨论了错误处理在实际开发中的应用,如网络编程、文件操作和并发编程,并推荐了一些学习资源。最后展望了未来Go语言在错误处理方面的优化方向。
218 5
|
存储 设计模式 安全
Go语言中的并发编程:从入门到精通###
本文深入探讨了Go语言中并发编程的核心概念与实践技巧,旨在帮助读者从理论到实战全面掌握Go的并发机制。不同于传统的技术文章摘要,本部分将通过一系列生动的案例和代码示例,直观展示Go语言如何优雅地处理并发任务,提升程序性能与响应速度。无论你是Go语言初学者还是有一定经验的开发者,都能在本文中找到实用的知识与灵感。 ###
|
Serverless Go
Go语言中的并发编程:从入门到精通
本文将深入探讨Go语言中并发编程的核心概念和实践,包括goroutine、channel以及sync包等。通过实例演示如何利用这些工具实现高效的并发处理,同时避免常见的陷阱和错误。
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。

热门文章

最新文章