Go 语言函数

函数是 Go 里面的基本代码块,Go 函数的功能非常强大,以至于被认为拥有函数式编程语言的多种特性。由于函数内容较多,这里将会分为多个章节去讲解:

函数参数与返回值

函数能够接收参数供自己使用,也可以返回零个或多个值。相比 C、C++、Java 和 C#,多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行提供了更方便的实现方案。

我们通过 return 关键字返回值。事实上,任何一个有返回值的函数都必须以 returnpanic 结尾。

在函数块里面,return 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支都要有 return 语句。

下面的函数将会编译错误,原因是什么大家可以自己去试试:

  1. func (st *Stack) Pop() int {
  2. v := 0
  3. for ix := len(st) - 1; ix >= 0; ix-- {
  4. if v = st[ix]; v != 0 {
  5. st[ix] = 0
  6. return v
  7. }
  8. }
  9. }

函数定义时,它的形参一般是有参数名的,不过我们也可以定义没有参数名的函数,只有相应的形参类型,就像这样:

  1. func f(int, int, float64)。

没有参数的函数通常被称为 niladic 函数(niladic function),就像 main.main()

按值传递(call by value) 按引用传递(call by reference)

Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)

如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。

在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的使用 & 指出指针)。

下面示例中的的 MultiPly3Nums 函数带有三个形参,分别是 a、b、c,还有一个 int 类型的返回值(被注释的代码具有和未注释部分同样的功能,只是多引入了一个本地变量):

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Printf("Multiply 2 * 5 * 6 = %d\n", MultiPly3Nums(2, 5, 6))
  5. // var i1 int = MultiPly3Nums(2, 5, 6)
  6. // fmt.Printf("MultiPly 2 * 5 * 6 = %d\n", i1)
  7. }
  8. func MultiPly3Nums(a int, b int, c int) int {
  9. // var product int = a * b * c
  10. // return product
  11. return a * b * c
  12. }

输出显示:

  1. Multiply 2 * 5 * 6 = 60

如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。

命名的返回值(named return variables)

看下面这个示例(multiple_return.go),getX2AndX3getX2AndX3_2 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int)

命名返回值作为结果形参被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来。

示例 multiple_return.go:

  1. package main
  2. import "fmt"
  3. var num int = 10
  4. var numx2, numx3 int
  5. func main() {
  6. numx2, numx3 = getX2AndX3(num)
  7. PrintValues()
  8. numx2, numx3 = getX2AndX3_2(num)
  9. PrintValues()
  10. }
  11. func PrintValues() {
  12. fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
  13. }
  14. func getX2AndX3(input int) (int, int) {
  15. return 2 * input, 3 * input
  16. }
  17. func getX2AndX3_2(input int) (x2 int, x3 int) {
  18. x2 = 2 * input
  19. x3 = 3 * input
  20. // return x2, x3
  21. return
  22. }

输出结果:

  1. num = 10, 2x num = 20, 3x num = 30
  2. num = 10, 2x num = 20, 3x num = 30

注意:return 或 return var 都是可以的。不过 return var = expression(表达式) 会引发一个编译错误:syntax error: unexpected =, expecting semicolon or newline or }

即使函数使用了命名返回值,你依旧可以无视它而返回明确的值。

任何一个非命名返回值(使用非命名返回值是很糟的编程习惯)在 return 语句里面都要明确指出包含返回值的变量或是一个可计算的值(就像上面警告所指出的那样)。

尽量使用命名返回值,会使代码更清晰、更简短,同时更加容易读懂。


练习:编写一个函数,接收两个整数,然后返回它们的和、积与差。编写两个版本,一个是非命名返回值,一个是命名返回值。

编写一个名字为 MySqrt 的函数,计算一个 float64 类型浮点数的平方根,如果参数是负数将返回一个错误。编写两个版本,一个是非命名返回值,一个是命名返回值。

空白符(blank identifier)

空白符用来匹配一些不需要的值,然后丢弃掉,下面的 blank_identifier.go 就是很好的例子。

ThreeValues 是拥有三个返回值且不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1 与 f1。第二个返回值赋给了空白符 _,然后自动丢弃掉。

示例 blank_identifier.go :

  1. package main
  2. import "fmt"
  3. func main() {
  4. var i1 int
  5. var f1 float32
  6. i1, _, f1 = ThreeValues()
  7. fmt.Printf("The int: %d, the float: %f \n", i1, f1)
  8. }
  9. func ThreeValues() (int, int, float32) {
  10. return 5, 6, 7.5
  11. }

输出结果:

  1. The int: 5, the float: 7.500000

另外一个示例 minmax.go,函数接收两个参数,比较它们的大小,然后按从小到大的顺序返回这两个数。

示例 minmax.go :

  1. package main
  2. import "fmt"
  3. func main() {
  4. var min, max int
  5. min, max = MinMax(78, 65)
  6. fmt.Printf("Minmium is: %d, Maximum is: %d\n", min, max)
  7. }
  8. func MinMax(a int, b int) (min int, max int) {
  9. if a < b {
  10. min = a
  11. max = b
  12. } else { // a = b or a < b
  13. min = b
  14. max = a
  15. }
  16. return
  17. }

输出结果:

  1. Minimum is: 65, Maximum is 78

改变外部变量(outside variable)

传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回。

如下的例子 side_effect.go ,reply 是一个指向 int 变量的指针,通过这个指针,我们在函数内修改了这个 int 变量的数值。

示例 side_effect.go

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. // this function changes reply:
  6. func Multiply(a, b int, reply *int) {
  7. *reply = a * b
  8. }
  9. func main() {
  10. n := 0
  11. reply := &n
  12. Multiply(10, 5, reply)
  13. fmt.Println("Multiply:", *reply) // Multiply: 50
  14. }

这仅仅是个演示的例子,当需要在函数内改变一个占用内存比较大的变量时,性能优势就更加明显了。然而,如果不小心使用的话,传递一个指针很容易引发一些不确定的事,所以,我们要十分小心那些可以改变外部变量的函数,在必要时,需要添加注释以便其他人能够更加清楚的知道函数里面到底发生了什么。

分类导航