通向Golang的捷径【6. 函数】

函数是 Go 语言中最基本的代码块, 它的用途广泛, 甚至可以说,Go 包含了函数语言的大多数特性, 本章将对4.2.2 节的内容进行扩展.

6.1 介绍

每个应用程序中都会一些函数, 由于 Go 代码的编译次序, 与程序包含的函数并无关系, 为了增加可读性, 最好从 main() 函数开始, 并基于一个逻辑次序, 来编写相关函数, 也就是调用次序.

函数的主要目的, 是将一个大问题分割成一组小任务 (函数), 并且相同的任务可多次调用, 以使函数代码实现重用. 一种较好的编程理念是 DRY( Don’t Repeat Yourself, 不要重复编写功能相同的代码), 这意味着功能相近的任务, 只需在程序中实现一次.

在 4.2 节中, 给出了函数的基本特性, 但本章将描述函数的更多细节和示例.

当执行到函数的最后一条语句 (在} 之前), 或是出现 return 语句时, 函数将终止执行, 同时函数可选择是否给出返回值, 而这些返回值即为函数的运算结果 (参见 6.2 节), 一个简单的 return 语句 (不包含返回值) 可用于终止 for 死循环, 或是终止一个并发协程(goroutine).

在 Go 语言中, 包含了三种函数类型:
• 使用标识符的通用函数
• 匿名 (或 lambda) 函数, 参见 6.8 节
• 方法 (method), 参见 10.6 节

函数可包含形参和返回值, 而函数的所有形参和返回值, 都必须给出类型, 这些类型将变成区分不同函数的特征.

首先需要注意函数的正确书写风格:
在这里插入图片描述
调用函数的通用格式如下:
在这里插入图片描述
pack1 包中定义了一个 Function 函数, 它包含了若干形参, 比如 arg1 等, 在调用函数时, 需将实参传递给形参(参见 6.2 节), 同时会构建形参的一个副本, 并将副本传递给调用函数, 而函数调用也可出现在另一个函数中,并且支持函数之间的层次化调用, 同时调用层次不做限制, 而函数调用会消耗堆栈的空间. 以下代码给出了一个函数中包含的函数调用.

例 6.1 greeting.go

在这里插入图片描述
在函数调用中, 可调用另一个函数, 并将该函数的返回值, 传递给前一个函数的形参, 但是后一个函数的返回值的个数和类型, 必须前一个函数的形参相一致, 比如 f1 有 3 个形参,f1(a, b, c int), 而 f2 有 3 个返回值,f2(a, b int) (int, int, int), 因此以下的 f1 调用可成立:
在这里插入图片描述
函数重载会在运行时中, 强制进行类型匹配, 从而导致性能的下降, 未给出重载功能, 意味着只会出现一个简单函数, 同时需要为简单函数, 设定一个唯一的函数名, 而函数名将作为函数的对应标记, 参见 11.12.5 节.

在 Go 语言中, 如果需要声明一个外部函数 (比如一个汇编程序), 则需要提供函数名和对应标记, 且无须给出函数体, 如下:
在这里插入图片描述
这时变量将获得函数类型的一个引用 (指针), 同时也清楚该函数类型的标识, 而一个不同标识的函数, 无法分配给该变量.

同时这类变量 (引用了某一函数, 即保存了某一函数的起始地址) 也可进行比较, 如果两变量引用了同一个函数, 或是同为 nil, 将得到相等结果. 在函数中, 不能声明其他函数 (不允许嵌套声明), 但是可以使用匿名
(anonymous) 函数, 参见 6.8 节.

到目前为止,Go 语言未引入泛型 (generic) 概念, 这意味着函数定义中, 必须提供一组变量类型, 在大多数情况下, 可使用接口 (interface), 简单解决这类问题, 尤其是在空接口和类型切换中 (参见 11.12 节), 也可选择发射(reflection) 功能, 参见 11.10, 但使用上述技巧, 会导致代码复杂度的提升, 以及性能的下降, 当要求程序的性能和代码可读性时, 创建函数必须显式提供所需的类型.

