小艾的自留地

Stay foolish, Stay hungry

在 Go 语言中,函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块,占据着重要的位置。

函数

在 Go 中,定义一个函数最常用的方式就是使用函数声明,以Go 标准库 fmt 包提供的 Fprintf 函数为例,看一下一个普通 Go 函数的声明长什么样:

可以看到一个函数是由五部分组成,下面一一介绍。

func 关键字

Go 函数声明必须以 func 关键字开始。

函数名

函数名是指代函数定义的标识符,函数声明后,可以通过函数名这个标识来使用这个函数。

在同一个 Go 包中,函数名是唯一的,如果重复定义则会编译失败。

同样的,函数的定义,也遵守 Go 标识符的导出规则,也就是,如果函数名的首字母是大写表示该函数可以在包外使用,反之,小写的就只在包内使用。

参数列表

参数列表中声明了将要在函数体中使用的各个参数。

在其他编程语言中,函数参数通常是允许定义默认值的,但是在 Go 语言中,函数参数不支持默认值。

参数名在前,参数类型在后,这和变量声明中变量名与类型的排列方式是一致的。

另外,Go 函数也是支持变长参数,后面详细介绍。

返回值列表

返回值承载了函数执行后要返回给调用者的结果。

如果不仅声明了返回值的类型,还声明了返回值的名称,那么这种返回值被称为具名返回值

函数体

函数体是函数的具体实现。

函数体内并不是一定需要有内容,也就是说,定义一个这样的函数也是合法的:

1
func foo(arr [5]int) {}

函数声明中的 func 关键字、参数列表和返回值列表共同构成了函数类型
参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素。

如果两个函数类型的函数签名是相同的,即便参数列表中的参数名,以及返回值列表中的返回值变量名都是不同的,那么这两个函数类型也是相同类型,比如下面两个函数类型:

1
2
3
4
5
6
func (a int, b string) (results []string, err error)
func (c int, d string) (sl []string, err error)

// 将两个函数类型的参数名与返回值变量名省略
// func (int, string) ([]string, error)
// 所以它们是相同的函数类型

每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例,就像 var a int = 13 这个变量声明语句中 a 是 int 类型的一个实例一样。

在前面的笔记中,使用复合类型字面值对结构体类型变量进行显式初始化的内容,在形式上,和上面这种使用变量声明来声明函数变量的形式很像。

把这两种形式都以最简化的样子表现出来:

1
2
s := T{}      // 使用复合类型字面值对结构体类型T的变量进行显式初始化
f := func(){} // 使用变量声明形式的函数声明

这里,T{}被称为复合类型字面值,处于同样位置的 func(){} 叫“函数字面值(Function Literal)”。

可以看到,函数字面值由函数类型与函数体组成,它特别像一个没有函数名的函数声明,因此也叫它匿名函数。匿名函数在 Go 中用途很广,稍后我们会细讲。

Go 函数与函数声明

参数列表

Go 语言中,函数参数传递采用是值传递的方式,所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中。

对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。

但是像 string、切片、map 这些属于引用类型,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的仅仅是数据内容的“描述符”,不包括数据内容本身,因此这些类型传递的开销是固定的,与数据内容大小无关,这种方式被成为浅拷贝

不过函数参数的传递也有两个例外,当函数的形参为接口类型,或者形参是变长参数时,简单的值传递就不能满足要求了。

对于类型为接口类型的形参,Go 编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go 编译器会将零个或多个实参按一
定形式转换为对应的变长形参

下面通过一段示例代码来说明变长参数的形参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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]
// 调用 myAppend 函数,传递实参 4,5,6
sl = myAppend(sl, 4, 5, 6)
fmt.Println(sl) // [1 2 3 4 5 6]
}

重点看一下代码中的 myAppend 函数,在 append 函数的基础上进行了扩展,支持变长参数。

通过打印变长参数的类型,可以看到类型是 []int,足以说明变长参数实际上是通过切片来实现的

返回值

和其他主流静态类型语言,Go 函数支持多返回值,多返回值可以让函数将更多结果信息返回给它的调用者。

函数返回值列表从形式上看主要有三种:

1
2
3
func foo()                       // 无返回值
func foo() error // 仅有一个返回值
func foo() (int, string, error) // 有2或2个以上返回值

前面提到过,如果一个返回值既有类型,也有名称,那么这类返回值就被成为具名返回值

Go 标准库以及大多数项目代码中的函数,都选择了使用普通的非具名返回值形式。

所以多数情况下,只需声明返回值的类型即可,无需使用具名返回值形式。

一等公民

函数作为“一等公民”,在 Go 语言中,占据着重要的地位。要知道,并不是在所有编程语言中函数都是“一等公民”。

那么,什么是编程语言的“一等公民”呢?

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。——wikipedia

基于这个解释,我们来看看 Go 语言的函数作为“一等公民”,表现出的各种行为特征。

函数可以存储在变量中

关于这一点,其实前面已经验证过了,下面用一个例子再一次理解一下:

1
2
3
4
5
6
7
8
9
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) // myFprintft 的类型 func(io.Writer, string, ...interface {}) (int, error)
myFprintf(os.Stdout, "%s\n", "Hello, Go") // 调用 myFprintft 输出Hello,Go
}

在这个例子中,创建一个匿名函数赋值给 myFprintf 变量,通过打印变量类型和调用函数,可以看到预期结果是一致的。

将函数作为返回值返回

Go 函数不仅可以在函数外创建,还可以在函数内创建。而且由于函数可以存储在变量中,所以函数也可以在创建后,作为函数返回值返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 注意返回值的类型是 函数类型
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")
}

// do some setup stuff for demo
// do some bussiness stuff
// do some teardown stuff for demo

和前面看到的匿名函数不同的是,这个匿名函数使用了定义它的函数 setup 的局部变量 task,这样的匿名函数在 Go 中也被称为闭包(Closure)

闭包的本质就是匿名函数,不过可以引用它的包裹函数,也就是创建它们的函数中定义的变量,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。

作为参数传入函数

函数除了可以存储在变量中、作为返回值返回、还可以作为参数传入。

1
time.AfterFunc(time.Second*2, func() { println("timer fired") })

拥有自己的类型

作为一等公民的整型值拥有自己的类型 int,同样的,作为一等公民的函数,也拥有自己的类型,也就是前面提到的函数类型(由 func 关键字、参数列表和返回值列表共同构成)。

可以基于函数类型来自定义类型,就像基于整型、字符串类型等类型来自定义类型一样。下面代码中的 HandlerFunc、visitFunc 就是 Go 标准库中,基于函数类型进行自定义的类型:

1
2
3
4
5
6
7
// $GOROOT/src/net/http/server.go
// 函数类型是 func(ResponseWriter, *Request)
type HandlerFunc func(ResponseWriter, *Request)

// $GOROOT/src/sort/genzfunc.go
// 函数类型是 func(ast.Node) ast.Visitor
type visitFunc func(ast.Node) ast.Visitor

总结

  • 函数支持多返回值
  • 函数参数不运行定义默认值
  • 对于像整型、数组、结构体这类类型是值传递,对于string、切片、map 这些类型则是引用传递
  • 函数类型由 func 关键字 + 参数列表 + 返回值列表组成; 函数签名由参数列表 + 返回值列表组成; 如果两个函数签名相同,则函数类型相同
  • Go 语言的函数是一等公民,具备一切作为“一等公民”的行为特征

评论