函数基础
函数对应的英文单词是 Function,Function 这个单词原本是功能、职责的意思。
编程语言使用 Function 这个单词,表示将一个大问题分解后而形成的、若干具有特定功能或职责的小任务,可以说十分贴切。函数代表的小任务可以在一个程序中被多次使用,甚至可以在不同程序中被使用,因此函数的出现也提升了整个程序界代码复用的水平。
在 Go 语言中,函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块(Go 语言中的方法本质上也是函数)。
如果忽略 Go 包在 Go 代码组织层面的作用,我们可以说 Go 程序就是一组函数的集合,实际上,我们日常的 Go 代码编写大多都集中在实现某个函数上。
# 1. 函数基础
# 1.1 函数声明
在 Go 中,定义一个函数的最常用方式就是使用函数声明。
package main
import "fmt"
func main() {
ret := intSum(1, 2)
fmt.Println(ret) // 3
}
func intSum(x, y int) int {
return x + y
}
2
3
4
5
6
7
8
9
10
11
12
以 Go 标准库 fmt 包提供的 Fprintf 函数为例,一个普通 Go 函数的声明如下:
# 1.1.1 组成部分
一个 Go 函数的声明由五部分组成:
- 第一部分:关键字 func。Go 函数声明必须以关键字 func 开始。
- **第二部分:函数名。**函数名是指代函数定义的标识符,函数声明后,可以通过函数名这个标识符来使用这个函数。在同一个 Go 包中,函数名应该是唯一的,并且它也遵守 Go 标识符的导出规则,首字母大写的函数名指代的函数是可以在包外使用的,小写的就只在包内可见。
- **第三部分:参数列表。**参数列表中声明了我们将要在函数体中使用的各个参数。参数列表紧接在函数名的后面,并用一个括号包裹。它使用逗号作为参数间的分隔符,而且每个参数的参数名在前,参数类型在后,这和变量声明中变量名与类型的排列方式是一致的。另外,Go 函数支持变长参数,也就是一个形式参数可以对应数量不定的实际参数。Fprintf 就是一个支持变长参数的函数,你可以看到它第三个形式参数 a 就是一个变长参数,而且变长参数与普通参数在声明时的不同点,就在于它会在类型前面增加了一个“…”符号。
- **第四部分:返回值列表。**返回值承载了函数执行后要返回给调用者的结果,返回值列表声明了这些返回值的类型,返回值列表的位置紧接在参数列表后面,两者之间用一个空格隔开。不过,上图中比较特殊,Fprintf 函数的返回值列表不仅声明了返回值的类型,还声明了返回值的名称,这种返回值被称为具名返回值。多数情况下,我们不需要这么做,只需声明返回值的类型即可。
- **第五部分:函数体。**放在一对大括号内,函数的具体实现都放在这里。不过,函数声明中的函数体是可选的。如果没有函数体,说明这个函数可能是在 Go 语言之外实现的,比如使用汇编语言实现,然后通过链接器将实现与声明中的函数名链接到一起。
# 1.1.2 函数类型
变量声明和函数声明转换:
函数声明中的函数名其实就是变量名,函数声明中的 func 关键字、参数列表和返回值列表共同构成了函数类型。
而参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素。因此,函数类型也可以看成是由 func 关键字与函数签名组合而成的。
通常,在表述函数类型时,我们会省略函数签名参数列表中的参数名,以及返回值列表中的返回值变量名。比如上面 Fprintf 函数的函数类型是:
func(io.Writer, string, ...interface{}) (int, error)
这样,如果两个函数类型的函数签名是相同的,即便参数列表中的参数名,以及返回值列表中的返回值变量名都是不同的,那么这两个函数类型也是相同类型,比如下面两个函数类型:
func (a int, b string) (results []string, err error)
func (c int, d string) (sl []string, err error)
2
结论:
每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例
# 1.1.3 匿名函数
s := T{} // 使用复合类型字面值对结构体类型T的变量进行显式初始化
f := func(){} // 使用变量声明形式的函数声明
2
T{}被称为复合类型字面值,那么处于同样位置的 func(){}叫“函数字面值(Function Literal)”。
可以看到,函数字面值由函数类型与函数体组成,它特别像一个没有函数名的函数声明,因此我们也叫它匿名函数。匿名函数在 Go 中用途很广。
# 1.1.4 函数参数
函数分为声明与使用两个阶段,在不同阶段,参数的称谓也有不同。
- 函数声明阶段,把参数列表中的参数叫做形式参数(Parameter,简称形参),在函数体中,使用的都是形参;
- 函数实际调用阶段,传入的参数被称为实际参数(Argument,简称实参)。
当我们实际调用函数的时候,实参会传递给函数,并和形式参数逐一绑定,编译器会根据各个形参的类型与数量,来检查传入的实参的类型与数量是否匹配。只有匹配,程序才能继续执行函数调用,否则编译器就会报错。
Go 语言中,函数参数传递采用是值传递的方式。所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中。
- 像整型、数组、结构体这类类型的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。
- 像 string、切片、map 这些类型的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为**“浅拷贝”**。
函数参数的传递有两个例外:形参为接口类型、变长参数。简单的值传递无法满足要求。
Go 编译器会介入:对于类型为接口类型的形参,Go 编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go 编译器会将零个或多个实参按一定形式转换为对应的变长形参。
// myAppend 函数,这个函数基于 append,实现了向一个整型切片追加数据的功能。
func myAppend(sl []int, elems ...int) []int {
fmt.Printf("%T\n", elems) // []int
if len(elems) == 0 {
println("no elems to append")
return sl
}
sl = append(sl, elems...)
return sl
}
func main() {
sl := []int{1, 2, 3}
sl = myAppend(sl) // no elems to append
fmt.Println(sl) // [1 2 3]
sl = myAppend(sl, 4, 5, 6)
fmt.Println(sl) // [1 2 3 4 5 6]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在 Go 中,变长参数实际上是通过切片来实现的。这会大大简化了变长参数的使用复杂度。比如 myAppend 中,使用 len 函数就可以获取到传给变长参数的实参个数。
# 1.1.5 函数支持多返回值
Go 函数支持多返回值。多返回值可以让函数将更多结果信息返回给它的调用者,Go 语言的错误处理机制很大程度就是建立在多返回值的机制之上的。
函数返回值列表从形式上看主要有三种:
func foo() // 无返回值
func foo() error // 仅有一个返回值
func foo() (int, string, error) // 有2或2个以上返回值
2
3
在函数声明的返回值列表中,通常会仅列举返回值的类型,也可以像 fmt.Fprintf 函数的返回值列表那样,为每个返回值声明变量名,这种带有名字的返回值被称为具名返回值(Named Return Value)。
这种具名返回值变量可以像函数体中声明的局部变量一样在函数体内使用。
Go 标准库以及大多数项目代码中的函数,都选择了使用普通的非具名返回值形式。
使用具名返回值的场景:
- 当函数使用 defer,而且还在 defer 函数中修改外部函数返回值时,具名返回值可以让代码显得更优雅清晰。
- 当函数的返回值个数较多时,每次显式使用 return 语句都会接一长串返回值,这时用具名返回值可以让函数实现的可读性更好一些,比如 Go 标准库 time 包中的 parseNanoseconds 函数:
// $GOROOT/src/time/format.go
func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) {
if !commaOrPeriod(value[0]) {
err = errBad
return
}
if ns, err = atoi(value[1:nbytes]); err != nil {
return
}
if ns < 0 || 1e9 <= ns {
rangeErrString = "fractional second"
return
}
scaleDigits := 10 - nbytes
for i := 0; i < scaleDigits; i++ {
ns *= 10
}
return
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Go 语言中通过 ==return== 关键字向外输出返回值。
函数多返回值,Go 语言中函数支持多返回值,函数如果有多个返回值时必须用 ==()== 将所有返回值包裹起来。
package main
import "fmt"
func main() {
plus, sub := calc(4, 5)
fmt.Println(plus) // 和为:9
fmt.Println(sub) // 差为:-1
}
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.2 函数是“一等公民”
函数在 Go 语言中属于“一等公民(First-Class Citizen)”。
wiki 发明人、C2 站点作者沃德·坎宁安 (Ward Cunningham)对“一等公民”的解释:
如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。
基于这个解释,来看 Go 语言的函数作为“一等公民”,表现出的各种行为特征。
# 1.2.1 Go 函数可以存储在变量中
按照沃德·坎宁安对一等公民的解释,身为一等公民的语法元素是可以存储在变量中的。
var (
myFprintf = func(w io.Writer, format string, a ...interface{}) (int, error) {
return fmt.Fprintf(w, format, a...)
}
)
func main() {
fmt.Printf("%T\n", myFprintf) // func(io.Writer, string, ...interface {}) (int, error)
myFprintf(os.Stdout, "%s\n", "Hello, Go") // 输出Hello,Go
}
2
3
4
5
6
7
8
9
10
把新创建的一个匿名函数赋值给了一个名为 myFprintf 的变量,通过这个变量,我们便可以调用刚刚定义的匿名函数。然后我们再通过 Printf 输出 myFprintf 变量的类型,也会发现结果与预期的函数类型是相符的。
# 1.2.2 支持在函数内创建并通过返回值返回
Go 函数不仅可以在函数外创建,还可以在函数内创建。而且由于函数可以存储在变量中,所以函数也可以在创建后,作为函数返回值返回。
func setup(task string) func() {
println("do some setup stuff for", task)
return func() {
println("do some teardown stuff for", task)
}
}
func main() {
teardown := setup("demo")
defer teardown()
println("do some bussiness stuff")
}
2
3
4
5
6
7
8
9
10
11
12
模拟了执行一些重要逻辑之前的上下文建立(setup),以及之后的上下文拆除(teardown)。在一些单元测试的代码中,我们也经常会在执行某些用例之前,建立此次执行的上下文(setup),并在这些用例执行后拆除上下文(teardown),避免这次执行对后续用例执行的干扰。
在这个例子中,我们在 setup 函数中创建了这次执行的上下文拆除函数,并通过返回值的形式,将这个拆除函数返回给了 setup 函数的调用者。setup 函数的调用者,在执行完对应这次执行上下文的重要逻辑后,再调用 setup 函数返回的拆除函数,就可以完成对上下文的拆除了。
从这段代码中也可以看到,setup 函数中创建的拆除函数也是一个匿名函数,但和前面的匿名函数有一个不同,在于这个匿名函数使用了定义它的函数 setup 的局部变量 task,这样的匿名函数在 Go 中也被称为闭包(Closure)。
闭包本质上就是一个匿名函数或叫函数字面值,它们可以引用它的包裹函数,也就是创建它们的函数中定义的变量。然后,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。显然,Go 语言的闭包特性也是建立在“函数是一等公民”特性的基础上的。
# 1.2.3 作为参数传入函数
在日常编码时经常使用、标准库 time 包的 AfterFunc 函数,就是一个接受函数类型参数的典型例子。
// 通过 AfterFunc 函数设置了一个 2 秒的定时器,并传入了时间到了后要执行的函数。这里传入的就是一个匿名函数:
time.AfterFunc(time.Second*2, func() { println("timer fired") })
2
3
# 1.2.4 拥有自己的类型
每个函数都和整型值、字符串值等一等公民一样,拥有自己的类型,也就是函数类型。甚至可以基于函数类型来自定义类型,就像基于整型、字符串类型等类型来自定义类型一样。
// HandlerFunc、visitFunc 就是 Go 标准库中,基于函数类型进行自定义的类型:
// $GOROOT/src/net/http/server.go
type HandlerFunc func(ResponseWriter, *Request)
// $GOROOT/src/sort/genzfunc.go
type visitFunc func(ast.Node) ast.Visitor
2
3
4
5
6
7
# 1.3 函数“一等公民”特性的高效运用
# 1.3.1 函数类型的妙用
函数也可以被显式转型,这样的转型在特定的领域具有奇妙的作用.
// 最常见的、用 Go 构建 Web Server
func greeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome, Gopher!\n")
}
func main() {
http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}
2
3
4
5
6
7
8
工作机制就是当用户通过浏览器,或者类似 curl 这样的命令行工具,访问 Web server 的 8080 端口时,会收到“Welcome, Gopher!”这样的文字应答。
// http 包的函数 ListenAndServe 的源码: // $GOROOT/src/net/http/server.go func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }
1
2
3
4
5
6
7函数 ListenAndServe 会把来自客户端的 http 请求,交给它的第二个参数 handler 处理,而 handler 参数的类型 http.Handler,是一个自定义的接口类型,它的源码是:
// $GOROOT/src/net/http/server.go type Handler interface { ServeHTTP(ResponseWriter, *Request) }
1
2
3
4这个接口只有一个方法 ServeHTTP,他的函数类型是func(http.ResponseWriter, *http.Request)。这和自己定义的 http 请求处理函数 greeting 的类型是一致的,但是我们没法直接将 greeting 作为参数值传入,否则编译器会报错:
func(http.ResponseWriter, *http.Request) does not implement http.Handler (missing ServeHTTP method)
1编译器提示,函数 greeting 还没有实现接口 Handler 的方法,无法将它赋值给 Handler 类型的参数。现在再回过头来看下代码,代码中我们也没有直接将 greeting 传给 ListenAndServe 函数,而是将http.HandlerFunc(greeting)作为参数传给了 ListenAndServe。接着看一下 http.HandlerFunc 的源码:
// $GOROOT/src/net/http/server.go type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
1
2
3
4
5
6
7
8通过它的源码,HandlerFunc 是一个基于函数类型定义的新类型,它的底层类型为函数类型func(ResponseWriter, *Request)。这个类型有一个方法 ServeHTTP,然后实现了 Handler 接口。也就是说http.HandlerFunc(greeting)这句代码的真正含义,是将函数 greeting 显式转换为 HandlerFunc 类型,后者实现了 Handler 接口,满足 ListenAndServe 函数第二个参数的要求。另外,之所以http.HandlerFunc(greeting)这段代码可以通过编译器检查,正是因为 HandlerFunc 的底层类型是func(ResponseWriter, *Request),与 greeting 函数的类型是一致的,这和下面整型变量的显式转型原理也是一样的:
type MyInt int var x int = 5 y := MyInt(x) // MyInt的底层类型为int,类比HandlerFunc的底层类型为func(ResponseWriter, *Request)
1
2
3
# 1.3.2 利用闭包简化函数调用
Go 闭包是在函数内部创建的匿名函数,这个匿名函数可以访问创建它的函数的参数与局部变量。
// times 函数用来进行两个整型数的乘法。
func times(x, y int) int {
return x * y
}
// 使用 times 函数的时候需要传入两个实参
times(2, 5) // 计算2 x 5
times(3, 5) // 计算3 x 5
times(4, 5) // 计算4 x 5
2
3
4
5
6
7
8
9
有些场景存在一些高频使用的乘数,这个时候就没必要每次都传入这样的高频乘数了。
// 柯里化
func partialTimes(x int) func(int) int {
return func(y int) int {
return times(x, y)
}
}
2
3
4
5
6
partialTimes 的返回值是一个接受单一参数的函数,这个由 partialTimes 函数生成的匿名函数,使用了 partialTimes 函数的参数 x。按照前面的定义,这个匿名函数就是一个闭包。partialTimes 实质上就是用来生成以 x 为固定乘数的、接受另外一个乘数作为参数的、闭包函数的函数。当程序调用 partialTimes(2) 时,partialTimes 实际上返回了一个调用 times(2,y) 的函数,这个过程的逻辑类似于下面代码:
timesTwo = func(y int) int { return times(2, y) }
1
2
3接下来使用 partialTimes,分别生成以 2、3、4 为固定高频乘数的乘法函数,以及这些生成的乘法函数的使用方法:
func main() { timesTwo := partialTimes(2) // 以高频乘数2为固定乘数的乘法函数 timesThree := partialTimes(3) // 以高频乘数3为固定乘数的乘法函数 timesFour := partialTimes(4) // 以高频乘数4为固定乘数的乘法函数 fmt.Println(timesTwo(5)) // 10,等价于times(2, 5) fmt.Println(timesTwo(6)) // 12,等价于times(2, 6) fmt.Println(timesThree(5)) // 15,等价于times(3, 5) fmt.Println(timesThree(6)) // 18,等价于times(3, 6) fmt.Println(timesFour(5)) // 20,等价于times(4, 5) fmt.Println(timesFour(6)) // 24,等价于times(4, 6) }
1
2
3
4
5
6
7
8
9
10
11