Go语言基础之函数
Go语言基础之函数
在Go语言中,Go的程序就是一组函数的组合,Go的函数是基于一组特定的输入,经过函数体的一定逻辑处理,输出一组结果。
Go函数申明
下面我门用一个Go函数申明代码块来介绍下Go函数的组成:
1 | // Spaces are always added between operands and a newline is appended. |
上面的代码片段截至于Go的fmt包下的print.go文件中,是一个比较典型的函数申明语法块。
我们分析上面的申明代码块,可以得出Go函数的申明组成有:
- 关键字func。Go函数的声明自func开始;
- 函数名Fprintln。函数名是指代函数定义的标识符。在Go里,函数的标识符是唯一不重复的,大写字母开始的标识符函数表示可以导出包外使用,而小写字母的标识符函数表示只能在当前包内使用;
- 参数列表(w io.Writer, a …interface{})。参数列表声明了我们将要在函数中的使用的变量名和类型,参数列表紧跟在函数名后;
- 这里要注意,声明的参数有普通参数和变长参数,变长参数要在普通参数之后,在类型前面加
...
表示该参数是可变长的;
- 这里要注意,声明的参数有普通参数和变长参数,变长参数要在普通参数之后,在类型前面加
- 返回值列表(n int, err error)。返回值承载了函数执行后要返回给调用者的结果,返回值列表声明了这些返回值的类型,返回值列表的位置紧接在参数列表后面,两者之间用一个空格隔开;
- 函数体。函数体放在一对大括号内,是函数的具体实现都放在这里。不过,函数声明中的函数体是可选的。如果没有函数体,说明这个函数可能是在 Go 语言之外实现的,这里不做过多的探索,留待后面进阶深入。
变量声明和函数声明
下面代码片段以普通变量声明的形式来实现函数声明同等的效果:
1 | var Fprintln = func(w io.Writer, a ...interface{}) (n int, err error) { |
对上面的代码片段进行分析,可以知道,函数声明中的函数名就是变量名,函数声明中的 func 关键字、参数列表和返回值列表共同构成了函数类型。而参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素。因此,函数类型也可以看成是由 func 关键字与函数签名组合而成的。
即当两个函数的参数列表和返回值列表组合相同,则代表两个函数是同一个函数。
综上,每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例,就像var go string= “Go”这个变量声明语句中 go 是 string类型的一个实例一样。
函数字面值
1 | f := func() () {} |
上面这种变量声明形式声明的函数被称为”函数字面值”(Function Literal),这种形式由于像一个没有函数名的函数,因此在Go语言中又被称之为”匿名函数”。可以看到,函数字面值由函数类型与函数体组成。
函数的参数
函数参数列表中的参数,是函数声明的、用于函数体实现的局部变量。函数分为声明与使用两个阶段,所以在不同阶段,参数的称谓也有不同。
形式参数与实际参数
在函数声明阶段,我们把参数列表中的参数叫做形式参数(Parameter,简称形参),在函数体中,我们使用的都是形参;而在函数实际调用时传入的参数被称为实际参数(Argument,简称实参)。
1 | func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { // w, a: 形式参数 |
当我们实际调用函数的时候,实参会传递给函数,并和形式参数逐一绑定,编译器会根据各个形参的类型与数量,来检查传入的实参的类型与数量是否匹配。只有匹配,程序才能继续执行函数调用,否则编译器就会报错。
在Go语言中,函数的参数传递采用的是值传递的形式,但是对于string、map、切片这类的数据结构就不是了。它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为“浅拷贝”。
所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中。对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。
不过函数参数的传递也有两个例外,当函数的形参为接口类型,或者形参是变长参数时,简单的值传递就不能满足要求了,这时 Go 编译器会介入:对于类型为接口类型的形参,Go 编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go 编译器会将零个或多个实参按一定形式转换为对应的变长形参。(通过切牌你来实现)
深拷贝与浅拷贝
深度拷贝对应的就是函数的值传递,在传递过程中会拷贝数据的实际内存值。而浅拷贝就是只拷贝数据的描述符,并未拷贝数据的实际内存值。
函数的返回值
函数返回值列表从形式上看主要有三种:
1 | func foo() // 无返回值 |
带有名字的返回值被称为具名返回值(Named Return Value)。这种具名返回值变量可以像函数体中声明的局部变量一样在函数体内使用。
具名返回值(Named Return Value)和普通返回值
Go 标准库以及大多数项目代码中的函数,都选择了使用普通的非具名返回值形式。但在一些特定场景下,具名返回值也会得到应用。比如,当函数使用 defer,而且还在 defer 函数中修改外部函数返回值时,具名返回值可以让代码显得更优雅清晰。关于 defer 的使用,我们会在后面课程中还会细讲。
再比如,当函数的返回值个数较多时,每次显式使用 return 语句时都会接一长串返回值,这时,我们用具名返回值可以让函数实现的可读性更好一些,比如下面 Go 标准库 time 包中的 parseNanoseconds 函数就是这样:
1 | // $GOROOT/src/time/format.go |
函数”一等公民”特性
wiki 发明人、C2 站点作者沃德·坎宁安 (Ward Cunningham)对“一等公民”的解释:
如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。
特性一:Go函数可存储在变量中
1 | var ( |
特性二:支持在函数内创建并返回
1 | func test(task string) func(string) { |
1 | func test(task string) func(string) { |
上面代码块中返回的的匿名函数在 Go 中也被称为闭包(Closure)。
闭包本质上就是一个匿名函数或叫函数字面值,它们可以引用它的包裹函数,也就是创建它们的函数中定义的变量。然后,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。
特性三:作为参数传入
1 | func testP(test1 func(string)) { |
特性四:拥有自己的数据类型
每个函数声明定义的函数仅仅是对应的函数类型的一个实例,就像var go string= “Go”这个变量声明语句中 go 是 string类型的一个实例一样。换句话说,每个函数都和整型值、字符串值等一等公民一样,拥有自己的类型,也就是我们讲过的函数类型。
我们甚至可以基于函数类型来自定义类型,就像基于整型、字符串类型等类型来自定义类型一样。下面代码中的 HandlerFunc、visitFunc 就是 Go 标准库中,基于函数类型进行自定义的类型:
1 | // $GOROOT/src/net/http/server.go |