defer延迟执行
defer延迟执行修饰符
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完 毕后,及时的释放资源,Go设计者提供了defer
(延时机制): 1
2
3
4
5
6func main() {
//当执行到defer语句时,暂不执行,会将defer后的语句压入到独立的栈中,当函数执行完毕后,再从该栈按照先入后出的方式出栈执行
defer fmt.Println("defer1...")
defer fmt.Println("defer2...")
fmt.Println("main...")
}1
2
3main...
defer2...
defer1...
defer
将语句放入到栈时,也会将相关的值拷贝同时入栈: 1
2
3
4
5
6func main() {
num := 0
defer fmt.Println("defer中:num=", num)
num = 3
fmt.Println("main中:num=",num)
}
输出结果: 1
2main中:num= 3
defer中:num= 0
defer最佳实践
defer最佳实践:用于关闭资源,比如:defer connect.close()
。
案例一:defer处理资源
没有使用defer时打开文件处理代码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
f,err := os.Open(file)
if err != nil {
return 0
}
info,err := f.Stat()
if err != nil {
f.Close()
return 0
}
f.Close()
return 0;
使用defer优化: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
f,err := os.Open(file)
if err != nil {
return 0
}
defer f.Close()
info,err := f.Stat()
if err != nil {
// f.Close() //这句已经不需要了
return 0
}
//后续一系列文件操作后执行关闭
// f.Close() //这句已经不需要了
return 0;
案例二:并发使用map的函数。
无defer代码: 1
2
3
4
5
6
7
8
9
10var (
mutex sync.Mutex
testMap = make(map[string]int)
)
func getMapValue(key string) int {
mutex.Lock() //对共享资源加锁
value := testMap[key]
mutex.Unlock()
return value
}
上述案例是很常见的对并发map执行加锁执行的安全操作,使用defer可以对上述语义进行简化: 1
2
3
4
5
6
7
8
9var (
mutex sync.Mutex
testMap = make(map[string]int)
)
func getMapValue(key string) int {
mutex.Lock() //对共享资源加锁
defer mutex.Unlock()
return testMap[key]
}
defer无法处理全局资源
使用defer语句, 可以方便地组合函数/闭包和资源对象,即使panic时,defer也能保证资源的正确释放。但是上述案例都是在局部使用和释放资源,如果资源的生命周期很长, 而且可能被多个模块共享和随意传递的话,defer语句就不好处理了,需要下面的方式。
Go的runtime
包的func SetFinalize(x, f interface{})函数可以提供类似C++析构函数的机制,比如我们可以包装一个文件对象,在没有人使用的时候能够自动关闭:
runtime.SetFinalizer1
2
3
4
5
6
7
8
9
10
11
12type MyFile struct {
f *os.File
}
func NewFile(name string) (&MyFile, error){
f, err := os.Open(name)
if err != nil {
return nil, err
}
runtime.SetFinalizer(f, f.f.Close)
return &MyFile{f: f}, nil
}时, 需要注意的地方是尽量要用指针访问内部资源,这样的话, 即使
*MyFile`对象忘记释放, 或者是被别的对象无意中覆盖, 也可以保证内部的文件资源可以正确释放。
二 错误Error
2.1 Go自带的错误接口
error是go语言声明的接口类型: 1
2
3type error interface {
Error() string
}
自定义错误
返回错误前,需要定义会产生哪些可能的错误,在Go中,使用errors包进行错误的定义,格式如下: 1
var err = errors.New("发生了错误")
下面这个例子演示了如何使用errors.New
: 1
2
3
4
5
6func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
在C语言里面是通过返回-1或者NULL之类的信息来表示错误,但是对于使用者来说,不查看相应的API说明文档,根本搞不清楚这个返回值究竟代表什么意思,比如:返回0是成功,还是失败,而Go定义了一个叫做error的类型,来显式表达错误。在使用时,通过把返回的error变量与nil的比较,来判定操作是否成功。例如os.Open
函数在打开文件失败时将返回一个不为nil的error变量
1 | func Open(name string) (file *File, err error) |
下面这个例子通过调用os.Open
打开一个文件,如果出现错误,那么就会调用log.Fatal
来输出错误信息: 1
2
3
4
5
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}os.Open
函数,标准包中所有可能出错的API都会返回一个error变量,以方便错误处理,这个小节将详细地介绍error类型的设计,和讨论开发Web应用中如何更好地处理error。
自定义错误案例
案例一:简单的错误字符串提示 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main
import (
"errors"
"fmt"
)
//定义除数为0的错误
var errByZero = errors.New("除数为0")
func div(num1, num2 int) (int, error) {
if num2 == 0 {
return 0, errByZero
}
return num1 / num2, nil
}
func main() {
fmt.Println(div(1, 0))
}
案例二:实现错误接口 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package main
import (
"fmt"
)
//声明一种解析错误
type ParseError struct {
Filename string
Line int
}
//实现error接口,返回错误描述
func (e *ParseError) Error() string {
return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
//创建一些解析错误
func newParseError(filename string, line int) error {
return &ParseError{filename, line}
}
func main() {
var e error
e = newParseError("main.go", 1)
fmt.Println(e.Error())
switch detail := e.(type) {
case *ParseError:
fmt.Printf("Filename: %s Line:%d \n", detail.Filename, detail.Line)
default:
fmt.Println("other error")
}
}
errors包分析
Go中的erros包对New的定义非常简单: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//创建错误对象
func New(text string) error {
return &errorString{text}
}
//错误字符串
type errorString struct {
s string
}
//返回发生何种错误
func (e *errorString) Error() string {
return e.s
}
panic 宕机
手动触发宕机
Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以及时发现错误。
Go语言程序在宕机时,会将堆栈和goroutine信息输出到控制台,所以宕机有额可以方便知晓发生错误的位置。如果在编译时加入的调试信息甚至连崩溃现场的变量值、运行状态都可以获取,那么如何触发宕机?
1 | package main |
运行结果是: 1
2
3
4
5
6panic: crash
goroutine 1 [running]:
main.main()
/Users/username/Desktop/TestGo/src/main.go:5 +0x39
exit status 2
使用panic
函数可以制造崩溃,panic声明如下; 1
func panic(v interface{})
注意:手动触发宕机并不是一种偷懒的方式,反而能迅速报错,终止程序继续运行,防止更大的错误产生,但是如果任何错误都使用宕机处理,也不是一个良好的设计。
defer与panic
当panic()发生宕机,panic()后面的代码将不会被执行,但是在panic前面已经运行过的defer语句依然会在宕机时发生作用: 1
2
3
4
5
6
7
8
9
10package main
import "fmt"
func main() {
defer fmt.Println("before")
panic("crash")
}
recover 宕机恢复
让程序在崩溃时继续执行
无论是代码运行错误由Runtime层抛出的panic崩溃,还是主动触发的panic崩溃,都可以配合defer和recover实现错误捕捉和处理,让代码在发生崩溃后允许继续执行。
在其他语言里,宕机往往以异常的形式存在,底层抛出异常,上层逻辑通过try/catch机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续执行。Go没有异常系统,使用panic触发宕机类似于其他语言的抛出异常,recover的宕机恢复机制就对应try/catch机制。
使用defer与recover处理错误
1 | func test(num1 int, num2 int){ |
panic recover综合示例
1 |
|
运行结果: 1
2
3
4
5
6运行前
手动宕机前
error: &{手动触发panic}
赋值宕机前
runtime error: runtime error: invalid memory address or nil pointer dereference
运行后
panic和recover关系
panic和defer的组合: - 有panic没有recover,程序宕机 - 有panic也有recover,程序不会宕机,执行完对应的defer后,从宕机点退出当前函数后继续执行
defer延迟执行