Go chapter3
函数
函数声明
我们通过阅读标准库的包中声明的函数来学习如何声明函数。
例如 rand 包中的 Intn 函数。
rand 包中的 Intn 函数的声明如下:
1 |
|
下面是一个使用 Intn 函数的例子:
1 |
|
下图标识了 Intn 函数声明的各个组成部分以及调用该函数的语法。关键字func
告知 go 这是一个函数声明,之后跟着的是首字母大写的函数名 Intn。
在 go 中,以大写字母开头的函数、变量以及其他标识符都会被导出并对其他包可用,反之则不然。
Intn 函数接受单个形式参数(简称形参)作为输入,并且形参的两边用括号包围。形参的声明跟变量的声明一样,都是变量名在前,变量类型在后:
1 |
|
在调用 Intn 函数时,整数 10 将作为单个的实际参数(简称实参)被传递,并且实参的两边也需要用括号包围。产生如单个实参正好符合 Intn 函数只有单个形参的预期,但如果我们以无实参方式调用函数,或者实参的类型不为 int
,那么 go 编译器将报告一个错误。
形参相当于占位符,实参就是占位符实际的内容。
Init 函数在执行之后将返回一个 int
类型的伪随机整数作为结果。这个结果会被回传至调用者,然后用于初始化新声明的变量 num。
虽然 Intn 函数只接受单个形参,但函数也可以通过以逗号分隔的列表来接受多个形参。 time 包中的 Unix 函数就接受两个 int64 形参,它们分别代表 1970年1月1日 以来经过的秒数和纳秒数。这个函数的声明是这样子的:
1 |
|
Unix 函数将返回一个 Time 类型的结果。
在声明函数的时候,如果多个形参用于相同的类型,那么我们只需要把这个类型写出来一次即可:
1 |
|
go 函数不仅能够接受多个形参,它还能够返回多个值。
前面 strconv 包中的Atoi函数展示过这一特性——这个函数会尝试将给定的字符串转换为数值,然后返回两个值。
1 |
|
strconv 包的文档记录了Atoi函数的声明方式:
1 |
|
跟函数的形参一样,函数的多个返回值也需要用括号包围,其中每个返回值的名字在前而类型在后。不过在声明函数的时候也可以把返回值的名字去掉,只保留类型:
1 |
|
注意:error类型是内置的错误处理类型
我们一直使用的 Println 函数是一个更为独特的函数,因为它不仅可用接受一个、两个甚至多个参数,而且这些形参的类型还可以各不相同,其中就包括整数和字符串:
1 |
|
Println 函数在文档中的声明看上去可能会显得有些古怪,因为它使用了我们尚未了解的特性:
1 |
|
我们可以向Println
函数传递可变数量的实参,形参中的省略号...
表明了这一点。Println
用专门的术语来讲就是一个可变参数函数,而其中的形参 a 则代表传递给该函数的所有实参。
另外需要注意的是,形参 a 的类型为interface{}
,也就是所谓的空接口类型。我们现在只需要知道这种特殊类型可以让Println
函数接受int
、float64
、string
、time.Time
以及其他任何类型的值作为参数而不会引发 go 编译器报错即可。
通过写出...interface{}
来组合使用可变参数函数和空接口,Println
函数将能够接受任意多个任意类型的实参,这样它就可以完美地打印出我们传递给它的任何东西了。
编写函数
定义一个将开氏度转换至摄氏度的函数。
1 |
|
代码声明定义了一个函数。除此之外,函数还会通过关键字return
,将一个float64
类型的值返回给调用者。
另外需要注意的是,在同一个包中声明的函数在调用彼此时不需要加上包名作为前缀。
隔离是一件好事:
代码清单中的函数与其他函数没有任何关系,它的唯一输入就是它接受的形参,而它的唯一输出就是它返回的结果。这个函数不会修改外部状态,也就是俗称的无副作用函数,这种函数最容易理解、测试和复用。
方法
声明新类型
如下代码所示,关键字 type
可以通过一个名字和一个底层类型来声明新的类型。
1 |
|
因为数字字面量 20 跟其他数字字面量一样都是无类型常量,所以无论是int
类型、flaot64
类型或者其他任何数字类型的变量,都可以将这个字面量用作值。
1 |
|
虽然 celsius 类型跟它的底层类型 float64 具有相同的行为,但因为 celsius 是一种独特的类型而非类型别名,所以尝试把 celsius 和 float64 放在一起将引发类型不匹配错误。
通过自定义新类型能够极大地提高代码的可读性和可靠性。
1 |
|
引入自定义类型
在声明新类型之后,你就可以像使用int
、float64
、string
等预声明 go 类型那样,将新类型应用到包括函数形参和返回值在内的各种地方,代码清单展示的就是一个例子:
1 |
|
kelvinToCelsius 函数只接受 Kelvin 类型的实参,这有助于避免不合理的错误。它不会接受类型错误的实参,如 fahrenheit、kilometers 甚至是 flaot64。不过因为go 是一门实用的语言,所以它仍然接受字面量或者无类型常量作为实参,这样你就可以编写 kelvinToCelsius(294) 而不是 kelvinToCelsius(kelvin(294))了。
另外需要注意的是,因为 kelvinToCelsius 接受的是 kelvin类型的实参,但是返回的是 celsius 类型的值,所以它在返回计算结果之前必须先将返回值的类型转换为 celsius 类型。
通过方法给类型添加行为
传统的面向对象语言总是说方法属于类,但 go 不是这样做的:它提供了方法,但是并没有提供类和对象。
使用 kelvinToCelsius、celssiusToFahrenheit、fahrenheitToCelsius、celsiuToKelvin 这样的函数虽然也能够完成温度转换工作,但是通过声明相应的方法并把它们放置属于自己的地方,能够让温度转换代码变得更加简洁明了。
我们可以将方法与同一个包中声明的任何类型相关联,但是不能为int
和float64
之类的预声明类型关联方法。其中,声明类型的方法在前面已经介绍过了:
1 |
|
kelvin 类型跟它的底层类型float64
具有相同的行为,我们可以像处理浮点数那样,对 kelvin 类型的值执行加法运算、乘法运算以及其他操作。此外声明一个将 kelvin转换为 celsius 的方法就跟声明一个具有同等作用的函数一样简单——它们都以关键字 func
开头,并且函数体跟方法体完全一样:
1 |
|
celsius 方法虽然没有接受任何形参,但它的名字前面却有一个类似形参的接收者。每个方法和函数都可以接受多个形参,但一个方法必须并且只能有一个接收者。在 clesius 方法体中,接收者的行为就跟其他形参一样。
除声明语法有些许不同之外,调用方法的语法与调用函数的语法也不一样:
1 |
|
跟调用其他包中的函数一样,调用方法也需要用到点记号。以上面的代码为例,在调用方法的时候,程序首先需要给出正确类型的变量,接着是一个点号,最后才是被调用方法的名字。
在同一个包里面,如果一个名字已经被函数占用了,那么这个包就无法再定义同名的类型,因此在使用函数的情况下,我们将无法使用 celsius 函数返回 celsius 类型的值。然而,如果我们使用的是方法,那么每种温度类型都可以具有自己的 celsius方法,就像如下代码一样。
1 |
|
通过让每种温度类型都具有相应的 celsius 方法以转换为摄氏度,我们可以创造出一种完美的对称。
一等函数
将函数赋值给变量
1 |
|
在这段代码中,变量 sensor 的值是函数本身,而不是调用函数获得的结果。正如之前所述,无论是调用函数还是方法,都需要像 fakeSensor() 这样用到圆括号,但这次的程序在赋值的时候并没有这样做。
注意:代码清单之所以能够将 realSensor 函数重新赋值给 sensor 变量,是因为 realSensor 与 fakeSensor 具有相同的函数签名。换句话说,这两个函数具有相同数量和相同类型的形参以及返回值。
现在,无论赋值给 sensor 变量的是 fakeSensor 函数还是 realSensor 函数,程序都可以通过调用 sensor() 来实际地调用它。
sensor 变量的类型是函数,具体来说就是一个不接受任何形参并且只返回一个 kelvin 值的函数。在不使用类型推断的情况下,我们需要为这个变量设置以下声明:
1 |
|
将函数传递给其他函数
因为变量既可以指向函数,又可以作为参数传递给函数,所以我们同样可以在 go 里面将函数传递给其他函数。
为了记录每秒的温度数据,
1 |
|
这种传递函数的能力是一种非常强大的代码拆分手段。如果 go 不支持一等函数,那么我们就必须写出两个代码相差无几的函数了。
measureTemperature 函数接受两个形参,其中第二个形参的类型为 func() kelvin
,这一声明与相同类型的变量声明非常相似:
1 |
|
声明函数类型
为函数声明新的类型有助于精简和明确调用者的代码。
在前面的几章中,我们就尝试了使用 kelvin 类型而不是底层表示来代表温度单位,同样的方法也可以应用于被传递的函数:
1 |
|
跟不接受任何形参并且只返回一个 kelvin 值的函数这一模糊的概念相比,现在代码可以通过 sensor 类型来确定地声明一个传感器函数。通过 sensor 类型还能够有效地精简代码,使函数声明
1 |
|
能够改写为
1 |
|
在这个简单的例子中,使用 sensor 类型看上去作用不大,毕竟人们在阅读代码的时候还是得看一眼 sensor 类型的声明才能够知道代码的具体行为。但如果 sensor 在多个地方都出现过,或者函数类型需要接受多个形参,那么使用函数类型将能够有效地减少混乱。
闭包和匿名类型
匿名函数也就是没有名字的函数,在 go 中也被称为函数字面量。跟普通函数不一样的是,因为函数字面量需要保留外部作用域的变量引用,所以函数字面量都是闭包的。
1 |
|
我们甚至还可以将声明匿名函数和调用匿名函数整合到一个步骤里面执行,就像代码清单所示的那样。
1 |
|
匿名函数适用于各种需要动态创建函数的情景,从函数里面返回另一个函数就是其中之一。虽然函数也可以返回已存在的具名函数,达能声明并返回全新的匿名函数无疑会更为有用。
1 |
|
值得一提的是,代码清单中的匿名函数利用了闭包特性,它引用了被 calibrate 函数用作形参的 s 变量和 offset 变量。尽管 calibrate 函数已经返回了,但是被闭包捕获的变量将继续存在,因此调用 sensor 仍然能够访问这两个变量。术语闭包就是由于匿名函数封闭并包围作用域中 的变量而得名的。
另外需要注意的是,因为闭包保留的是周围变量的引用而不是副本值,所以修改被闭包捕获的变量可能会导致调用匿名函数的结果发生变化。
1 |
|
请务必牢记这一点,特别是当你在for循环中使用闭包的时候。
后言
参考书籍:Go语言趣学指南
参考课程:Go语言编程快速入门(Golang)