Go语言基础之函数

函数是组织好的、可重复使用的、用于执行指定任务的代码块。本文介绍了 Go 语言中函数的相关内容。

Go 语言中支持函数、匿名函数和闭包,并且函数在 Go 语言中属于 “一等公民”。

1. 介绍

Go语言里面拥三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者 闭包(lambda) 函数
  • 方法:接收器中的函数

与其他语言的差异

  1. 可以有多个返回值

  2. 所有参数都是值传递:slice,map,channel会有传应用的错觉

  3. 函数可以作为变量的值

  4. 函数可以作为参数和返回值

原则

  • 函数与变量名首字母小写,本包可见(private),首字母大写,整个项目可见(public)
  • 同包内函数与变量名不能重复

2. 函数的定义

Go语言是编译型语言,所以函数定义的顺序是无关紧要的。

Go 语言中定义函数使用func关键字,具体格式如下:

func 函数名(形式参数列表)(返回值列表){
函数体
}

其中:

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。

  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。

  • 返回值:Go函数支持多返回值。 函数可以有返回值也可以没有。如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾。返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。

  • 函数体:实现指定功能的代码块。

下面的例子展示了一个简单的加减乘除运算的函数:

package main

import (
"errors"
"fmt"
)

func operation(a,b int,symbol string)(result int,err error){
switch symbol {
case "+":
return a+b,nil
case "-":
return a-b,nil
case "*":
return a*b,nil
case "/":
if b==0 {
return 0,errors.New("division by zero")
}
return a/b,nil
}
return 0,errors.New("unsupported symbol:"+symbol)
}

func main() {
r,_:=operation(2,3,"+")
fmt.Println(r)
}

常见使用语法

// 无参有匿名返回值
func fn1() int {
return 100
}

// 返回值可以命名也可以不命名
// 命名的返回值就相当于在函数中事先声明了一个变量
// func fn2(x int, y int) (ret int) {
// 参数类型简写:当参数中连续两个及以上参数的一致时,可将非最后一个参数的类型简写
func fn2(x, y int) (ret int) {
ret = x + y
// return ret
return // 使用命名返回值可以在return时省略
}

// 可变长参数:可传多个可不传,必须放在函数参数的最后(即可变参数要放在固定参数的后面)
func fn4(s string, y ...int) int {
fmt.Println(y) // y的类型的是切片[]int
return 100
}

// 多个返回值
func fn3() (int, string) {
return 1, "你好"
}

// Go语言中没有默认参数的概念,所见即所得

3. 函数的调用

定义了函数之后,我们可以通过函数名()的方式调用函数。 例如我们调用上面定义的两个函数,代码如下:

func main() {
sayHello()
ret := intSum(10, 20)
fmt.Println(ret)
}

注意,调用有返回值的函数时,可以不接收其返回值。

  1. 函数调用时,Go语言没有默认参数值。

  2. 不需要的返回值可使用匿名变量 _ 来占位:

    • 任何类型都可以赋值给它,但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。
    • 匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
    • 在 Lua 等编程语言里,匿名变量也被叫做哑元变量。

4. 参数

Go语言中函数的传递的参数都是拷贝(都是副本,不会影响原数据)

类型简写

函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:

func intSum(x, y int) int {
return x + y
}

上面的代码中,intSum函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型。

可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。

注意:可变参数通常要作为函数的最后一个参数。

固定类型

举个例子:

