Go 函数
介绍
每一个程序都包含很多的函数:函数是基本的代码块。
Go 是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main()
函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。而且,同一个任务(函数)可以被调用多次,有助于代码重用。
(事实上,好的程序是非常注意 DRY 原则的,即不要重复你自己(Don’t Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次。)
Go 里面有三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者 lambda 函数
- 方法Methods
除了 main ()
、init ()
函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。
作为提醒,提前介绍一个语法:
这样是不正确的 Go 代码:
1 | func g() |
它必须是这样的:
1 | func g() { |
函数被调用的基本格式如下:
1 | pack1.Function(arg1, arg2, …, argn) |
一个简单的函数调用其他函数的例子:
1 | // greeting.go |
代码输出:
1 | In main before calling greeting |
函数也可以以申明的方式被使用,作为一个函数类型,就像:
1 | type binOp func(int, int) int |
函数参数与返回值
函数能够接收参数我们通过 return
关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 return
或 panic
结尾。供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。
在函数块里面,return
之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支(code-path)都要有 return
语句。
按值传递(call by value) 按引用传递(call by reference)
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)
。
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加 & 符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1)
,此时传递给函数的是一个指针。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用,就像输出文本到终端,发送一个邮件或者是记录一个错误等。
但是绝大部分的函数还是带有返回值的。
如下,simple_function.go
里的 MultiPly3Nums
函数带有三个形参,分别是 a
、b
、c
,还有一个 int
类型的返回值(被注释的代码具有和未注释部分同样的功能,只是多引入了一个本地变量):
1 | // simple_function.go |
输出显示:
1 | Multiply 2 * 5 * 6 = 60 |
如下的两个函数调用有什么不同:
1 | (A) func DoSomething(a *A) { |
命名的返回值(named return variables)
命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的 return 语句。
需要注意的是,即使只有一个命名返回值,也需要使用 ()
括起来。
1 | package main |
输出结果:
1 | num = 10, 2x num = 20, 3x num = 30 |
- return 或 return var 都是可以的。
- 不过
return var = expression
(表达式) 会引发一个编译错误:syntax error: unexpected =, expecting semicolon or newline or }
。
即使函数使用了命名返回值,你依旧可以无视它而返回明确的值。
任何一个非命名返回值(使用非命名返回值是很糟的编程习惯)在 return
语句里面都要明确指出包含返回值的变量或是一个可计算的值(就像上面警告所指出的那样)。
尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。
空白符(blank identifier)
空白符用来匹配一些不需要的值,然后丢弃掉,下面的 blank_identifier.go 就是很好的例子。
ThreeValues
是拥有三个返回值的不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1
与 f1
。第二个返回值赋给了空白符 _
,然后自动丢弃掉。
1 | // blank_identifier.go |
输出结果:
1 | The int: 5, the float: 7.500000 |
另外一个示例,函数接收两个参数,比较它们的大小,然后按小 - 大的顺序返回这两个数,示例代码为 minmax.go
。
1 | // minmax.go |
输出结果:
1 | Minimum is: 65, Maximum is 78 |
改变外部变量(outside variable)
传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return
返回。如下的例子,reply
是一个指向 int
变量的指针,通过这个指针,我们在函数内修改了这个 int
变量的数值。
1 | // side_effect.go |
传递变长参数
如果函数的最后一个参数是采用 ...type
的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变长函数。
1 | func myFunc(a, b, arg ...int) {} |
这个函数接受一个类似某个类型的 slice 的参数,该参数可以通过第 5.4.4 节中提到的 for 循环结构迭代。
1 | func Greeting(prefix string, who ...string) |
在 Greeting 函数中,变量 who
的值为 []string{"Joe", "Anna", "Eileen"}
。
如果参数被存储在一个 slice 类型的变量 slice
中,则可以通过 slice...
的形式来传递参数调用变参函数。
1 | // varnumpar.go |
输出:
1 | The minimum is: 0 |
defer
和追踪
关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return
语句同样可以包含一些操作,而不是单纯地返回某个值)。
关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally
语句块,它一般用于释放某些已分配的资源。
1 | // defer.go |
输出:
1 | In Function1 at the top |
请将 defer 关键字去掉并对比输出结果。
使用 defer 的语句同样可以接受参数,下面这个例子就会在执行 defer 语句时打印 0
:
1 | func a() { |
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出):
1 | func f() { |
上面的代码将会输出:4 3 2 1 0
。
关键字 defer 允许我们进行一些函数执行完成后的收尾工作,例如:
1 | // 关闭文件流 |
内置函数
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。
名称 | 说明 |
---|---|
close |
用于管道通信 |
len 、cap |
len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map) |
new 、make |
new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new (type)、make (type)。new (T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int) 。make (T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作,new () 是一个函数,不要忘记它的括号 |
copy 、append |
用于复制和连接切片 |
panic 、recover |
两者均用于错误处理机制 |
print 、println |
底层打印函数,在部署环境中建议使用 fmt 包 |
complex 、real 、imag |
用于创建和操作复数 |
递归函数
当一个函数在其函数体内调用自身,则称之为递归。最经典的例子便是计算斐波那契数列,即前两个数为 1,从第三个数开始每个数均为前两个数之和。
1 | 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, … |
下面的程序可用于生成该数列:
1 | // fibonacci.go |
输出:
1 | fibonacci(0) is: 1 |
许多问题都可以使用优雅的递归来解决,比如说著名的快速排序算法。
在使用递归函数时经常会遇到的一个重要问题就是栈溢出:一般出现在大量的递归调用导致的程序栈内存分配耗尽。
Go 语言中也可以使用相互调用的递归函数:多个函数之间相互调用形成闭环。因为 Go 语言编译器的特殊性,这些函数的声明顺序可以是任意的。
将函数作为参数
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。下面是一个将函数作为参数的简单例子:
1 | // function_parameter.go |
1 | package main |
输出:
1 | The sum of 1 and 2 is: 3 |
将函数作为参数的最好的例子是函数 strings.IndexFunc()
:
该函数的签名是 func IndexFunc(s string, f func(c int) bool) int
,它的返回值是在函数 f(c)
返回 true、-1 或从未返回时的索引值。
例如 strings.IndexFunc(line, unicode.IsSpace)
就会返回 line
中第一个空白字符的索引值。当然,您也可以书写自己的函数:
1 | func IsAscii(c int) bool { |
闭包
当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }
。
这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body
),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y }
,然后通过变量名对函数进行调用:fplus(3,4)
。
当然,您也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)
。
下面是一个计算从 1 到 1 百万整数的总和的匿名函数:
1 | func() { |
表示参数列表的第一对括号必须紧挨着关键字 func
,因为匿名函数没有名称。花括号 {}
涵盖着函数体,最后的一对括号表示对该匿名函数的调用。
1 | package main |
输出:
1 | 0 - g is of type func(int) and has value 0x681a80 |
使用闭包调试
当您在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtime
或 log
包中的特殊函数来实现这样的功能。包 runtime
中的函数 Caller()
提供了相应的信息,因此可以在需要的时候实现一个 where()
闭包函数来打印函数执行的位置:
1 | where := func() { |
您也可以设置 log
包中的 flag 参数来实现:
1 | log.SetFlags(log.Llongfile) |
或使用一个更加简短版本的 where
函数:
1 | var where = log.Print |