介绍
每一个程序都包含很多的函数:函数是基本的代码块。
Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
Go 里面拥三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者lambda函数
- 方法
所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。
注意
go语言方法后面要紧跟大括号
这样是不正确的 Go 代码:
func g() { }
它必须是这样的:
func g() { }
函数被调用的基本格式如下:
pack1.Function(arg1, arg2, …, argn)
Function 是 pack1 包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参(parameter)。
一个简单的函数调用其他函数的例子:
package main func main() { println("In main before calling greeting") greeting() println("In main after calling greeting") } func greeting() { println("In greeting: Hi!!!!!") }
代码输出:
In main before calling greeting
In greeting: Hi!!!
In main after calling greeting
注意
函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如:
假设 f1 需要 3 个参数 f1(a, b, c int) ,同时 f2 返回 3 个参数 f2(a, b int) (int, int, int) ,就可以这样调用 f1: f1(f2(a, b)) 。
注意在 Go 里面函数重载是不被允许的。这将导致一个编译错误:funcName redeclared in this book, previous declaration at lineno
Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以你需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名。如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:
func flushICache(begin, end uintptr) // implemented externally
函数也可以以申明的方式被使用,作为一个函数类型,就像:type binOp func(int, int) int在这里,不需要函数体 {} 。
函数参数与返回值
- 按值传递(call by value) 按引用传递(call by reference)
- 命名的返回值(named return variables)
- 空白符(blank identifier)
- 改变外部变量(outside variable)
函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行提供了方便。
我们通过 return 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以return 或 panic 结尾。
在函数块里面, return 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支(code-path)都要有 return 语句。
按值传递 按引用传递
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1) 。
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1) ,此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。
几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显示的指出指针)。
package main import "fmt" func main() { fmt.Printf("Multiply 2 * 5 * 6 = %d\n", MultiPly3Nums(2, 5, 6)) // var i1 int = MultiPly3Nums(2, 5, 6) // fmt.Printf("MultiPly 2 * 5 * 6 = %d\n", i1) } func MultiPly3Nums(a int, b int, c int) int { // var product int = a * b * c // return product return a * b * c }
输出显示:
Multiply 2 * 5 * 6 = 60
如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。
命名的返回值(named return variables)
函数带有一个 int 参数,返回两个 int 值;其中一个函数的返回值在函数调用时就已经被赋予了一个初始零值。getX2AndX3 与 getX2AndX3_2 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int) 。命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来。
package main import "fmt" var num int = 10 var numx2, numx3 int func main() { numx2, numx3 = getX2AndX3(num) PrintValues() numx2, numx3 = getX2AndX3_2(num) PrintValues() } func PrintValues() { fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3) } func getX2AndX3(input int) (int, int) { return 2 * input, 3 * input } func getX2AndX3_2(input int) (x2 int, x3 int) { x2 = 2 * input x3 = 3 * input // return x2, x3 return }
输出结果:
num = 10, 2x num = 20, 3x num = 30
num = 10, 2x num = 20, 3x num = 30
警告:
return 或 return var 都是可以的。
不过 return var = expression (表达式) 会引发一个编译错误:
syntax error: unexpected =, expecting semicolon or newline or } 。
空白符(blank identifier)
空白符用来匹配一些不需要的值,然后丢弃掉,下面的 blank_identifier.go 就是很好的例子。ThreeValues 是拥有三个返回值的不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1 与 f1 。第二个返回值赋给了空白符 _ ,然后自动丢弃掉。
package main import "fmt" func main() { var i1 int var f1 float32 i1, _, f1 = ThreeValues() fmt.Printf("The int: %d, the float: %f \n", i1, f1) } func ThreeValues() (int, int, float32) { return 5, 6, 7.5 }
输出结果:
The int: 5, the float: 7.500000
改变外部变量
传递指针给函数不但可以节省内存,而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回。如下的例子, reply 是一个指向 int 变量的指针,通过这个指针,我们在函数内修改了这个 int 变量的数值。
package main import ( "fmt" ) // this function changes reply: func Multiply(a, b int, reply *int) { *reply = a * b } func main() { n := 0 reply := &n Multiply(10, 5, reply) fmt.Println("Multiply:", *reply) // Multiply: 50 }
然而,如果不小心使用的话,传递一个指针很容易引发一些不确定的事,所以,我们要十分小心那些可以改变外部变量的函数,在必要时,需要添加注释以便其他人能够更加清楚的知道函数里面到底发生了什么。
传递变长参数
如果函数的最后一个参数是采用 …type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
func myFunc(a, b, arg ...int) {}
示例函数和调用:
func Greeting(prefix string, who ...string) Greeting("hello:", "Joe", "Anna", "Eileen")
在 Greeting 函数中,变量 who 的值为 []string{“Joe”, “Anna”, “Eileen”} 。
如果参数被存储在一个数组 arr 中,则可以通过 arr… 的形式来传递参数调用变参函数。
package main import "fmt" func main() { x := Min(1, 3, 2, 0) fmt.Printf("The minimum is: %d\n", x) arr := []int{7,9,3,5,1} x = Min(arr...) fmt.Printf("The minimum in the array arr is: %d", x) } func Min(a ...int) int { if len(a)==0 { return 0 } min := a[0] for _, v := range a { if v < min { min = v } } return min }