func intSum2(x ...int) int {
fmt.Println(x) //x是一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}

调用上面的函数:

ret1 := intSum2()
ret2 := intSum2(10)
ret3 := intSum2(10, 20)
ret4 := intSum2(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,示例代码如下:

func intSum3(x int, y ...int) int {
fmt.Println(x, y)
sum := x
for _, v := range y {
sum = sum + v
}
return sum
}

调用上述函数:

ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160

任意类型

如果你希望传任意类型,可以指定类型为interface{}

func MyPrintf(args ...interface{}) {
// 字节缓冲作为快速字符串连接
var b bytes.Buffer
// 遍历参数
for _, s := range slist {
// 将interface{}类型格式化为字符串
str := fmt.Sprintf("%v", s)
// 类型的字符串描述
var typeString string
// 对s进行类型断言
switch s.(type) {
case bool: // 当s为布尔类型时
typeString = "bool"
case string: // 当s为字符串类型时
typeString = "string"
case int: // 当s为整型类型时
typeString = "int"
}
// 写字符串前缀
b.WriteString("value: ")
// 写入值
b.WriteString(str)
// 写类型前缀
b.WriteString(" type: ")
// 写类型字符串
b.WriteString(typeString)
// 写入换行符
b.WriteString("\n")
}
return b.String()
}

func main() {
var v1 int = 1
var v2 string = "hello"
var v3 bool = false
MyPrintf(v1, v2, v3)
}

传递可变参数

可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。

// 实际打印的函数
func rawPrint(rawList ...interface{}) {
// 遍历可变参数切片
for _, a := range rawList {
// 打印参数
fmt.Println(a)
}
}
// 打印函数封装
func print(slist ...interface{}) {
// 将slist可变参数切片完整传递给下一个函数
rawPrint(slist...)
}
func main() {
print(1, 2, 3)
}

参数传递

实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。

package main
import "fmt"
// 用于测试值传递效果的结构体
type Data struct {
complax []int // 测试切片在参数传递中的效果
instance InnerData // 实例分配的innerData
ptr *InnerData // 将ptr声明为InnerData的指针类型
}
// 代表各种结构体字段
type InnerData struct {
a int
}
// 值传递测试函数
// 函数的参数和返回值都是 Data 类型。
// 在调用中,Data 的内存会被复制后传入函数,
// 当函数返回时,又会将返回值复制一次,
func passByValue(inFunc Data) Data {
// 输出参数的成员情况
fmt.Printf("inFunc value: %+v\n", inFunc)
// 打印inFunc的指针
fmt.Printf("inFunc ptr: %p\n", &inFunc)
return inFunc
}
func main() {
// 准备传入函数的结构
in := Data{
complax: []int{1, 2, 3},
instance: InnerData{
5,
},
ptr: &InnerData{1},
}
// 输入结构的成员情况
fmt.Printf("in value: %+v\n", in)
// 输入结构的指针地址
fmt.Printf("in ptr: %p\n", &in)
// 传入结构体,返回同类型的结构体
out := passByValue(in)
// 输出结构的成员情况
fmt.Printf("out value: %+v\n", out)
// 输出结构的指针地址
fmt.Printf("out ptr: %p\n", &out)
}

5. 返回值

Go语言中通过return关键字向外输出返回值。

Go语言的函数支持多返回值。我们在开发时候会习惯返回运算结果与一个error对象。

Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。

同一类型返回值

  1. 如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。

  2. 使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致。

  3. 纯类型的返回值对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义。

func typedTwoValues() (int, int) {
return 1, 2
}
func main() {
a, b := typedTwoValues()
fmt.Println(a, b) // 1,2
}

带有变量名的返回值

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。

  1. Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。
  2. 命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}

func f2(a string, b string) (res string, err error) {
return b, errors.New("is err")
}

// 1.可以在函数体中直接对函数返回值进行赋值
// 2.在函数结束前需要显式地使用 return 语句进行返回
func f2(a string, b string) (res string, err error) {
err = errors.New("is err")
return
}

返回值补充

当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。

func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}

6. 变量作用域

全局变量

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。

package main
import "fmt"
//定义全局变量num
var num int64 = 10
func testGlobalVar() {
fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func main() {
testGlobalVar() //num=10
}

局部变量

函数内定义的变量

局部变量又分为两种: 函数内定义的变量无法在该函数外使用,例如下面的示例代码main函数中无法使用testLocalVar函数中定义的变量x:

func testLocalVar() {
//定义一个函数局部变量x,仅在该函数内生效
var x int64 = 100
fmt.Printf("x=%d\n", x)
}
func main() {
testLocalVar()
fmt.Println(x) // 此时无法使用变量x
}

如果局部变量和全局变量重名,优先访问局部变量。

package main
import "fmt"
//定义全局变量num
var num int64 = 10
func testNum() {
num := 100
fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量
}
func main() {
testNum() // num=100
}

语句块定义的变量

接下来我们来看一下语句块定义的变量,通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。

func testLocalVar2(x, y int) {
fmt.Println(x, y) //函数的参数也是只在本函数中生效
if x > 0 {
z := 100 //变量z只在if语句块生效
fmt.Println(z)
}
//fmt.Println(z)//此处无法使用变量z
}

还有我们之前讲过的for循环语句中定义的变量,也是只在for语句块中生效:

func testLocalVar3() {
for i := 0; i < 10; i++ {
fmt.Println(i) //变量i只在当前for语句块中生效
}
//fmt.Println(i) //此处无法使用变量i
}

7. 函数类型与变量

在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中。

func fire() {
fmt.Println("fire")
}
func main() {
// demo1
var f func()
f = fire
f()

// demo2
var f = fire
f()
}

定义函数类型

我们可以使用type关键字来定义一个函数类型,具体格式如下:

type calculation func(int, int) int

上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。

简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。

func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}

add和sub都能赋值给calculation类型的变量。

var c calculation
c = add

函数类型变量

我们可以声明函数类型的变量并且为该变量赋值:

func main() {
var c calculation // 声明一个calculation类型的变量c
c = add // 把add赋值给c
fmt.Printf("type of c: %T\n", c) // type of c: main.calculation
fmt.Println(c(1, 2)) // 像调用add一样调用c
f := add // 将函数add赋值给变量f1
fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
fmt.Println(f(10, 20)) // 像调用add一样调用f
}