6.2 形参和返回值

函数可包含形参, 调用时传入的对应实参, 可在函数体中使用, 同时函数可返回零个或多个数值, 多返回值也是一个相较于 C,C++,Java,C# 语言的重大改进, 这更容易获取函数的运行状态, 参见 5.2 节. 在 return 语句中,将包含多个返回值, 事实上所有函数都会使用 return 或 panic, 至少返回一个数值, 参见第 13 章.

在同一个代码块中,return 语句之后的代码可能不会执行, 如果 return 语句被执行, 无论函数代码多么复杂, 都将返回.

形参通常会给出一个参数名, 但函数定义时, 可以不给出形参名, 而只提供形参的类型, 比如 func f(int, int, float64), 同时函数也可省略形参, 比如 main.main().

6.2.1 值传递和引用传递

在 Go 语言的默认情况下, 会将一个变量传递给函数的形参, 也就是值传递, 其中将创建变量的一个副本, 当函数执行时, 可修改此变量副本, 但原始变量无法修改, 如 Function(arg1).

如果需要函数 Function 可修改原始变量 arg1 的数值, 则应使用 &, 将原始变量的内存地址传递给函数 Function, 这也就是引用传递Function(&arg1), 它可将一个指针传递给函数, 当变量指针传递给函数时, 也将创建一个指针副本, 并会让指针副本指向原始变量的数值. 同时传递一个指针 (32 位或 64 位地址) 的开销远小于值传递.

引用类型, 比如切片 (第 7 章),map(第 8 章), 接口 (第 10 章), 并发通道 (第 13 章), 在默认情况下, 都将采用引用传递 (只是从 Go 代码中, 无法直接看到指针的操作).

在有些函数中, 只给出了任务实现, 但未提供返回值, 当然这种方式可以使用, 但存在缺陷, 比如控制台的信息输出, 邮件发送, 日志记录时, 函数无法提供对应的信息 (返回值). 大多数函数的返回值可以是命名数值或是未命名数值, 在以下代码中, 函数包含 3 个 int 形参 a,b,c, 并可返回一个 int 值, 注释行中给出了一个更复杂的编程方法, 因为其中采用一个本地变量:

例 6.2 simple_function.go

在这里插入图片描述
如果函数必须返回多个数值 (4-5 个) 时, 当这些数值的类型相同, 最好选择切片 ( slice, 参见第 7 章), 如果这些数值的类型不同, 则可选择一个结构指针 ( 参见第 10 章), 指针传递的成本更低, 并能对原始数值进行修改.

6.2.2 命名的返回变量

在以下代码中, 函数包含一个 int 形参, 并可返回 2 个 int 值, 而这两个返回值可实现并发赋值.

例 6.3 multiple_return.go

在这里插入图片描述
在这里插入图片描述
getX2AndX3 和 getX2AndX3_2 函数展示了命名返回变量与未命名返回变量的用法, 如果未命名的返回变量超过 1 个, 则必须封闭在圆括号 () 中, 比如 (int,int).

命名的返回变量将视为一个局部参数, 并可自动初始化为默认值, 且能实现自动返回, 因此给出命名返回变量的函数, 只需提供一个简单的 return 语句 (不包含任何返回变量), 另外, 即使函数定义中, 存在一个命名的返回变量, 也必须将其放入圆括号中, 参见 6.6 节的示例 fibonacci.go.

在上述示例中,return 或 return var(变量名) 语句都可接受, 如果使用 return var = expression(表达式) 语句,编译器将产生一个错误:
在这里插入图片描述
命名的返回变量即使忽略它的变量名, 也可完成变量值的返回, 如果命名的返回变量存在外部影子 ( 即函数之外, 存在与命名返回变量同名的变量, 这并不是一种好风格), 在 return 语句中, 必须包含命名的返回变量名.因此使用命名返回变量, 可使代码更清爽和简短, 更易于阅读.

