本文最初发表在我的个人博客,查看原文,获得更好的阅读体验
Go中的函数除了可以声明入参之外,还可以声明结果参数(即返回值)。函数可以没有参数或接受多个参数,并且类型在变量名之后。当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。
一 函数声明
语法:
func 函数名 (签名 类型)[([结果签名 ]类型)]][函数体]
例如:
示例1:
// 普通函数
func info() {
fmt.Println("some tips.")
}
示例2:
// 2个入参x, y; 1个未命名结果参数(int类型)
func add(x int, y int) int {
return x + y // 必须要有return
}
上边的示例2还可以简写成这样:
示例3:
// 2个入参x, y; 1个未命名结果参数(int类型)
func add(x, y int) int {
return x + y
}
注意第2行写法变化
1 多返回值
Go一个与众不同的特点是,函数和方法可以返回多个值:
func swap(x, y string) (string, string) {
return y, x
}
上述示例接受两个字符串类型的参数,并将其交换返回。
2 可命名结果参数
Go函数的返回“参数”(或称为结果“参数”)可以给出名称并用作常规变量,就像传入参数一样。如果给出名称,它们将在在函数开始时被初始化为其类型的零值;如果函数执行不带参数的return语句(即直接返回),结果参数的当前值将用作返回值。
例如,上一小节的示例还可以这样写:
func swap(x, y string) (m string, n string) {
m = y
n = x
return m, n
}
直接返回语句应当仅用在短函数中。在长的函数中它们会影响代码的可读性。
另外,名称不是强制的,但可以使代码更简短清晰,它们本身就是文档。
因为命名结果被初始化并与简单的return
相关联,因此它们可以更简洁,例如io.ReadFull
:
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
// 等价于 return n, err
}
如果函数的签名声明了结果参数,则函数体的语句列表必须以终止语句(return
)结束:
示例1:
// 2个入参x, y; 1个结果参数(返回值,int类型)
func add(x int, y int) int {
return x + y // 必须要有return
}
示例2:
func IndexRune(s string, r rune) int {
for i, c := range s {
if c == r {
return i
}
}
// 无效: 此处缺少return语句
}
3 无函数体的函数
函数声明可以省略函数体。这样的声明为Go外部实现的函数提供签名,例如:
func min(x int, y int) int {
if x < y {
return x
}
return y
}
func flushICache(begin, end uintptr) // 此函数由外部实现
二 函数类型
函数类型表示具有相同参数和结果类型的所有函数的集合。
在参数或结果列表中,名称(标识符列表)必须要么全部提供,要么全部省略。如果提供了,则每一个名称代表指定类型的一个条目(参数或结果),并且签名中所有非空白名称必须是唯一的。如果不提供,则每种类型代表该类型的一个条目。参数和结果列表应始终带括号,除非只有一个未命名结果参数,则可以不带括号。
。
func() // 无入参,无结果参数
func(x int) int // 1个入参,1个未命名结果参数
func(x int) (y int, string) // 语法错误:命名和未命名结果参数混合使用
func(a, _ int, z float32) bool
func(a, b int, z float32) bool
func(prefix string, values ...int) // values为可变参数
func(a, b int, z float64, opt ...interface{}) (success bool) //4个入参,1个已命名结果参数;其中入参中含一个可变参数
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T) //函数类型的结果参数
1 函数值
函数也是值,它们可以像其它值一样传递,函数值的类型是函数类型。函数值可以用作函数的参数或返回值。
例如:
package main
import (
"fmt"
"math"
)
// compute函数的入参fn,是一个函数类型的参数;
// 该函数类型同时又有两个入参和一个返回值
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
// 将函数作为值传入函数
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
// 匿名函数值
fmt.Println(compute(func(x, y float64) float64 { return x + y }))
}
2 函数的闭包
Go函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
例如,下面的函数 adder 返回一个闭包。每个闭包都被绑定在其各自的sum
变量上:
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
fmt.Println("sum:", sum)
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 3; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
3 延伸:斐波那契闭包
普通版
package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci() func(int) int {
j := 0
k := 1
z := 0
return func(x int) int {
if x < 2 {
return x
}
z = j + k
j = k
k = z
return z
}
}
func main() {
f := fibonacci()
for i := 0; i < 15; i++ {
fmt.Println(f(i))
}
}
借助平行赋值时的求值顺序:
package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci() func() int {
// 费波那契数列由0和1开始
f1, f2 := 0, 1
return func() int {
// 之后的费波那契系数由之前的两数相加得出
f1, f2 = f2, f1+f2
return f2 - f1
}
}
func main() {
f := fibonacci()
for i := 0; i < 15; i++ {
fmt.Println(f())
}
}
另外,在下一篇文章中,我们还可以看到借助defer语句实现的斐波那契闭包。
三 调用
给定一个F
类型的表达式f
:f(a1, a2, … an)
,用参数a1, a2, … an
调用f
。除一种特殊情况外,这些参数必须是可赋值给F
类型参数的单值表达式,并且在函数调用前进行求值。表达式f
的类型即F
的结果类型。方法调用类似,但方法本身被指定为方法接收器类型的值的选择器。
math.Atan2(x, y) // 函数调用
var pt *Point
pt.Scale(3.5) // 接收器为pt的方法调用
方法就是特殊的函数,后续文章中会介绍。
函数调用中,函数值和参数按正常顺序求值,计算完成后,调用的参数通过值传递给函数,然后被调用的函数开始执行。函数返回时,函数的返回参数也通过值传递给调用方。
函数类型的零值为nil,调用nil函数值会导致运行时恐慌。
上边提到,作为一种特殊情况,如果一个函数或方法g
的返回值在数量上等于另一个函数或方法f
的入参,并且可以单独赋值给f
,那么调用f(g(parameters_of_g))
首先会将g
的返回值按顺序绑定到f
的参数,然后再调用f
。此时f
的调用不能包含除g
的调用之外的参数,并且g
必须至少具有一个返回值。如果f
有类似final ...
的可变参数(见下文),则将g
的返回值优先赋值给常规参数,剩余的返回值继续赋值给可变参数。
func Split(s string, pos int) (string, string) {
return s[0:pos], s[pos:]
}
func Join(s, t string) string {
return s + t
}
if Join(Split(value, len(value)/2)) != value {
log.Panic("test fails")
}
下面的示例中,Split
函数返回3个字符串值,而Join
函数有2个参数,其中,Split
函数的第一个返回值会赋值给Join
函数的第一个参数s
,Split
函数剩余的2个返回值将全部赋值给可变参数t
:
package main
import (
"fmt"
)
func main() {
fmt.Println("可变参数赋值演示:")
s1 := "hello,Golang"
fmt.Println(Join(Split(s1, 6)))
}
func Split(s string, pos int) (string, string, string) {
if pos > len(s) {
return "", "", "No enough strings"
}
return "内容:", s[0:pos], s[pos:]
}
func Join(s string, t ...string) string {
fmt.Println("s: " + s)
fmt.Println("t:", t)
for _, value := range t {
s += value
}
return s
}
1 可变参数
函数或方法签名中最后一个传入的参数可以具有...T
的类型。这样的参数称为可变参数,并且可以使用该参数的零个或多个参数来调用。其中,该参数的类型为[]T
,即可变参数是一个切片类型。向可变参数传递值会生成一个新的切片,其底层包含一个新的数组,数组的元素即为传递的参数。因此,切片的长度和容量就是可变参数的实际数量,并且每次调用都可能不一样。
package main
import (
"fmt"
)
func main() {
fmt.Println(Join("Hello, program languages:", "Golang", "Java", "PHP"))
}
func Join(s string, t ...string) string {
for _, value := range t {
s += " " + value
}
return s
}
如果可变参数没有传参,则该参数为
nil
。
如果最后一个参数(实参)可赋值给切片类型[]T
,则将该实参后跟...
传递给...T
参数(形参)可以保持其值不变。这种情况下,不会创建新的切片。
示例:
s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...) // Greeting的可变参数与s具有相同的底层数组。
参考:
https://golang.org/doc/effective_go.html#functions
https://golang.org/ref/spec#Calls
https://zh.wikipedia.org/wiki/斐波那契数列