8. 高阶函数

高阶函数分为函数作为参数和函数作为返回值两部分。

函数作为参数

函数可以作为参数:

func add(x, y int) int {
return x + y
}
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}

函数作为返回值

函数也可以作为返回值:

func do(s string, , add func(x, y int) int, sub func(x, y int) int) (func(int, int) int, error) {
switch s {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")// 通过errors生成一个错误类型:*errors.errorString
return nil, err
}
}

9. 匿名函数和闭包

匿名函数

  1. Go语言支持匿名函数,即在需要使用函数时再定义函数。

  2. 匿名函数没有函数名只有函数体,可以作为一种类型被赋值给函数类型的变量,匿名函数也可以变量方式传递。

  3. 匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。

定义

函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

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

匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}

匿名函数多用于实现回调函数和闭包。

调用

在定义时调用匿名函数

func(data int) {
fmt.Println("hello", data)
}(100)

将匿名函数赋值给变量

// 将匿名函数体保存到f()中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用f()调用
f(100)

用作回调函数

func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
// 使用匿名函数打印切片内容
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}

闭包

介绍

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境

  1. Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说: 函数 + 引用环境 = 闭包
  2. 一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。

其它编程语言中的闭包

  1. 闭包(Closure)在某些编程语言中也被称为 Lambda 表达式。

  2. 闭包对环境中变量的引用过程也可以被称为“捕获”,在 C++11 标准中,捕获有两种类型,分别是引用和复制,可以改变引用的原值叫做“引用捕获”,捕获的过程值被复制到闭包中使用叫做“复制捕获”。

  3. 在 Lua 语言中,将被捕获的变量起了一个名字叫做 Upvalue,因为捕获过程总是对闭包上方定义过的自由变量进行引用。

  4. 闭包在各种语言中的实现也是不尽相同的,在 Lua 语言中,无论闭包还是函数都属于 Prototype 概念,被捕获的变量以 Upvalue 的形式引用到闭包中。

  5. C++ 与 C# 中为闭包创建了一个类,而被捕获的变量在编译时放到类中的成员中,闭包在访问被捕获的变量时,实际上访问的是闭包隐藏类的成员。

示例

func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder()
fmt.Println(f(10)) //10 一开始x是0
fmt.Println(f(20)) //30 x被上面赋值为10
fmt.Println(f(30)) //60
f1 := adder() // 函数重新赋值,x仍然有效
fmt.Println(f1(40)) //40
fmt.Println(f1(50)) //90
}

变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。 闭包进阶示例1:

func adder2(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder2(10)
fmt.Println(f(10)) //20
fmt.Println(f(20)) //40
fmt.Println(f(30)) //70
f1 := adder2(20)
fmt.Println(f1(40)) //60
fmt.Println(f1(50)) //110
}

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {// 结尾
return name + suffix
}
return name
}
}
func main() {
jpgFunc := makeSuffixFunc(".jpg")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc("test")) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}

闭包进阶示例3:

func calc(base int) (func(int) int, func(int) int) {
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 := calc(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}

闭包其实并不复杂,只要牢记闭包=函数+引用环境

闭包内部修改引用的变量

闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改:

// 准备一个字符串
str := "hello world"
// 创建一个匿名函数
foo := func() {
// 匿名函数中访问str
str = "hello dude"
}
// 调用匿名函数
foo()
fmt.Println(str) // hello dude

记忆效应

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。

package main
import (
"fmt"
)
// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
// 返回一个闭包
return func() int {
// 累加
value++
// 返回一个累加值
return value
}
}
func main() {
// 创建一个累加器, 初始值为1
accumulator := Accumulate(1)
// 累加1并打印
fmt.Println(accumulator())
fmt.Println(accumulator())
// 打印累加器的函数地址
fmt.Printf("%p\n", &accumulator)
// 创建一个累加器, 初始值为10
accumulator2 := Accumulate(10)
// 累加1并打印
fmt.Println(accumulator2())
// 打印累加器的函数地址
fmt.Printf("%p\n", &accumulator2)
}
  1. 对比输出的日志发现 accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例。
  2. 每调用一次 accumulator 都会自动对引用的变量进行累加。

工程模式

闭包的记忆效应被用于实现类似于设计模式中工厂模式的生成器,下面的例子展示了创建一个玩家生成器的过程。

package main
import (
"fmt"
)
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func() (string, int) {
// 将变量引用到闭包中
return name, hp
}
}
func main() {
// 创建一个玩家生成器
generator := playerGen("high noon")
// 返回玩家的名字和血量
name, hp := generator()
// 打印值
fmt.Println(name, hp) // high noon 150
}