6.2.3 空白标识符

空白标识符 _ 可用于丢弃数值, 以高效实现右侧赋值 (通常是并发赋值), 可参考以下示例 blank_identifier.go,其中函数 ThreeValues 未包含形参, 并给出了 3 个返回值,i1 和 f1 将接收 ThreeValues 返回的第一个和第三个数值.

例 6.4 blank_identifier.go

在这里插入图片描述
在这里插入图片描述
在以下代码中, 函数包含了 2 个形参, 并可给出 2 个返回值, 以计算外部传入的两个实参的最大值和最小值.

例 6.5 minmax.go

在这里插入图片描述

6.2.4 修改外部变量

传递指针给函数, 可节省内存的用量, 因为不会生成数值的副本, 当然这也存在一个副作用, 也就是在函数内部, 可会修改原始变量或对象的数值, 因此这类对象无须函数返回, 参见以下代码的 reply 变量, 它是一个指向整型的指针:

例 6.6 side_effect.go

在这里插入图片描述
上述代码只是一个示例, 用于展示函数中对外部变量的修改, 虽然在理解上存在一些困难, 但是编程者必须清楚这类副作用, 以便掌握函数的用法.

6.3 变长形参

如果函数的最后一个形参为… 类型名, 这表示函数能接受不同个数的形参 (且类型相同), 同时形参的个数也可能为 0, 因此这类函数被称为可变函数:

在这里插入图片描述
在 Greeting 调用中, 等同于使用了[ ]string{”Joe”,”Anna”,”Eileen”}. 如果实参保存在数组 arr 中, 在上述函数的调用中, 也可使用 arr… 数组.

例 6.7 varnumpar.go

在这里插入图片描述
函数的可变形参也可传递给其他函数, 比如以下的代码片段:
在这里插入图片描述
在这里插入图片描述
如果可变形参的类型并不一致, 可使用以下两种方法.

使用结构 (参见第 10 章)

定义一个结构, 包含所有的形参,

在这里插入图片描述

使用空接口

如果可变形参中出现了未指定的类型, 它将默认为空接口 interface , 同时它可被指定为任意类型 (参见 11.9节), 如果可变参数不但个数未知, 而且类型也没有指定 ( 可能每个形参的类型都不同), 当函数接收其对应的实参时, 将使用 for-range 循环, 基于实参的数值, 以确定其类型:

在这里插入图片描述

6.4 defer 和跟踪

关键字 defer 可使一条语句或一个函数, 在封闭代码块 (使用{}, 比如函数体) 结束之前完成执行 (即延期执行), 当函数体返回时 (不光是函数体结束的返回, 也可能是每次返回之后, 或是函数体执行过程中出现错误),延期执行都可完成所需的任务 (比如一个函数调用或是一个表达式计算), 也就是在} 之前, 因为 return 语句也是一个表达式, 用于返回一个或多个变量.

defer 与 Java,C# 的 finally 程序块很相似, 在大多数情况下, 它将用于已分配资源的释放.

例 6.8 defer.go

在这里插入图片描述
在这里插入图片描述
在上述代码中, 可移除 defer, 查看 Function2() 函数的执行次序. 如果 defer 语句包含了参数, 将会在 defer 语句行, 传入对应的参数值, 在以下代码中,defer 语句将打印 0.
在这里插入图片描述
如果代码中出现多条 defer 语句, 将基于 defer 语句的排列次序, 实现逆向执行, 类似于堆栈或 LIFO(后进先出),
在这里插入图片描述
defer 语句可保证, 在函数返回之前, 能执行一些清理任务, 并有助于代码的清爽和简洁, 如下:

关闭文件流

在这里插入图片描述
在这里插入图片描述

defer 可实现跟踪功能

