一、函数定义
在Go中,函数是一段可以被重复使用的代码片段,用于执行特定的任务或完成特定的操作。函数可以接受参数,执行特定的操作,然后返回结果。
Go支持函数、匿名函数以及闭包,通过使用func
关键字来对函数进行定义,具体的格式如下:
go
func 函数名(参数列表) (返回值列表) {
函数体
}
上述结构中:
函数名
:由字母、数组、下划线组成。其中函数名首字母不能为数组,在同一个包中不能有相同的函数名。函数名与参数列表一起构成了函数签名。参数列表
:函数的参数列表,由参数变量及其数据类型组成,多个参数变量用逗号分隔开,并且有些函数也可以不包含参数列表。返回值列表
:由返回值变量及其数据类型组成,返回值列表如果有多个返回值则需要括号,多个返回值用逗号分隔。同样,函数也可以不需要返回值。函数体
:函数的代码逻辑块。
例如:
go
func sum(x int, y int) int {
return x + y
}
func hello() {
fmt.Println("Hello World")
}
在Go中,函数属于"一等公民"
,体现在:
- 函数自身可以作为值进行传递,包括函数赋值变量、函数作为参数传递、函数作为返回值;
- 支持匿名函数与闭包;
在后续的内容中会介绍到。
二、函数调用
在定义了函数之后,可以通过函数名()
的方式调用函数。
go
func main() {
hello()
result := sum(10, 20)
fmt.Println(result)
}
func sum(x int, y int) int {
return x + y
}
func hello() {
fmt.Println("Hello World")
}
三、函数特性
1、参数
参数传递
在定义函数时,参数列表中有参数,可以称参数列表中的变量为函数的形参,形参即定义在函数内的局部变量。在调用函数时,放入的变量为函数的实参,可以通过两种方式向函数传递参数。
- 值传递
在默认情况下,Go函数中使用的是值传递,即将传入的实参进行拷贝,在调用过程中不会影响到实际参数。
go
func modify(num int) {
num = 20
}
func main() {
num := 10
fmt.Println("modify before: ", num) // modify before: 10
modify(num)
fmt.Println("modify after: ", num) // modify after: 10
}
- 引用传递
引用传递指的是在调用函数时,将实际参数的地址传递到函数中,在函数中对参数进行修改,会影响到实际参数。
go
func modify(num *int) {
*num = 20
}
func main() {
num := 10
fmt.Println("modify before: ", num) // modify before: 10
modify(&num)
fmt.Println("modify after: ", num) // modify after: 20
}
上述代码中,modify
接收一个指针变量*int
,在main调用时,modify接收&num
,&num
为指向 num
的指针,是num
变量的地址, *num = 20
则是修改num
指针变量指向的值,因此在函数中对参数进行修改,会影响到实际参数。
在Go函数中,无论是值传递还是引用传递,实际上传递给函数的都是变量的拷贝副本,只不过对于值传递来说,拷贝的是变量值,而引用传递拷贝的是变量的地址。一般来说,地址拷贝更为高效,而值拷贝取决于拷贝的对象大小,对象越大则性能越低。在Go中map、slice、chan、指针、interface默认以引用的方式传递。
参数类型简写
在Go函数中,如果参数列表的相邻参数的数据类型相同,则可以省略类型,例如:
go
func sum(x, y int) int {
return x + y
}
上述代码中,sum函数有x, y两个参数,这两个参数的数据类型均为int,因此可以省略x的类型,在y的后面声明数据类型,则x也是该数据类型。
可变参数
函数中的可变参数是指函数的参数数量可能不确定,Go语言的函数可以通过在参数列表使 用...
标识来表示可变参数。
go
func main() {
result := sum(10, 20, 30)
fmt.Printf("Result:%d", result)
}
func sum(x ...int) int {
fmt.Println(x) // x为一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}
// 执行结果
[10 20 30]
Result:60
Go函数中,可变参数本质上是一个切片,并且只能有一个可变参数,需要注意的是可变参数需要作为函数参数的最后一个参数使用,在传递可变参数时,可以一个个元素传递,同时也可以直接传递一个数组或者切片。
go
func sum(x int, args ...int) int {
}
2、返回值
Go函数中,通过使用return
关键字返回函数的输出值。并且Go函数允许返回多个返回值。
go
func cal(x, y int) (sum, sub int) {
sum = x + y
sum = x -y
}
同时Go支持对返回值命名,默认值为类型的零值。
go
func fun() (names []string, m map[string]int, num int) {
m = make(map[string]int)
m["m"] = 10
return
}
func main() {
names, m, num := fun()
fmt.Println(names, m, num) // [] map[m:10] 0
}
3、变量作用域
全局变量
全局变量时定义在函数外部的变量,它在程序整个运行周期内都有效,在函数中可以访问到全局变量。
go
package main
import "fmt"
var num int = 10
func sum(x int) int {
return x + num
}
func main() {
result := sum(10)
fmt.Println(result) // 20
}
局部变量
局部变量主要分为两种,一种是在函数内定义的变量,在函数内可以使用,在函数外无法使用,若局部变量与全局变量同名,则优先访问局部变量。
go
package main
import "fmt"
// 定义全局变量num
var num int = 10
func sum(x int) int {
num := 20
return x + num // 函数中优先使用局部变量
}
func main() {
result := sum(10)
fmt.Println(result) // 30
}
另一种是例如在for语句
、if语句
内定义的变量,这种类型的变量的作用域只有在其for
、if
代码块内能够访问。
go
func sum(x... int) int {
sum := 0
for i := 0; i < len(x); i++ {
sum += x[i]
}
// fmt.Println(i) // 变量i只能在当前for语句中访问
return sum
}
func main() {
result := sum(10, 20, 30)
fmt.Println(result) // 30
}
4、函数类型
在Go中,可以使用type
关键字来定义函数类型,具体如下:
go
type calculator func(int, int) int
上述语句定义了一个sum
类型,他是一种函数类型,接收两个int参数并返回一个int类型的返回值。只要满足定义的函数类型的函数都属于sum类型的函数。例如:
go
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
此时可以声明并初始化函数类型的变量,并且像add()
调用函数一样,使用定义的函数变量进行调用cal()
:
go
package main
import "fmt"
type calculator func(int, int) int // 定义函数类型,类型为func(int, int) int
func add(x int, y int) int {
return x + y
}
func sub(x int, y int) int {
return x - y
}
func main() {
var cal calculator // 声明函数类型变量
cal = add // 初始化
fmt.Printf("type of cal: %T\n", cal)
fmt.Println(cal(20, 10))
cal = sub
fmt.Printf("type of cal: %T\n", cal)
fmt.Println(cal(20, 10))
}
// 执行结果
type of cal: main.calculator
30
type of cal: main.calculator
10
在Go中,函数做为一等公民,可以作为函数参数或者返回值。
- 函数作为参数:
go
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func sub(x int, y int) int {
return x - y
}
// 将func(int, int) int类型参数传入函数中
func calculator(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
result := calculator(10, 20, add)
fmt.Println(result) // 30
result = calculator(100, 20, sub)
fmt.Println(result) // 80
}
- 函数作为返回值:
go
package main
import (
"errors"
"fmt"
)
func add(x int, y int) int {
return x + y
}
func sub(x int, y int) int {
return x - y
}
func calculator(operation string) (func(int, int) int, error) {
switch operation {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("operation error")
return nil, err
}
}
func main() {
cal, _ := calculator("+")
result := cal(10, 20)
fmt.Println(result) // 30
cal, _ = calculator("-")
result = cal(200, 50)
fmt.Println(result) // 150
}
三、匿名函数
在Go中,可以在函数内部定义匿名函数,匿名函数即没有函数名的函数,具体定义格式如下:
go
func(参数) (返回值) {
函数体
}
在使用匿名函数时,需要将声明好的匿名函数赋值到某个变量中,或者立即执行匿名函数,例如:
go
func main() {
// 1、定义匿名函数并赋值给add变量
add := func(x int, y int) int {
return x + y
}
result := add(10, 20)
fmt.Println(result) // 30
// 2、定义匿名函数并立刻调用
result = func(x int, y int) int {
return x - y
}(100, 50)
fmt.Println(result) // 50
}
四、闭包
在Go中,闭包指的是函数与其相关的引用环境组成的实体。具体来说,就是一个拥有许多变量和绑定使用了这些变量的环境表达式(函数),即闭包 = 函数 + 引用环境,例如:
go
package main
import "fmt"
func add() func(int) int {
var x int
return func(y int) int {
fmt.Printf("x = %d\n", x)
x += y
return x
}
}
func main() {
adder1 := add()
fmt.Println(adder1(10)) // x = 0 print: 10
fmt.Println(adder1(20)) // x = 10 print: 30
fmt.Println(adder1(30)) // x = 30 print: 60
adder2 := add()
fmt.Println(adder2(40)) // x = 0 print: 40
fmt.Println(adder2(50)) // x = 40 print: 90
fmt.Println(adder2(60)) // x = 90 print: 150
}
从上述代码可知,在add()
函数中,return
返回值返回了一个匿名函数,且该匿名函数使用了外部变量 x。在main
函数中,变量adder1
与adder2
是一个函数并且它引用了其外部作用域中的 x 变量,此时则adder1
与adder2
为一个闭包。并且在一个闭包的生命周期内,外部变量 x 的值一直有效。
五、defer
1、defer介绍
在Go中,defer
关键字放在指定的代码语句前,会将其跟随的语句进行延迟处理。在defer
语句所在的函数将要退出时,将延迟处理的语句按照defer
声明的逆序以此进行执行,即先声明的defer
语句最后执行,最后声明的defer
语句最先被执行。
go
func main() {
fmt.Println("defer start")
defer fmt.Println("one")
defer fmt.Println("two")
defer fmt.Println("three")
}
// 执行结果
defer start
three
two
one
2、defer执行时机
在Go的函数中,return
语句在底层并不是原子操作,而是分为两个步骤,一是将返回值进行赋值,二是执行RET指令。defer
语句的执行时间则是在这两者之间,在返回值赋值操作后,RET指令执行前。

3、defer特性
由于defer语句的特性,常用于处理资源释放问题。例如资源清理、文件清理、解锁以及记录时间等等。
defer注册要延迟执行的函数时,需要该函数确定好所有的参数才能够进行注册。例如:
go
package main
import "fmt"
func main() {
x := 1
y := 2
defer calculator("AA", x, calculator("A", x, y))
x = 10
defer calculator("BB", x, calculator("B", x, y))
y = 20
}
func calculator(tag string, x, y int) int {
result := x + y
fmt.Println(tag, x, y, result)
return result
}
// 执行结果
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4
观察上述代码的执行结果可知,在第8行defer语句想要注册calculator("AA", x, calculator("A", x, y))
时,由于calculator("A", x, y)
值未确定从而导致defer无法注册,因此先执行calculator("A", x, y)
获取结果后,才能将calculator("AA", x, calculator("A", x, y))
进行注册,defer calculator("BB", x, calculator("B", x, y))
同理,因此执行结果为上述结果。
六、异常处理panic与recover
在Go中,可以抛出一个panic
异常,之后在defer
中通过recover
捕获这个异常并进行正常处理。
panic具体来说:
- 当函数内使用了
panic
语句后,会终止后续需要执行的代码,之后panic
所在的函数如果有需要执行的defer
执行列表,则按照defer
注册的顺序逆序执行。 - 如果是调用函数的调用者了含有
panic
的函数,则调用者之后的代码也不会执行,假如调用者所在的函数也存在需要执行的defer
执行列表,则按照defer
注册的顺序逆序执行。 - 直到整个
goroutine
整个退出,并报告错误。
而recover
的作用在于捕获panic
。当一个函数在执行过程中出现了异常或者遇到panic()
,正常语句会立即终止,然后执行defer
语句,再报告异常信息,最后退出goroutine
。如果在defer
中使用了recover()
,则会捕获错误信息,并让该错误信息终止报告。
例如:
go
package main
func testPanic() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()
panic("panic error!")
}
func main() {
testPanic()
}
// 执行结果
panic error!
上述代码中,利用recover
处理panic
,需要注意的是,defer
应该放在panic
定义之前,且recover
只有在 defer
调用的函数中才有效。否则当panic
时,recover
无法捕获到panic
。
在defer
延迟调用中引发的panic
错误,可被后续的defer
延迟调用使用recover
捕获,但只能捕获到最后一个panic
错误,之前的错误无法捕获到。
go
package main
func testPanic() {
defer func() {
if err := recover(); err != nil {
println(err.(string))
}
}()
defer func() {
panic("defer panic")
}()
panic("testPanic panic") // 将 interface{} 转型为具体类型。
}
func main() {
testPanic()
}
// 执行结果
defer panic
如果需要确保后续的代码可以执行,可以使用匿名函数将代码块重构,保护代码段。
go
package main
import "fmt"
func testPanic(x int, y int) {
var z int
func() { // 匿名函数
defer func() {
if recover() != nil {
z = 0
}
}()
panic("testPanic panic")
z = x / y
return
}()
fmt.Printf("x / y = %d\n", z) // 后续的代码可执行
}
func main() {
testPanic(20, 10)
}
// 执行结果
x / y = 0