闭包还具有一定的封装性,第 8 行的变量是 playerGen 的局部变量,playerGen 的外部无法直接访问及修改这个变量,这种特性也与面向对象中强调的封装性类似。

10. 递归函数

递归,就是在运行过程中调用自己。

介绍

  1. 所谓递归函数指的是在函数内部调用函数自身的函数。
  2. 从数学解题思路来说,递归就是把一个大问题拆分成多个小问题,再各个击破。实际开发过程中,递归函数可以解决许多数学问题,如计算给定数字阶乘、产生斐波系列等。

条件

构成递归需要具备以下条件:

  • 一个问题可以被拆分成多个子问题;

  • 拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的;

  • 不能无限制的调用本身,子问题需要有退出递归状态的终止条件。否则就会无限调用下去,直到内存溢出。

例子

1. 斐波那契数列

以递归函数的经典示例 —— 斐波那契数列为例,数列的形式如下所示:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, …

实现:

func main() {
result := 0
start := time.Now()
for i := 1; i <= 40; i++ {
result = fibonacci(i)
fmt.Printf("数列第 %d 位: %d\n", i, result)
}
end := time.Now()
delta := end.Sub(start)
fmt.Printf("程序的执行时间为: %s\n", delta)
}
func fibonacci(n int) (res int) {
if n <= 2 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}

输出:

数列第 1 位: 1
数列第 2 位: 1
数列第 3 位: 2
数列第 4 位: 3
...
数列第 39 位: 63245986
数列第 40 位: 102334155
程序的执行时间为: 2.2848865s

2. 数字阶乘

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且 0 的阶乘为 1,自然数 n 的阶乘写作n!,“基斯顿·卡曼”在 1808 年发明了n!这个运算符号。

例如,n!=1×2×3×…×n,阶乘亦可以递归方式定义:0!=1,n!=(n-1)!×n

func Factorial(n uint64) (result uint64) {
if n > 0 {
result = n * Factorial(n-1)
return result
}
return 1
}
func main() {
var i int = 10
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
// 10 的阶乘是 3628800
}

内存缓存

  1. 递归函数的缺点就是比较消耗内存,而且效率比较低,性能比较低。
  2. 当在进行大量计算的时候,提升性能最直接有效的一种方式是避免重复计算,通过在内存中缓存并重复利用缓存从而避免重复执行相同计算的方式称为内存缓存。
const LIM = 41
var fibs [LIM]uint64
func main() {
var result uint64 = 0
start := time.Now()
for i := 1; i < LIM; i++ {
result = fibonacci(i)
fmt.Printf("数列第 %d 位: %d\n", i, result)
}
end := time.Now()
delta := end.Sub(start)
fmt.Printf("程序的执行时间为: %s\n", delta)
}
func fibonacci(n int) (res uint64) {
// 记忆化:检查数组中是否已知斐波那契(n)
if fibs[n] != 0 {
res = fibs[n]
return
}
if n <= 2 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
fibs[n] = res
return
}

输出如下:

数列第 1 位: 1
数列第 2 位: 1
数列第 3 位: 2
数列第 4 位: 3
...
数列第 39 位: 63245986
数列第 40 位: 102334155
程序的执行时间为: 0.0149603s

通过运行结果可以看出,同样获取数列第 40 位的数字,使用内存缓存后所用的时间为 0.0149603 秒,对比之前未使用内存缓存时的执行效率,可见内存缓存的优势还是相当明显的。

11. defer 延迟执行

介绍

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

规则:未被defer的语句按序先执行;等待未defer执行完毕,被defer的语句逆序执行。

  1. Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数运行完返回语句时,将延迟处理的语句按后进先出执行。
  2. 关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。

例子:

func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println("中间")
defer fmt.Println(3)
fmt.Println("end")
}

输出结果:

start
中间
end
3
2
1

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

defer执行时机

在Go语言的函数中return语句在底层并不是原子操作(一步完成操作),它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:

释放资源

释放锁

var (
valueByKey = make(map[string]int)
valueByKeyGuard sync.Mutex
)

func readValue(key string) int {
valueByKeyGuard.Lock()
defer valueByKeyGuard.Unlock()

return valueByKey[key]
}

释放文件句柄

func fileSize(filename string) int64 {
f, err := os.Open(filename)
if err != nil {
return 0
}
// 延迟调用Close, 此时Close不会被调用
defer f.Close()

info, err := f.Stat()
if err != nil {
// defer机制触发, 调用Close关闭文件
return 0
}

size := info.Size()

// defer机制触发, 调用Close关闭文件
return size
}

参考感谢
函数 · 语雀 (yuque.com)
05.函数 · 语雀 (yuque.com)
函数 · 语雀 (yuque.com)
Go语言基础之函数 · 语雀 (yuque.com)
函数 · 语雀 (yuque.com)
func 函数 · 语雀 (yuque.com)