当进入和退出某些函数时, 可打印出对应的消息, 将实现程序的高效跟踪, 比如以下的 2 个函数:
在这里插入图片描述
其中 untrace 函数可放入 defer 语句, 如下:

例 6.10 defer_tracing.go

在这里插入图片描述
在这里插入图片描述
上述代码还可以更加简洁, 如下:

例 6.11 defer_tracing2.go

在这里插入图片描述

使用 defer 创建函数调用中参数值和返回值的记录

有了这些记录, 将为程序调试提供帮助:

例 6.12 defer_logvalues.go

在这里插入图片描述
在这里插入图片描述

6.5 内建函数

Go 语言提供了一些内建函数, 这类函数并未放入包中, 因此无须导入包, 也能使用这些函数, 只是这类函数能适应不同的类型, 比如 len,cap 和 append, 或是与系统层相关的 panic, 所以编译器对这些函数提供了支持, 这里列出了一些内建函数, 并会在后续小节中讨论它们.
• close: 用于并发通道.

• len,cap: len 可获取一些类型 (比如 string,array,slice,map,channel) 的长度, cap 可获取类型的存储容量(目前只支持 slice 和 map).

• new,make: 都可用于内存分配,new 可用于数值类型和用户自定义类型 (比如 struct), make 可用于内建的引用类型 (比如 slice,map,channel).

类型名可作为该类函数的参数, 如 new(type),make(type), 同时 new(T) 可为类型 T, 分配一个的存储空
间 (初始为空), 并返回存储地址, 也就是一个指针, 参见 10.1 节, 例如基本类型的分配:
在这里插入图片描述
make(T) 可返回一个 T 类型的变量 (已初始化), 因此它的任务比 new 更多, 参见 7.2-4 节,8.1.1 节和
14.2.1 节, 注意,new() 是一个函数, 不要忘记它的圆括号!

• copy,append: 可用于 slice 的复制和合并, 参见 7.5.5 节.
• panic,recover: 可用于错误处理机制, 参见 13.2 节.
• print,println: 底层打印函数 (参见 4.2 节), 在应用程序中, 可选择 fmt 包定义的打印函数.
• complex,real imag: 可用于创建和处理复数, 参见 4.5.2.2 节.

6.6 递归函数

如果一个函数需要调用其自身, 则称为递归函数, 最有名的示例即斐波纳契数列 ( Fibonacci sequence) 的计算, 也就是前两个数相加, 将得到后一个数, 如下:
在这里插入图片描述

例 6.13 defer_logvalues.go

在这里插入图片描述
在这里插入图片描述
大多数问题都会用到递归操作, 比如快速排序算法, 在递归函数的用法中, 最重要的问题是堆栈溢出, 即所需的大量递归调用, 超出了分配给程序的堆栈空间, 解决该问题, 可采用延迟求值( lazy evaluation) 的方法, 在 Go语言中, 可使用一个并发通道 (channel) 和一个并发协程 (goroutine) 实现该方法 (参见 14.8 节).

在 Go 语言中, 也可使用函数之间的相互递归, 即两个函数之间的相互调用而形成递归操作, 由于 Go 语言的编译特性, 这两个函数的声明次序并无要求, 如下:

例 6.14 mut_recurs.go

在这里插入图片描述
在这里插入图片描述

6.7 函数传递

函数形参可包含其他函数, 因此在函数执行中, 也可调用形参包含的函数, 这被称为回调(callback), 如下:

例 6.15 function_parameter.go

在这里插入图片描述
进行函数传递的另一个示例为 strings.IndexFunc() 函数, 其原型为:
在这里插入图片描述
如果 f© 为真 (大于 0), 将返回字符串 s 中第一个 Unicode 字符的索引值, 如果 f© 为-1, 则执行空操作.
strings.IndexFunc(line, unicode.IsSpace) 可返回字符串 line 中首个空格的索引值, 当然也可在上述函数中, 加入以下的自定义函数:
在这里插入图片描述
在 14.10.1 节中, 还将讨论函数的传递, 并编写一个客户端与服务器之间的通讯程序.
在这里插入图片描述

