函数
函数
在Go中,函数是一等公民,函数是Go最基础的组成部分,也是Go的核心。
声明
函数的声明格式如下
func 函数名([参数列表]) [返回值] {
函数体
}
声明函数有两种办法,一种是通过func
关键字直接声明,另一种就是通过var
关键字来声明,如下所示
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}
函数签名由函数名称,参数列表,返回值组成,下面是一个完整的例子,函数名称为Sum
,有两个int
类型的参数a
,b
,返回值类型为int
。
func Sum(a int, b int) int {
return a + b
}
还有一个非常重要的点,即Go中的函数不支持重载,像下面的代码就无法通过编译
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}
Go的理念便是:如果签名不一样那就是两个完全不同的函数,那么就不应该取一样的名字,函数重载会让代码变得混淆和难以理解。这种理念是否正确见仁见智,至少在Go中你可以仅通过函数名就知道它是干什么的,而不需要去找它到底是哪一个重载。
参数
Go中的参数名可以不带名称,一般这种是在接口或函数类型声明时才会用到,不过为了可读性一般还是建议尽量给参数加上名称
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}
对于类型相同的参数而言,可以只需要声明一次类型,不过条件是它们必须相邻
func Log(format string, a1, a2 any) {
...
}
变长参数可以接收0个或多个值,必须声明在参数列表的末尾,最典型的例子就是fmt.Printf
函数。
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
值得一提的是,Go中的函数参数是传值传递,即在传递参数时会拷贝实参的值。如果你觉得在传递切片或map时会复制大量的内存,我可以告诉你大可不必担心,因为这两个数据结构本质上都是指针。
返回值
下面是一个简单的函数返回值的例子,Sum
函数返回一个int
类型的值。
func Sum(a, b int) int {
return a + b
}
当函数没有返回值时,不需要void
,不带返回值即可。
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}
Go允许函数有多个返回值,此时就需要用括号将返回值围起来。
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0不能作为被除数")
}
return a / b, nil
}
Go也支持具名返回值,不能与参数名重复,使用具名返回值时,return
关键字可以不需要指定返回哪些值。
func Sum(a, b int) (ans int) {
ans = a + b
return
}
和参数一样,当有多个同类型的具名返回值时,可以省略掉重复的类型声明
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}
不管具名返回值如何声明,永远都是以return
关键字后的值为最高优先级。
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c,d将不会被返回
return a + b, a * b
}
匿名函数
匿名函数就是没有签名的函数,例如下面的函数func(a, b int) int
,它没有名称,所以我们只能在它的函数体后紧跟括号来进行调用。
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}
在调用一个函数时,当它的参数是一个函数类型时,这时名称不再重要,就可以直接传递一个匿名函数,如下所示
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}
这是一个自定义排序规则的例子,slices.SortFunc
接受两个参数,一个是切片,另一个就是比较函数,不考虑复用的话,我们就可以直接传递匿名函数。
闭包
闭包(Closure)这一概念,在一些语言中又被称为Lamda表达式,与匿名函数一起使用,闭包 = 函数 + 环境引用吗,看下面一个例子:
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}
输出
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512
Exp
函数的返回值是一个函数,这里将称成为grow
函数,每将它调用一次,变量e
就会以指数级增长一次。grow
函数引用了Exp
函数的两个变量:e
和n
,它们诞生在Exp
函数的作用域内,在正常情况下随着Exp
函数的调用结束,这些变量的内存会随着出栈而被回收。但是由于grow
函数引用了它们,所以它们无法被回收,而是逃逸到了堆上,即使Exp
函数的生命周期已经结束了,但变量e
和n
的生命周期并没有结束,在grow
函数内还能直接修改这两个变量,grow
函数就是一个闭包函数。
利用闭包,可以非常简单的实现一个求费波那契数列的函数,代码如下
func main() {
// 10个斐波那契数
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}
输出为
0
1
1
2
3
5
8
13
21
34
延迟调用
defer
关键字可以使得一个函数延迟一段时间调用,在函数返回之前这些defer描述的函数最后都会被逐个执行,看下面一个例子
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}
输出
2
1
因为defer是在函数返回前执行的,你也可以在defer中修改函数的返回值
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}
当有多个defer描述的函数时,就会像栈一样先进后出的顺序执行。
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}
0
2
5
4
3
1
延迟调用通常用于释放文件资源,关闭网络连接等操作,还有一个用法是捕获panic
,不过这是错误处理一节中才会涉及到的东西。
循环
虽然没有明令禁止,一般建议不要在for循环中使用defer,如下所示
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}
输出如下
4
3
2
1
0
这段代码结果是正确的,但过程也许不对。在Go中,每创建一个defer,就需要在当前协程申请一片内存空间。假设在上面例子中不是简单的for n循环,而是一个较为复杂的数据处理流程,当外部请求数突然激增时,那么在短时间内就会创建大量的defer,在循环次数很大或次数不确定时,就可能会导致内存占用突然暴涨,这种我们一般称之为内存泄漏。
参数预计算
对于延迟调用有一些反直觉的细节,比如下面这个例子
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}
这个坑还是非常隐晦的,笔者以前就因为这个坑,半天排查不出来是什么原因,可以猜猜输出是什么,答案如下
2
3
1
可能很多人认为是下面这种输出
3
2
1
按照使用者的初衷来说,fmt.Println(Fn1())
这部分应该是希望它们在函数体执行结束后再执行,fmt.Println
确实是最后执行的,但Fn1()
是在意料之外的,下面这个例子的情况就更加明显了。
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}
它的输出一定是3而不是7,如果使用闭包而不是延迟调用,结果又不一样了
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}
闭包的输出是7,那如果把延迟调用和闭包结合起来呢
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}
这次就正常了,输出的是7。下面再改一下,没有闭包了
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}
输出又变回3了。通过对比上面几个例子可以发现这段代码
defer fmt.Println(sum(a,b))
其实等价于
defer fmt.Println(3)
go不会等到最后才去调用sum
函数,sum
函数早在延迟调用被执行以前就被调用了,并作为参数传递了fmt.Println
。总结就是,对于defer
直接作用的函数而言,它的参数是会被预计算的,这也就导致了第一个例子中的奇怪现象,对于这种情况,尤其是在延迟调用中将函数返回值作为参数的情况尤其需要注意。