1. 6.2 函数参数与返回值
函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。相比与 C、C++、Java 和 C#,多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行(参考 第 5.2 节)提供了方便。
我们通过 return
关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 return
或 panic
(参考 第 13 章)结尾。
在函数块里面,return
之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支(code-path)都要有 return
语句。
问题 6.1:下面的函数将不会被编译,为什么呢?大家可以试着纠正过来。
func (st *Stack) Pop() int {
v := 0
for ix := len(st) - 1; ix >= 0; ix-- {
if v = st[ix]; v != 0 {
st[ix] = 0
return v
}
}
}
函数定义时,它的形参一般是有名字的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,就像这样:func f(int, int, float64)
。
没有参数的函数通常被称为 niladic 函数(niladic function),就像 main.main()
。
1.1. 6.2.1 按值传递(call by value) 按引用传递(call by reference)
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)
。
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1)
,此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(译者注:指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。)
几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用,就像输出文本到终端,发送一个邮件或者是记录一个错误等。
但是绝大部分的函数还是带有返回值的。
如下,simple_function.go 里的 MultiPly3Nums
函数带有三个形参,分别是 a
、b
、c
,还有一个 int
类型的返回值(被注释的代码具有和未注释部分同样的功能,只是多引入了一个本地变量):
示例 6.2 simple_function.go
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
如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。
问题 6.2:
如下的两个函数调用有什么不同:
(A) func DoSomething(a *A) {
b = a
}
(B) func DoSomething(a A) {
b = &a
}
1.2. 6.2.2 命名的返回值(named return variables)
如下,multiple_return.go 里的函数带有一个 int
参数,返回两个 int
值;其中一个函数的返回值在函数调用时就已经被赋予了一个初始零值。
getX2AndX3
与 getX2AndX3_2
两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 ()
把它们括起来,比如 (int, int)
。
命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 ()
括起来(参考 第 6.6 节的 fibonacci.go 函数)。
示例 6.3 multiple_return.go
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 }
。
即使函数使用了命名返回值,你依旧可以无视它而返回明确的值。
任何一个非命名返回值(使用非命名返回值是很糟的编程习惯)在 return
语句里面都要明确指出包含返回值的变量或是一个可计算的值(就像上面警告所指出的那样)。
尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。
练习 6.1 mult_returnval.go
编写一个函数,接收两个整数,然后返回它们的和、积与差。编写两个版本,一个是非命名返回值,一个是命名返回值。
练习 6.2 error_returnval.go
编写一个名字为 MySqrt 的函数,计算一个 float64 类型浮点数的平方根,如果参数是一个负数的话将返回一个错误。编写两个版本,一个是非命名返回值,一个是命名返回值。
1.3. 6.2.3 空白符(blank identifier)
空白符用来匹配一些不需要的值,然后丢弃掉,下面的 blank_identifier.go 就是很好的例子。
ThreeValues
是拥有三个返回值的不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1
与 f1
。第二个返回值赋给了空白符 _
,然后自动丢弃掉。
示例 6.4 blank_identifier.go
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
另外一个示例,函数接收两个参数,比较它们的大小,然后按小-大的顺序返回这两个数,示例代码为minmax.go。
示例 6.5 minmax.go
package main
import "fmt"
func main() {
var min, max int
min, max = MinMax(78, 65)
fmt.Printf("Minmium is: %d, Maximum is: %d\n", min, max)
}
func MinMax(a int, b int) (min int, max int) {
if a < b {
min = a
max = b
} else { // a = b or a < b
min = b
max = a
}
return
}
输出结果:
Minimum is: 65, Maximum is 78
1.4. 6.2.4 改变外部变量(outside variable)
传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return
返回。如下的例子,reply
是一个指向 int
变量的指针,通过这个指针,我们在函数内修改了这个 int
变量的数值。
示例 6.6 side_effect.go
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
}
这仅仅是个指导性的例子,当需要在函数内改变一个占用内存比较大的变量时,性能优势就更加明显了。然而,如果不小心使用的话,传递一个指针很容易引发一些不确定的事,所以,我们要十分小心那些可以改变外部变量的函数,在必要时,需要添加注释以便其他人能够更加清楚的知道函数里面到底发生了什么。