6.8 匿名函数

当不想给出函数名时, 可使用匿名函数,
在这里插入图片描述
这类函数无法独立存在, 否则编译器将产生一个错误:
在这里插入图片描述
需将其分配一个变量, 也就是让变量引用该匿名函数, 如下:
在这里插入图片描述
以下匿名函数可获取一百万以内的整型值之和, 而 gofmt 工具可对匿名函数, 进行重新排版:
在这里插入图片描述
在上述代码中, 第一个圆括号可包含形参列表, 并且直接放在 func 关键字之后, 因为该函数无函数名, {} 之间给出了函数体, 第二个圆括号也可包含一个形参列表, 当匿名函数被调用时, 如果第一个和第二个圆括号中, 都出现的形参, 调用者必须提供所有对应的实参.

例 6.16 function_literal.go

在这里插入图片描述
从输出结果中可知,g 变量的类型为 func(int), 它的数值是一个内存地址. 因此一个变量可包含一个匿名函数的入口地址 (即指针), 而匿名函数也和其他函数一样, 可选择是否需要形参, 以下匿名函数在调用时, 需要传入一个 v 实参:
在这里插入图片描述

例 6.17 return_defer.go

在这里插入图片描述在这里插入图片描述
在上述代码中, 输出结果为 2, 因为 return 1 之后, 又执行了 ret++, 这便于修改函数返回的错误码.

defer 经常与匿名函数一起使用, 这既可以修改返回值, 又可使用传入宿主函数的实参, 而匿名函数可视为宿主函数的救生艇, 或是 Go 语言的并发协程, 参见第 14 章和 16.9 节.

匿名函数也被称为闭环 (closure), 这类函数可使用已定义的变量, 并能捕获一些外部状态, 比如匿名函数的创建, 另一个更规范的描述是, 匿名函数继承了宿主函数的作用域, 同时宿主函数和匿名函数之间, 可共享一些状态 (比如变量), 而且它们对变量的访问方式也相同, 参见 6.9 节, 匿名函数还可作为封装函数, 为需要封装的函数, 预定义一个或多个形参, 之后的章节中, 将大量包含这类例子, 当然推荐的应用方法是, 使用匿名函数, 执行干净的错误检查, 参见 16.10.2 节.

6.9 匿名函数的应用: 函数封装

在以下程序 function_return.go 中, Add2 和 Adder 函数都可返回一个匿名函数 func(b int) int:
在这里插入图片描述
Add2 函数中未包含形参, 而 Adder 函数则包含了一个 int 形参, 以下将创建 Adder 的一个特例, 并设定一个名称, 如下例的 TwoAdder.

例 6.18 function_return.go

在这里插入图片描述
Add2 和 Adder 都封装了一个匿名函数, 同时 p2 复制了 Add2 的入口地址, p2(3) 等同于调用了 func(3), 因此输出结果为 5, 同样 TwoAdder 复制了 Adder 的入口地址, 由于 Adder 需要提供一个形参, 而 Adder(2) 的意义在于, 实现了匿名函数的初始化, 也就是将实参 2 传递给 func 匿名函数, 之后的 TwoAdder(3), 则是将实参 3 传递给 a, 因此 a=3,b=2, 所以输出结果为 5.

以下使用了略微不同的方法, 实现了一个相同的函数:

例 6.19 function_closure.go

