在 Go 语言中,函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块,占据着重要的位置。
函数
在 Go 中,定义一个函数最常用的方式就是使用函数声明,以Go 标准库 fmt 包提供的 Fprintf 函数为例,看一下一个普通 Go 函数的声明长什么样:
可以看到一个函数是由五部分组成,下面一一介绍。
func 关键字
Go 函数声明必须以 func 关键字开始。
函数名
函数名是指代函数定义的标识符,函数声明后,可以通过函数名这个标识来使用这个函数。
在同一个 Go 包中,函数名是唯一的,如果重复定义则会编译失败。
同样的,函数的定义,也遵守 Go 标识符的导出规则,也就是,如果函数名的首字母是大写表示该函数可以在包外使用,反之,小写的就只在包内使用。
参数列表
参数列表中声明了将要在函数体中使用的各个参数。
在其他编程语言中,函数参数通常是允许定义默认值的,但是在 Go 语言中,函数参数不支持默认值。
参数名在前,参数类型在后,这和变量声明中变量名与类型的排列方式是一致的。
另外,Go 函数也是支持变长参数,后面详细介绍。
返回值列表
返回值承载了函数执行后要返回给调用者的结果。
如果不仅声明了返回值的类型,还声明了返回值的名称,那么这种返回值被称为具名返回值。
函数体
函数体是函数的具体实现。
函数体内并不是一定需要有内容,也就是说,定义一个这样的函数也是合法的:
1 | func foo(arr [5]int) {} |
函数声明中的 func 关键字、参数列表和返回值列表共同构成了函数类型。
而参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素。
如果两个函数类型的函数签名是相同的,即便参数列表中的参数名,以及返回值列表中的返回值变量名都是不同的,那么这两个函数类型也是相同类型,比如下面两个函数类型:
1 | func (a int, b string) (results []string, err error) |
每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例,就像 var a int = 13
这个变量声明语句中 a 是 int 类型的一个实例一样。
在前面的笔记中,使用复合类型字面值对结构体类型变量进行显式初始化的内容,在形式上,和上面这种使用变量声明来声明函数变量的形式很像。
把这两种形式都以最简化的样子表现出来:
1 | s := T{} // 使用复合类型字面值对结构体类型T的变量进行显式初始化 |
这里,T{}
被称为复合类型字面值,处于同样位置的 func(){}
叫“函数字面值(Function Literal)”。
可以看到,函数字面值由函数类型与函数体组成,它特别像一个没有函数名的函数声明,因此也叫它匿名函数。匿名函数在 Go 中用途很广,稍后我们会细讲。
Go 函数与函数声明
参数列表
Go 语言中,函数参数传递采用是值传递的方式,所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中。
对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。
但是像 string、切片、map 这些属于引用类型,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的仅仅是数据内容的“描述符”,不包括数据内容本身,因此这些类型传递的开销是固定的,与数据内容大小无关,这种方式被成为浅拷贝。
不过函数参数的传递也有两个例外,当函数的形参为接口类型,或者形参是变长参数时,简单的值传递就不能满足要求了。
对于类型为接口类型的形参,Go 编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go 编译器会将零个或多个实参按一
定形式转换为对应的变长形参。
下面通过一段示例代码来说明变长参数的形参:
1 | func myAppend(sl []int, elems ...int) []int { |
重点看一下代码中的 myAppend 函数,在 append 函数的基础上进行了扩展,支持变长参数。
通过打印变长参数的类型,可以看到类型是 []int
,足以说明变长参数实际上是通过切片来实现的。
返回值
和其他主流静态类型语言,Go 函数支持多返回值,多返回值可以让函数将更多结果信息返回给它的调用者。
函数返回值列表从形式上看主要有三种:
1 | func foo() // 无返回值 |
前面提到过,如果一个返回值既有类型,也有名称,那么这类返回值就被成为具名返回值。
Go 标准库以及大多数项目代码中的函数,都选择了使用普通的非具名返回值形式。
所以多数情况下,只需声明返回值的类型即可,无需使用具名返回值形式。
一等公民
函数作为“一等公民”,在 Go 语言中,占据着重要的地位。要知道,并不是在所有编程语言中函数都是“一等公民”。
那么,什么是编程语言的“一等公民”呢?
如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。——wikipedia
基于这个解释,我们来看看 Go 语言的函数作为“一等公民”,表现出的各种行为特征。
函数可以存储在变量中
关于这一点,其实前面已经验证过了,下面用一个例子再一次理解一下:
1 | var ( |
在这个例子中,创建一个匿名函数赋值给 myFprintf 变量,通过打印变量类型和调用函数,可以看到预期结果是一致的。
将函数作为返回值返回
Go 函数不仅可以在函数外创建,还可以在函数内创建。而且由于函数可以存储在变量中,所以函数也可以在创建后,作为函数返回值返回:
1 | // 注意返回值的类型是 函数类型 |
和前面看到的匿名函数不同的是,这个匿名函数使用了定义它的函数 setup 的局部变量 task,这样的匿名函数在 Go 中也被称为闭包(Closure)。
闭包的本质就是匿名函数,不过可以引用它的包裹函数,也就是创建它们的函数中定义的变量,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。
作为参数传入函数
函数除了可以存储在变量中、作为返回值返回、还可以作为参数传入。
1 | time.AfterFunc(time.Second*2, func() { println("timer fired") }) |
拥有自己的类型
作为一等公民的整型值拥有自己的类型 int,同样的,作为一等公民的函数,也拥有自己的类型,也就是前面提到的函数类型(由 func 关键字、参数列表和返回值列表共同构成)。
可以基于函数类型来自定义类型,就像基于整型、字符串类型等类型来自定义类型一样。下面代码中的 HandlerFunc、visitFunc 就是 Go 标准库中,基于函数类型进行自定义的类型:
1 | // $GOROOT/src/net/http/server.go |
总结
- 函数支持多返回值
- 函数参数不运行定义默认值
- 对于像整型、数组、结构体这类类型是值传递,对于string、切片、map 这些类型则是引用传递
- 函数类型由 func 关键字 + 参数列表 + 返回值列表组成; 函数签名由参数列表 + 返回值列表组成; 如果两个函数签名相同,则函数类型相同
- Go 语言的函数是一等公民,具备一切作为“一等公民”的行为特征