Go基础-函数

2022/12/14 go

在编程中,函数是指一段可以直接被另一段程序或代码引用的、可重复使用的、用来实现单一或相关联功能的代码段。目的是为了提高应用的模块性和代码的重复利用率。

相比较其他语言,Go 语言 在设计上对函数进行了优化和改进,使其使用起来更加便利。

Go 语言中函数有着以下特性:

  • 函数本身可以作为值进行传递;
  • 支持普通函数、匿名函数和闭包(closure);
  • 函数可以满足接口;

# 函数定义

在 Go 语言中,定义一个函数需要声明参数函数名返回值等。

# 定义一个普通函数

定义函数需要以 func 标识开头,后面紧接着函数名、参数列表、返回参数列表以及函数体

func 函数名(参数列表) (返回参数列表) {
  函数体
}
1
2
3
  • 函数名:函数名由字母、数字、下划线组成;函数名不能以数字开头;同一包内,函数名不能重名。
  • 参数列表:参数列表中声明的每一个参数由变量名和参数类型组成,可省略不写,不写代表不需要传参
  • 返回参数列表:可以是返回一个值类型,也可以是一个组合,即返回多个参数。另外,当函数中声明了有返回值时,函数体中必须 使用 return 语句提供返回值。一个返回值的时候()可省略,可省略不写,不写代表不需要返回值

一个简单的加法函数:

func sum(a int, b int)int {
    return a+b
}
1
2
3

# 参数列表简写

当参数列表中定义了多个参数,且参数类型相同时,代码如下:

func sum(a int, b int) int {
}
1
2

上面代码中,变量 a、b 的类型均为整型 int,可以采用以下简写方式:

func sum(a , b int) int {
}
1
2

统一定义一个 int 类型即可。

# 函数返回值

返回值支持返回多个,常用场景下,多返回值的最后一个参数会返回函数执行中可能发生的错误,示例代码如下:

conn, err := connectToDatabase()
1

上面这段代码中,函数 connectToDatabase() 用来获取数据库连接,conn 表示数据库连接,err 用来接收获取过程中可能发生的错误。

# 同一类型的返回值

如果函数返回值是统一类型,则用括号将多个返回值括起来,以逗号隔开,示例代码如下:

package main

import "fmt"

func say(name, content string) (string, string) {
	return name, content
}