在这里插入图片描述
首先 Adder() 函数分配给变量 f, 同时 Adder() 又封装了一个匿名函数 func(int) int, 并且在 return 语句中, 给出了匿名函数的实现 func(delta int) int, 当 f(1) 出现时, 实参的传递次序为 Adder>func(int)>func(delta int),即 delta=1, 而 x 的初始值为 0, 输出结果为 1, 这里需提到匿名函数的一个特性, 将会保存函数内部的变量值,所以 x=1, 之后 delta=20, 输出结果为 21, 因此 x=21, 再后 delta=300, 输出结果为 321, 从上例中, 可知匿名函数采用闭环机制 (只关心内部操作), 而在匿名函数中使用或修改的变量, 也可在匿名函数外进行声明, 如下:
在这里插入图片描述
匿名函数也可用于一个集合中的所有元素, 并能更新这些变量, 之后这些变量可作为全局变量使用.

可返回另一个函数的函数, 可称之为工厂函数, 当需要创建一组相似函数时, 可使用该功能, 即编写一个工厂函数, 而不是编写出所有的相似函数, 以下代码可返回一个函数, 为文件名添加一个唯一的后缀:
在这里插入图片描述
之后可创建以下函数:
在这里插入图片描述
在这里插入图片描述
可返回另一个函数的函数, 或是将另一个函数作为形参的函数, 都称之为高阶函数, 基于这个特性, 可将 Go 语言视为一种函数语言, 同时在 Go 语言中, 匿名函数的使用相当频繁, 经常会和并发协程和并发通道一同使用(参见第 14 章的 8-9 节), 从 11.14 节的 cars.go 示例, 将看到 Go 语言强大的函数功能.

6.10 匿名函数的调试

在分析和调试复杂程序时, 由于复杂程序在不同的代码文件中, 将包含了大量的函数以及函数调用, 如果能了解程序在执行那个文件, 以及当前执行的代码行, 这将有助于分析和调试, 而这类功能可使用 runtime 或 log包的特殊函数, 在 runtime 包中,Caller() 可提供上述的信息, 如果匿名函数 where() 调用了 Caller(), 之后就可在任意位置调用它.
在这里插入图片描述
设定 log 包的一个标志, 也提供与 Caller() 相似的信息:
在这里插入图片描述
或是定义一个变量, 如下:
在这里插入图片描述

6.11 计时函数

有时我们需要了解运算所消耗的时间, 比如在测试和比较过程中, 一个最简单的方式, 是在运算之前, 记录一个起始时间, 并在运算之后, 记录一个结束时间, 这类记录操作可选择 time 包的 Now() 函数, 而起始时间与结束时间之间的间隔时间, 可调用 Sub() 函数, 如下:
在这里插入图片描述
在代码块执行时间的优化中, 可使用上述函数, 对优化结果进行评估, 以确定优化是否成功.

6.12 使用缓存

在繁重的运算过程中, 我们需要考虑大量运算对性能的影响, 比如是否存在无谓计算, 运算是否可以实现重用,同时还需要考虑内存的用量.

例如在 Fibonacci 数列的计算中, 需要之前的两个相邻数值, 如果这两个相邻数值未保存, 后续数列的运算将产生大量的重复, 这也是例 6.11 的计算方法. 最简单的解决方法, 是将 n 阶 Fibonacci 数列放入一个数组, 数组索引为 n, 参见第 7 章, 以便存取之前的数列, 避免重复运算, 这也是例 16.21 的编程思路, 同时性能的提升也相当惊人, 同样的计算时间内, 后者可实现 40 阶 Fibonacci 数列的计算.

之前的计算方法 (例 6.11) 所消耗的时间: 4.730270s
改进的计算方法 (例 6.21) 所消耗的时间: 0.001000s

算法的改进结果相当明显, 当然这也可用于其他类型的计算, 同时也可使用 map 类型, 替代数组或 slice.

例 6.21 fibonacci_memoization.go

在这里插入图片描述
对于开销巨大的函数 (并非专指递归函数) 来说, 使用缓存可产生巨大的效益, 在相同参数下, 缓存可减少计算时间的消耗, 但是这类算法只能用于纯函数, 即给出相同参数, 可生成相同结果的函数, 同时也不会造成副作用.

在这里插入图片描述