函数声明和调用已经在前面的文章中解释过了。当前这篇文章将介绍更多关于函数的概念和细节。
事实上,在Go中,函数是一种一等公民类型。换句话说,我们可以把函数当作值来使用。尽管Go是一门静态语言,但是Go函数的灵活性宛如甚至超越了很多动态语言。
Go中有一些内置函数,这些函数展示在
builtin
和
unsafe
标准包中。内置函数和自定义函数有很多差别。这些差别将在下面逐一提及。
刚已经提到了,在Go中,函数是一种一等公民类型。一个函数类型的字面表示形式由一个func
关键字和一个函数签名字面表示表示形式组成。一个函数签名由一个输入参数类型列表和一个输出结果类型列表组成。参数名称和结果名称可以出现函数签名的字面表示形式中,但是它们并不重要。
func
关键字可以出现在函数签名的字面形式中,也可以不出现。鉴于此,我们常常混淆使用函数类型(见下)和函数签名这两个概念。
下面是一个函数类型的字面形式:
func (a int, b string, c string) (x int, y int, z bool)
从前面的
函数声明和调用一文中,我们了解到连续的同类型参数和结果可以声明在一块儿。所以上面的字面形式等价于:
func (a int, b, c string) (x, y int, z bool)
参数名称和结果名称并不重要,只要它们不重名即可。上面两个字面形式等价于下面这个:
func (x int, y, z string) (a, b int, c bool)
参数名和结果名可以是空标识符_
。上面的字面形式等价于:
func (_ int, _, _ string) (_, _ int, _ bool)
函数参数列表中的参数名或者结果列表中的结果名可以同时省略(即匿名)。上面的字面形式等价于:
func (int, string, string) (int, int, bool) // 标准函数字面形式
func (a int, b string, c string) (int, int, bool)
func (x int, _ string, z string) (int, int, bool)
func (int, string, string) (x int, y int, z bool)
func (int, string, string) (a int, b int, _ bool)
所有上面列出的函数类型字面形式表示同一个(无名)函数类型。
参数列表必须用一对小括号()
括起来,即使此列表为空。如果一个函数类型一个结果列表为空,则它可以在函数类型的字面形式中被省略掉。当一个结果列表含有最多一个结果,则此结果列表的字面形式在它不包含结果名称的时候可以不用括号()
括起来。
// 这三个函数类型字面形式是等价的。
func () (x int)
func () (int)
func () int
// 这两个函数类型字面形式是等价的。
func (a int, b string) ()
func (a int, b string)
一个函数的最后一个参数可以是一个变长参数。一个函数可以最多有一个变长参数。一个变长参数的类型总为一个切片类型。变长参数在声明的时候必须在它的(切片)类型的元素类型前面前置三个点...
,以示这是一个变长参数。两个变长函数类型的例子:
func (values ...int64) (sum int64)
func (sep string, tokens ...string) string
一个变长函数类型和一个非变长函数类型绝对不可能是同一个类型。
后面的一节将展示几个变长函数声明和使用的例子。
Go类型系统概述一文已经提到了函数类型属于不可比较类型。但是,和映射值以及切片值类似,一个函数值可以和类型不确定的
nil
比较。(函数值将在本文最后一节介绍。)
因为函数类型属于不可比较类型,所以函数类型不可用做映射类型的键值类型。
一个函数原型由一个函数名称和一个函数类型(或者说一个函数签名)组成。它的字面形式由一个func
关键字、一个函数名和一个函数签名字面形式组成。
一个函数原型的例子:
func Double(n int) (result int)
换句话说,一个函数原型可以看作是一个不带函数体的函数声明;或者说一个函数声明由一个函数原型和一个函数体组成。
普通非变长函数的声明和调用已经在
函数声明和调用一文中介绍过了。本节将介绍变长函数的声明和调用。
变长函数声明和普通函数声明类似,只不过最后一个参数必须为变长参数。一个变长参数在函数体内将被视为一个切片。
// Sum返回所有输入实参的和。
func Sum(values ...int64) (sum int64) {
// values的类型为[]int64。
sum = 0
for _, v := range values {
sum += v
}
return
}
// Concat是一个低效的字符串拼接函数。
func Concat(sep string, tokens ...string) string {
// tokens的类型为[]string。
r := ""
for i, t := range tokens {
if i != 0 {
r += sep
}
r += t
}
return r
}
从上面的两个变长参数函数声明可以看出,如果一个变长参数的类型部分为...T
,则此变长参数的类型实际为[]T
。
事实上,在前面的文章中多次使用过的fmt
标准库包中的Print
、Println
和Printf
函数均为变长参数函数。它们的声明大致如下:
func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)
这三个函数中的变长参数的类型均为
[]interface{}
。此类型的元素类型为
interface{}
,这是一个接口类型。接口类型和接口值将在后面的
接口一文中详述。
在变长参数函数调用中,可以使用两种风格的方式将实参传递给类型为[]T
的变长形参:
-
传递一个切片做为实参。此切片必须可以被赋值给类型为[]T
的值(或者说此切片可以被隐式转换为类型[]T
)。此实参切片后必须跟随三个点...
。
-
传递零个或者多个可以被隐式转换为T
的实参(或者说这些实参可以赋值给类型为T
的值)。这些实参将被添加入一个匿名的在运行时刻创建的类型为[]T
的切片中,然后此切片将被传递给此函数调用。
注意,这两种风格的方式不可在同一个变长参数函数调用中混用。
下面这个例子展示了一些变长参数函数调用:
package main
import "fmt"
func Sum(values ...int64) (sum int64) {
sum = 0
for _, v := range values {
sum += v
}
return
}
func main() {
a0 := Sum()
a1 := Sum(2)
a3 := Sum(2, 3, 5)
// 上面三行和下面三行是等价的。
b0 := Sum([]int64{}...) // <=> Sum(nil...)
b1 := Sum([]int64{2}...)
b3 := Sum([]int64{2, 3, 5}...)
fmt.Println(a0, a1, a3) // 0 2 10
fmt.Println(b0, b1, b3) // 0 2 10
}
另一个展示了一些变长参数函数调用的例子:
package main
import "fmt"
func Concat(sep string, tokens ...string) (r string) {
for i, t := range tokens {
if i != 0 {
r += sep
}
r += t
}
return
}
func main() {
tokens := []string{"Go", "C", "Rust"}
langsA := Concat(",", tokens...) // 风格1
langsB := Concat(",", "Go", "C","Rust") // 风格2
fmt.Println(langsA == langsB) // true
}
下面这个例子编译不通过,因为两种调用风格混用了。
package main
// 这两个函数的声明见前面几例。
func Sum(values ...int64) (sum int64) {......}
func Concat(sep string, tokens ...string) string {......}
func main() {
// 下面两行报同样的错:实参数目太多了。
_ = Sum(2, []int64{3, 5}...)
_ = Concat(",", "Go", []string{"C", "Rust"}...)
}
一般来说,同一个包中声明的函数的名称不能重复,但有两个例外:
-
-
多个函数的名称可以被声明为空标识符_
。这样声明的函数不可被调用。
大多数函数调用都是在运行时刻被估值的。但
unsafe
标准库包中的函数的调用都是在编译时刻估值的。另外,某些其它内置函数(比如
len
和
cap
等)的调用在所传实参满足一定的条件的时候也将在编译时刻估值。详见
在编译时刻估值的函数调用。
再重申一次,和赋值一样,传参也属于值(浅)复制。当一个值被复制时,只有它的
直接部分被复制了。
我们可以使用
Go汇编(Go assembly)来实现一个Go函数。Go汇编代码放在后缀为
.a
的文件中。一个使用Go汇编实现的函数依旧必须在一个
*.go
文件中声明,但是它的声明必须不能含有函数体。换句话说,一个使用Go汇编实现的函数的声明中只含有它的原型。
如果一个函数有返回值,则它的函数体内的最后一条语句必须为一条
终止语句。Go中有多种终止语句,
return
语句只是其中一种。所以一个有返回值的函数的体内不一定需要一个
return
语句。比如下面两个函数(它们均可编译通过):
func fa() int {
a:
goto a
}
func fb() bool {
for{}
}
自定义函数的调用结果都是可以被舍弃掉的。但是大多数内置函数(除了recover
和copy
)的调用结果都是不可被舍弃的。调用结果不可被舍弃的函数是不可以被用做延迟调用函数和协程起始函数的,比如append
函数。
一个有且只有一个返回值的函数的每个调用总可以被当成一个单值表达式使用。比如,它可以被内嵌在其它函数调用中当作实参使用,或者可以被当作其它表达式中的操作数使用。
如果一个有多个返回结果的函数的一个调用的返回结果没有被舍弃,则此调用可以当作一个多值表达式使用在两种场合:
-
此调用可以在一个赋值语句中当作源值来使用,但是它不能和其它源值掺和到一块。
-
此调用可以内嵌在另一个函数调用中当作实参来使用,但是它不能和其它实参掺和到一块。
一个例子:
package main
func HalfAndNegative(n int) (int, int) {
return n/2, -n
}
func AddSub(a, b int) (int, int) {
return a+b, a-b
}
func Dummy(values ...int) {}
func main() {
// 这几行编译没问题。
AddSub(HalfAndNegative(6))
AddSub(AddSub(AddSub(7, 5)))
AddSub(AddSub(HalfAndNegative(6)))
Dummy(HalfAndNegative(6))
_, _ = AddSub(7, 5)
// 下面这几行编译不通过。
/*
_, _, _ = 6, AddSub(7, 5)
Dummy(AddSub(7, 5), 9)
Dummy(AddSub(7, 5), HalfAndNegative(6))
*/
}
本文开头已经介绍了函数类型是Go中天然支持的一种类型。函数类型的值称为函数值。在字面上,函数类型的零值也使用预定义的nil
来表示。
当我们声明了一个函数的时候,我们实际上同时声明了一个不可修改的函数值。此函数值用此函数的名称来标识。此函数值的类型的字面表示形式为此函数的原型刨去函数名部分。
注意:内置函数和init
函数不可被用做函数值。
任何函数值都可以被当作普通声明函数来调用。调用一个nil函数来开启一个协程将产生一个致命的不可恢复的错误,此错误将使整个程序崩溃。在其它情况下调用一个nil函数将产生一个可恢复的恐慌。
从
值部一文,我们得知,当一个函数值被赋给另一个函数值后,这两个函数值将共享底层部分(内部的函数结构)。换句话说,这两个函数值表示的函数可以看作是同一个函数。调用它们的效果是相同的。
一个例子:
package main
import "fmt"
func Double(n int) int {
return n + n
}
func Apply(n int, f func(int) int) int {
return f(n) // f的类型为"func(int) int"
}
func main() {
fmt.Printf("%T\n", Double) // func(int) int
// Double = nil // error: Double是不可修改的
var f func(n int) int // 默认值为nil
f = Double
g := Apply
fmt.Printf("%T\n", g) // func(int, func(int) int) int
fmt.Println(f(9)) // 18
fmt.Println(g(6, Double)) // 12
fmt.Println(Apply(6, f)) // 12
}
在上例中,g(6, Double)
和Apply(6, f)
是等价的。
在实践中,我们常常将一个匿名函数赋值给一个函数类型的变量,从而可以在以后多次调用此匿名函数。
package main
import "fmt"
func main() {
// 此函数返回一个函数类型的结果,亦即闭包(closure)。
isMultipleOfX := func (x int) func(int) bool {
return func(n int) bool {
return n%x == 0
}
}
var isMultipleOf3 = isMultipleOfX(3)
var isMultipleOf5 = isMultipleOfX(5)
fmt.Println(isMultipleOf3(6)) // true
fmt.Println(isMultipleOf3(8)) // false
fmt.Println(isMultipleOf5(10)) // true
fmt.Println(isMultipleOf5(12)) // false
isMultipleOf15 := func(n int) bool {
return isMultipleOf3(n) && isMultipleOf5(n)
}
fmt.Println(isMultipleOf15(32)) // false
fmt.Println(isMultipleOf15(60)) // true
}
Go中所有的函数都可以看作是闭包,这是Go语言编程常常给人一种和动态脚本语言一样灵活的一个重要原因。
从Go 1.23开始,底层类型为下列函数类型的函数值可以使用for-range
循环来遍历。
// K和V是特定类型
func(yield func() bool)
func(yield func(V) bool)
func(yield func(K, V) bool)
这样的函数值称为推遍历器(push iterator),常简称为遍历器。
当使用一个for-range
循环遍历这样的一个遍历器函数值时,这个遍历器函数值将被调用(一次)并被传入一个隐式创建的yield
回调函数。此yield
回调函数返回一个bool
结果。当它返回false
时,这个遍历器函数的调用应该(但并不强求一定)立即退出;否则(当此yield
回调函数返回true
时),这个遍历器函数应该继续执行,直到自然退出。
下面是一些使用遍历器函数的例子:
package main
import "fmt"
func Loop3(yield func() bool) {
for range 3 {
if (!yield()) {
return
}
}
}
func OneDigitNumbers(onValue func(int) bool) {
for i := range 10 {
if (!onValue(i)) {
return
}
}
}
func SquareLessThan50(onKeyValue func(int, int) bool) {
for i := range 8 {
if (!onKeyValue(i, i*i)) {
return
}
}
}
func main() {
var n = 0
for range Loop3 {
fmt.Print(n)
n++
}
fmt.Println()
// 输出:012
for i := range OneDigitNumbers {
fmt.Print(i)
}
fmt.Println()
// 输出:0123456789
for i, ii := range SquareLessThan50 {
fmt.Printf("%v:%v ", i, ii)
}
fmt.Println()
// 输出:0:0 1:1 2:4 3:9 4:16 5:25 6:36 7:49
}
上面这些for-range
循环和下面这些相应的函数调用是等价的:
func main() {
var n = 0
Loop3(func() bool {
fmt.Print(n)
n++
return true
})
fmt.Println()
OneDigitNumbers(func(i int) bool {
fmt.Print(i)
return true
})
fmt.Println()
SquareLessThan50(func(i, ii int) bool {
fmt.Printf("%v:%v ", i, ii)
return true
})
fmt.Println()
}
是的,我们可以认为Go 1.23引入的遍历函数特性是一个语法糖。