func main()  {
	name, content := say("hello", "Go")

	fmt.Println(name, content)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

注意:使用 return 语句返回多个值时,值的顺序需要与函数声明的返回值一致。

代码输出:

hello Go
1

# 带有变量名的返回值

Go 语言支持对返回值进行命名,命名后代码的可读性更佳。

命名的返回值默认值为该类型的默认值,例如,若返回值为整型 int,则默认值为 0;若为字符串string,则默认值为空字符串;布尔为 false; 指针为 nil 等。

下面是代码示例:

func initValue() (a int, b int) {//声明返回变量名
	a = 1
	b = 2
	return //不填写返回值
}
1
2
3
4
5

上面这段代码中,函数声明中将返回值变量命名为 ab,然后在函数体中分别对 ab 进行赋值, 然后使用 return 语句进行返回。

注意: 当函数使用命名进行返回时,可以在 return 语句中不填返回值列表,当然填写也是可以的,上面的代码与下面的代码执行效果相同:

func initValue() (a int, b int) {//声明返回变量名
	a = 1
	return a, 2 //填写返回值
}
1
2
3
4

接收的时候变量名不一定要求要跟返回变量名一样

# 调用函数

函数的调用格式如下:

//带返回值的
返回值列表 = 函数名(参数列表) 
//不带返回值
函数名(参数列表) 
1
2
3
4
  • 函数名:需要被调用的函数名;
  • 参数列表: 传入的参数列表,以逗号隔开;
  • 返回值列表:多个返回值以逗号隔开;
package main

import "fmt"

func initValue() (a int, b int) {//声明返回变量名
	a = 1
	return a, 2 //填写返回值
}

func main() {
	// 调用 initValue 函数
	a, b := initValue()
	fmt.Println(a, b)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

代码输出:

1 2
1

函数内定义的局部变量只能作用在函数体中,函数执行结束后,这些变量都会被释放掉,无法再次访问。

# 函数变量(将函数保存到变量中)

在 Go 语言 中,函数也是一种类型,可以和其他类型(如intfloatstring 等)一样被保存到变量中,类型为func(参数列表) (返回参数列表)

以下为几种情况的示例代码:

package main

import "fmt"

//无参数无返回值
func say1() {
	fmt.Println("hello Go")
}

//有参数,无返回值
func say2(a string) {
	fmt.Println(a)
}

//无参数,有返回值
func say3() string {
	return "hello GO"
}

//有参数有返回值
func say4(a string) string {
	return a
}

func main() {
	//无参数无返回值
	var f func()
	f = say1
	f()
	//有参数,无返回值
	var f2 func(string2 string)
	f2 = say2
	f2("haha")
	//无参数,有返回值
	var f3 func() string
	f3 = say3
	s := f3()
	fmt.Println(s)
	//有参数有返回值
	var f4 func(string) string
	f4 = say4
	s2 := f4("bbbb")
	fmt.Println(s2)
}

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
38
39
40
41
42
43
44
45

代码输出:

hello Go
haha
hello GO
bbbb
1
2
3
4

函数变量也可以使用短变量声明和初始化进行简化写法,可使代码更为简洁美观:

package main

import "fmt"

//无参数无返回值
func say1() {
	fmt.Println("hello Go")
}

//有参数,无返回值
func say2(a string) {
	fmt.Println(a)
}

//无参数,有返回值
func say3() string {
	return "hello GO"
}

//有参数有返回值
func say4(a string) string {
	return a
}

func main() {
	//无参数无返回值
	f := say1
	f()
	//有参数,无返回值
	f2 := say2
	f2("haha")
	//无参数,有返回值
	f3 := say3
	s := f3()
	fmt.Println(s)
	//有参数有返回值
	f4 := say4
	s2 := f4("bbbb")
	fmt.Println(s2)
}

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
38
39
40
41

代码输出:

hello Go
haha
hello GO
bbbb
1
2
3
4

# 匿名函数_回调函数

匿名函数 是没有名字的函数,只有函数体匿名函数经常以变量 的形式被传递。

大部分场景下,匿名函数经常被使用于实现函数回调、闭包等。

# 定义一个匿名函数

要定义一个匿名函数,其实格式跟普通函数差不多,只是没有函数名

func(参数列表) (返回参数列表) {
  函数体
}
1
2
3

# 使用方式

# 定义后立即调用匿名函数

定义完匿名函数后,在函数后面跟上传入参数,就可以立即调用了

package main

import "fmt"

func main() {
	func(name string) {
		fmt.Printf("hello, %s", name)
	}("Go")
}
1
2
3
4
5
6
7
8
9

代码输出:

hello Go
1

# 将匿名函数赋值给变量

匿名函数可以赋值给变量,格式跟函数变量一样:

package main

import "fmt"

func main() {
	f := func(name string) {
		fmt.Printf("hello, %s", name)
	}
	f("Go")
}

1
2
3
4
5
6
7
8
9
10
11

代码输出:

hello Go
1

# 匿名函数用作回调函数

回调函数可以当作函数的一个参数传参,在函数本身做完对应的逻辑后,扩展后续的操作。比如两个数相加后,你可以将其输出或者做其他操作等,提升代码的扩展性

两数相加并打印的例子:

package main

import "fmt"

func sumPrint(a int, b int, f func(int)) {
	sum := a + b
	f(sum)
}

func main() {
	//两数相加,回调函数将其输出
	sumPrint(1, 1, func(i int) {
		fmt.Println(i)
	})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

代码输出:

2
1

# 函数实现接口(将函数当做接口来调用)

其他基本类型能够实现接口,函数 同样可以实现接口。

# 如何定义一个接口

定义格式如下:

type 接口名字 interface {
	方法名(interface{})
}
1
2
3

下面是一个动物的接口:

// 定义一个名为 Animal 的接口
type Animal interface {
    // 需要实现一个说话 say() 方法
	say(interface{})
}
1
2
3
4
5

这个接口需要实现 say() 方法,调用时需传入一个 interface{} 类型的变量,这种类型表示你可以传入任意类型的值。

# 实现方法

# 结构体实现接口

package main

import "fmt"

type Animal interface {
	say(interface{})
}

// 定义一个结构体类型:狗
type Dog struct {
}

// 实现接口 animal 定义的 say 方法,方法中打印一句话
func (s *Dog) say(p interface{}) {
	fmt.Println("狗说: ", p)
}

func main() {
	// 定义接口
	var animal Animal
	// 实例化狗结构体
	dog := new(Dog)
	// 将实例化的结构体赋给接口
	animal = dog
	// 使用接口调用实例化结构体的方法 Say()
	animal.say("汪汪")
}

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

代码输出:

狗说:  汪汪
1

# 函数实现接口

函数要想实现接口,需要先将自己定义为类型,然后实现接口方法,同时需要再方法中调用函数本体。函数实现接口无需实例化,只需要将函数转换为 函数类型即可。

示例代码如下:

package main

import "fmt"

type Animal interface {
	say(interface{})
}

// 将函数定义为类型
type Cat func(interface{})

// 实现接口 Dog 的 Say 方法
func (f Cat) say(p interface{}) {
	// 调用 f() 函数本体
	f(p)
}

func main() {
	// 定义接口
	var animal Animal
	// 将匿名函数转换为 Cat 类型,此时 Cat 类型实现了 say 方法,赋值给接口是成功的
	animal = Cat(func(p interface{}) {
		fmt.Println("猫说: ", p)
	})
	// 使用接口调用 say 方法
	animal.say("喵喵")
}

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

代码输出:

猫说:  喵喵
1

# 闭包函数

“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

维基百科讲,闭包(Closure),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

简单来说就是引用了外部变量 的匿名函数

# 定义闭包

闭包是由函数及其相关引用环境组合而成的实体。

函数 + 引用外部变量 = 闭包
1

下面代码演示了如何在 Go 语言中定义闭包:

package main

import "fmt"

func main() {
	// 定义一个字符串
	str := "hello World"

	// 创建一个匿名函数
	function := func() {
		// 给字符串 str 赋予一个新的值,注意: 匿名函数引用了外部变量,这种情况形成了闭包
		str = "hello Go"
		// 打印
		fmt.Println(str)
	}

	// 执行闭包
	function()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

代码输出:

hello Go
1

闭包复制的是原对象指针,这就很容易解释延迟引用现象。

package main

import "fmt"

func test() func() {
    x := 100
    fmt.Printf("x (%p) = %d\n", &x, x)

    return func() {
        fmt.Printf("x (%p) = %d\n", &x, x)
    }
}

func main() {
    f := test()
    f()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

代码输出:

x (0xc0000180a8) = 100
x (0xc0000180a8) = 100
1
2

在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。

FuncVal { func_address, closure_var_pointer ... }
1

# 闭包的记忆效应

闭包在引用外部变量后具有记忆效应,闭包中可以修改变量,变量会随着闭包的生命周期一直存在,此时,闭包如同变量一样拥有了记忆效应。

示例代码如下:

package main

import "fmt"

// 定义一个累加函数,返回类型为 func() int, 入参为整数类型,每次调用函数对该值进行累加
func add(value int) func() int {
	// 返回一个闭包
	return func() int {
		// 累加
		value++
		// 返回累加值
		return value
	}
}

func main() {
	// 创建一个累加器,初始值为 1
	accumulator := add(1)

	// 累加1并打印
	fmt.Println(accumulator())
	// 再来一次
	fmt.Println(accumulator())

	// 创建另一个累加器,初始值为 10
	accumulator2 := add(10)

	// 累加1并打印
	fmt.Println(accumulator2())
}

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

代码输出:

2
3
11
1
2
3

通过输出可以看出闭包的记忆效应,每次调用 accumulator() 后,都会对 value 进行累加操作。

# 返回2个闭包

package main

import "fmt"

// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {
    // 定义2个函数,并返回
    // 相加
    add := func(i int) int {
        base += i
        return base
    }
    // 相减
    sub := func(i int) int {
        base -= i
        return base
    }
    // 返回
    return add, sub
}

func main() {
    f1, f2 := test01(10)
    // base一直是没有消
    fmt.Println(f1(1), f2(2))
    // 此时base是9
    fmt.Println(f1(3), f2(4))
}
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

代码输出:

11 9
12 8
1
2

# 通过闭包实现一个生成器

可以通过闭包的记忆效应来实现设计模式中工厂模式的生成器。

下面的代码示例展示了创建游戏玩家生成器的过程:

package main

import "fmt"

// 定义一个玩家生成器,它的返回类型为 func() (string, int),输入名称,返回新的玩家数据
func genPlayer(name string) func() (string, int) {
	// 定义玩家血量
	hp := 1000
	// 返回闭包
	return func() (string, int) {
		// 引用了外部的 hp 变量, 形成了闭包
		return name, hp
	}
}

func main() {
	// 创建一个玩家生成器
	generator := genPlayer("新手1号")
	// 返回新创建玩家的姓名, 血量
	name, hp := generator()
	// 打印
	fmt.Println(name, hp)

}

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

代码输出:

新手11000
1

从上面代码看出,闭包具有面向对象语言的特性 —— 封装性,变量 hp 无法从外部直接访问和修改。

# 递归函数

递归,就是在运行的过程中调用自己,一个函数调用自己,就叫做递归函数

构成递归需具备的条件:

  1. 子问题须与原始问题为同样的事,且更为简单。
  2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。

# 数字阶乘

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。1808年,基斯顿·卡曼引进这个表示法。

package main

import "fmt"

func factorial(i int) int {
    if i <= 1 {
        return 1
    }
    return i * factorial(i-1)
}

func main() {
    var i int = 7
    fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

代码输出:

Factorial of 7 is 5040
1

# 斐波那契数列(Fibonacci)

这个数列从第3项开始,每一项都等于前两项之和。

package main

import "fmt"

func fibonaci(i int) int {
    if i == 0 {
        return 0
    }
    if i == 1 {
        return 1
    }
    return fibonaci(i-1) + fibonaci(i-2)
}

func main() {
    var i int
    for i = 0; i < 10; i++ {
        fmt.Printf("%d\n", fibonaci(i))
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码输出:

0
1
1
2
3
5
8
13
21
34
1
2
3
4
5
6
7
8
9
10

# 延迟调用(defer)

Go语言的延迟调用是使用关键字defer来注册的,使得调用直到return前才被执行,多个defer语句按先进后出的方式执行,defer语句中的变量,在defer声明时就决定了。

defer常用于关闭文件句柄锁资源释放数据库连接释放

例子:

package main

import "fmt"

func main() {
    var whatever [5]struct{}

    for i := range whatever {
        defer fmt.Println(i)
    }
} 
1
2
3
4
5
6
7
8
9
10
11

代码输出:

4
3
2
1
0
1
2
3
4
5

从上面看出来,原本输出应该是0,1,2,3,4,但由于是先进后出,所以最后的小标4先输出了

defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。因为是先进后出后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。

# defer 碰上闭包

package main

import "fmt"

func main() {
    var whatever [5]struct{}
    for i := range whatever {
        defer func() { fmt.Println(i) }()
    }
} 
1
2
3
4
5
6
7
8
9
10

代码输出:

4
4
4
4
4
1
2
3
4
5

其实Go说的很清楚,官方对 defer的解释:

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.

每次defer语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来;当外层函数(非代码块,如一个for循环)退出时,defer函数按照定义的逆序执行;如果defer执行的函数为nil, 那么会在最终调用函数的产生panic.

也就是说函数正常执行,由于闭包用到的变量i 在执行的时候已经变成4,所以输出全都是4

Go语言编程举了一个可能一不小心会犯错的例子:

package main

import "fmt"

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(t.name, " closed")
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer t.Close()
    }
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

代码输出:

c  closed
c  closed
c  closed
1
2
3

这个输出并不会像预计的输出c b a,而是输出c c c

可是按照前面的go spec中的说明,应该输出c b a才对啊.

那换一种方式来调用一下.

package main

import "fmt"

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(t.name, " closed")
}
func Close(t Test) {
    t.Close()
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        defer Close(t)
    }
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码输出:

c  closed
b  closed
a  closed
1
2
3

这个时候输出的就是c b a

当然,如果不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a

看似多此一举的声明

package main

import "fmt"

type Test struct {
    name string
}

func (t *Test) Close() {
    fmt.Println(t.name, " closed")
}
func main() {
    ts := []Test{{"a"}, {"b"}, {"c"}}
    for _, t := range ts {
        t2 := t
        defer t2.Close()
    }
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

代码输出:

c  closed
b  closed
a  closed
1
2
3

通过以上例子,结合

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.

这句话。可以得出下面的结论:

defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的this指针如何处理,通过这个例子可以看出Go语言并没有把这个明确写出来的this指针当作参数来看待。

多个 defer 注册,按先进后出次序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

package main

func test(x int) {
	defer println("a")
	defer println("b")

	defer func() {
		println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
	}()

	defer println("c")
}

func main() {
	test(0)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

代码输出:

c
b
a
panic: runtime error: integer divide by zero
1
2
3
4

延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。

package main

func test() {
	x, y := 10, 20

	defer func(i int) {
		println("defer:", i, y) // y 闭包引用
	}(x) // x 被复制,此时是10

	x += 10
	y += 100
	println("x =", x, "y =", y)
}

func main() {
	test()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

代码输出:

x = 20 y = 120
defer: 10 120
1
2

滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。

package main

import (
    "fmt"
    "sync"
    "time"
)

var lock sync.Mutex

func test() {
    lock.Lock()
    lock.Unlock()
}

func testdefer() {
    lock.Lock()
    defer lock.Unlock()
}

func main() {
    func() {
        t1 := time.Now()

        for i := 0; i < 10000; i++ {
            test()
        }
        elapsed := time.Since(t1)
        fmt.Println("test elapsed: ", elapsed)
    }()
    func() {
        t1 := time.Now()

        for i := 0; i < 10000; i++ {
            testdefer()
        }
        elapsed := time.Since(t1)
        fmt.Println("testdefer elapsed: ", elapsed)
    }()

}
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
38
39
40
41

代码输出:

test elapsed:  82µs
testdefer elapsed:  503.7µs
1
2

# defer陷阱

# defer 与闭包( closure)

package main

import (
    "errors"
    "fmt"
)

func foo(a, b int) (i int, err error) {
    defer fmt.Printf("first defer err %v\n", err)
    defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
    defer func() { fmt.Printf("third defer err %v\n", err) }()
    if b == 0 {
        err = errors.New("divided by zero!")
        return
    }

    i = a / b
    return
}

func main() {
    foo(2, 0)
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

代码输出:

third defer err divided by zero!
second defer err <nil>
first defer err <nil>
1
2
3

解释:如果 defer 后面跟的不是一个 闭包( closure)最后执行的时候我们得到的并不是最新的值。

# defer 与 return

package main

import "fmt"

func foo() (i int) {

    i = 0
    defer func() {
        fmt.Println(i)
    }()

    return 2
}

func main() {
    foo()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

代码输出:

2
1

解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i的值重新赋值为 2。所以输出结果为 2 而不是 0。

# efer nil 函数

package main

import (
    "fmt"
)

func test() {
    var run func() = nil
    defer run()
    fmt.Println("runs")
}

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    test()
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码输出:

runs
runtime error: invalid memory address or nil pointer dereference
1
2

解释:名为 test的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。

# 在错误的位置使用 defer

http.Get 失败时会抛出异常。

package main

import "net/http"

func do() error {
    res, err := http.Get("http://www.google.com")
    defer res.Body.Close()
    if err != nil {
        return err
    }

    // ..code...

    return nil
}

func main() {
    do()
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

代码输出:

panic: runtime error: invalid memory address or nil pointer dereference
1

因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了Body 中的空变量 res ,因此会抛出异常

# 解决方案

总是在一次成功的资源分配下面使用 defer,对于这种情况来说意味着:当且仅当 http.Get成功执行时才使用 defer

package main

import "net/http"

func do() error {
    res, err := http.Get("http://xxxxxxxxxx")
    if res != nil {
        defer res.Body.Close()
    }

    if err != nil {
        return err
    }

    // ..code...

    return nil
}

func main() {
    do()
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body

解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。

# 不检查错误

在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉

package main

import "os"

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer f.Close()
    }

    // ..code...

    return nil
}

func main() {
    do()
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

改进一下

package main

import "os"

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                // log etc
            }
        }()
    }

    // ..code...

    return nil
}

func main() {
    do()
} 
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

再改进一下

通过命名的返回变量来返回 defer内的错误。

package main

import "os"

func do() (err error) {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer func() {
            if ferr := f.Close(); ferr != nil {
                err = ferr
            }
        }()
    }

    // ..code...

    return nil
}

func main() {
    do()
} 
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

释放相同的资源

如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。

package main

import (
    "fmt"
    "os"
)

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }()
    }

    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }()
    }

    return nil
}

func main() {
    do()
} 
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
38
39
40

代码输出:

defer close book.txt err close ./another-book.txt: file already closed
1

当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭

解决方案:

package main

import (
    "fmt"
    "io"
    "os"
)

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }(f)
    }

    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }(f)
    }

    return nil
}

func main() {
    do()
} 
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
38
39
40
41