函数

函数

函数声明

每个函数声明都包含一个名字, 一个形参列表, 要给可选的返回列表以及函数体:

1
2
3
func name(parameter-list) (result-list) {
body
}

返回值也可以像形参一样命名. 这个时候, 每个命名的返回值会声明为一个局部变量, 并根据变量类型初始化为相应的0值.

函数的类型称作函数签名. 当两个函数拥有相同的形参列表和返回列表时, 认为这两个函数的类型或签名是相同的.

每一次调用函数都需要提供实参来对应函数的每一个形参. Go语言没有默认参数值的概念, 也没有任何方法可以通过参数名指定形参.

实参是按值传递的, 所以函数接收到的是每个实参的副本; 修改函数的形参变量并不会影响到抵用者提供的实参. 然后就, 如果提供的实参包含引用类型, 比如指针, slice, map, 函数或者通道, 那么当函数使用形参变量时就有可能会间接修改实参变量.

如果函数声明没有函数体, 那说明这个函数使用了除Go以外的语言实现.

递归

许多编程语言使用固定长度的函数调用栈; 大小在64KB2MB之间. 递归的深度会受限于固定长度的栈大小, 所以但进行深度递归调用时必须谨慎防栈溢出. 固定长度的栈甚至会造成一定的安全隐患. 相比固定长的栈, Go语言的实现是使用了可变长度的栈, 栈的大小会随着使用而增长, 可达到1GB左右的上限.

多返回值

Go语言的垃圾回收机制将回收未使用的内存, 但不能指望它会释放未使用的操作系统资源, 比如打开的文件以及网络连接. 必须显式地关闭它们.

返回一个多值结果可以是调用另一个多字返回的函数.

一个多值调用可以作为单独的实参传递给拥有多个形参的函数中.

良好的名称可以使得返回值更加有意义. 尤其在一个函数返回多个结果且类型相同时.

一个函数如果有命名的返回值, 可以省略return语句的操作数, 这称为裸返回. 因为能直观看出返回值, 应该保守使用裸返回.

错误

如果当函数函数调用发生错误时返回一个附加的结果作为错误值, 习惯上将错误值作为最后一个结果返回. 如果错误只有一种情况, 结果通常设置为布尔类型. 更多时候, 尤其对于I/O操作, 错误的原因可能多种多样, 而调用者则需要一些详细的信息. 在这种情况下, 错误的结果类型往往是error.

error是内置的接口类型. 非空的错误类型有一个错误消息字符串, 可以通过调用它的Error方法或者通过调用fmt.Println(err)fmt.Print("%v", err)直接输出错误消息.

Go中, 函数运行失败时会返回错误信息, 这些错误信息被认为时一种预期的值而非异常, 这使得Go有区别于那些将函数运行失败看作异常的语言. 虽然Go有各种异常机制, 但这些机制仅被使用在处理那些未被预料到的错误, 即bug, 而不是那些键壮程序中应该被避免的程序错误.

错误处理策略

当一个函数调用返回一个错误时, 调用者应负责检查错误并采取合适的处理应对.

首先也还是最常见的情形是将错误传递下去, 使得子例程中发生的错误变为主调例程的错误. 我们为原始错误消息不断添加额外的上下文信息来建立一个可读的错误描述. 当错误最终被程序的main函数处理时, 它应该能够提供一个根本问题到总体故障的清晰因果链.

因为错误消息频繁地串联起来, 所以消息字符串首字母不应该大写, 而且应该避免换行.

一般而言, 被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者, 调用者需要添加一些错误信息中不包含的信息.

第二种错误处理策略. 对于不固定或者不可预测的错误, 在短暂的间隔后对操作进行重试是合乎情理的, 超出一定的重试次数和限定的时间后在报错退出.

第三, 如果依旧不能顺利进行下去, 调用者能够输出错误然后优雅地停止程序, 但一般这样的处理应该留给主程序部分. 通常库函数应当将错误传递给调用者, 除非这个错误表示一个内部一致性错误, 这意味着库内部存在bug.

一个更方便的方法是通过调用log.Fatalf实现相同的效果. 就和所有日志函数一样, 它默认会将时间和日期作为前缀添加到错误消息前.

第四, 在一些错误情况下, 只记录下错误信息然后程序继续运行. 可以使用log.Printf或者直接输出到标准错误流. log包中所有函数会为没有换行符的字符串增加换行符.

第五, 在某些罕见的情况下我们可以直接安全地忽略掉整个错误. 要习惯考虑到每个函数调用可能发生的出错情况, 当你有意地忽略一个错误的时候, 清晰地注释一下你的意图.

如果某个错误会导致函数返回, 那么成功的逻辑代码不应该放在else语句中, 而应直接放在函数体中.

文件结尾错误(EOF)

Io包保证任何由文件结束引起的读取失败都返回同一个错误(io.EOF).

函数值

Go中, 函数被看作第一类值: 函数像其他值一样, 拥有类型, 可以被赋值给其他变量, 传递给其他变量, 传递给函数, 从函数返回. 对函数值的调用类似函数调用.

函数类型的零值是nil, 调用一个空的函数变量将导致panic错误. 函数变量可以和nil比较, 但它们本省不可比较, 所以不可以相互进行比较或者作为键值出现在map中.

匿名函数

命名函数只能在包级别的作用域进行声明, 但我们能够使用函数字面量在任何表达式内指定函数变量. 函数字面量就像函数声明, 但在func关键字后面没有函数名称. 它是一个表达式, 它的值称作匿名函数.

函数字面量在我们需要的时候才定义.

更重要的是, 以这种方式定义的函数能够获取到整个词法环境, 因此里层的函数可以使用外层函数中的变量.

函数值不仅仅是一串代码, 还记录了状态. 存在变量引用. 这就是函数值属于引用类型和函数值不可比较的原因. Go使用闭包技术实现函数值, Go程序员也把函数值叫做闭包.

当一个匿名函数需要进行递归, 必须先声明变量然后将匿名函数赋值给这个变量.

警告: 捕获迭代变量

为什么在循环体内将循环变量赋给一个新的局部变量, 而不是直接使用循环变量.

问题的原因在于循环变量的作用域. for循环引入了新的语法块, 循环变量在这个词法块中被声明. 在该循环中生成的所有函数值都共享相同的函数变量. 需要注意, 函数值中记录的是循环变量的内存地址, 而不是循环变量某一时刻的值.

这样的隐患不仅仅存在于使用rangefor循环中. 在普通for循环中也存在.

go语句和defer语句中会经常遇到此类问题. 者不是godefer本身导致的, 而是因为它们都会等待循环结束后, 在执行函数值.

变长函数

变长函数被调用的时候可以有可变的参数个数. 最令人熟知的例子就是fmt.Printf与其变种. Prinf需要在开头提供一个固定的参数, 后续便可以就收任意数目的参数.

在参数列表最后的类型名称之前使用省略号...表示声明一个变长函数, 调用这个函数的时候可以传递该类型任意数目的参数.

调用者隐式地创建一个数组, 并将原始参数复制到数组中, 再把数组的一个切片作为参数传递给被调用函数. 如果原始参数已经是切片类型, 我们如何传值? 只需要在slice后加....

虽然在可变参数函数内部, 尽管可变参数的行为很像切片类型, 但实际上, 可变参数函数和以切片作为参数的函数是不同的.

可变参数函数经常被用于格式化字符串. interface{}表示函数的最后一个参数可以接收任意类型.

延迟函数调用

语法上, 一个defer语句就是一个普通的函数或方法调用, 在调用之前加上关键字defer. 函数和参数表达式会在语句执行时求值, 但是无论是正常情况下, 执行reutrn或函数执行完毕, 还是不正常的情况下, 比如发生宕机, 实际的调用推迟到包含defer语句的函数结束后才执行. defer语句没有限制使用次数; 执行的时候以调用defer语句的顺序倒序进行.

defer语句经常被用于处理成对的操作, 如打开, 关闭, 连接, 断开连接, 加锁, 释放锁. 通过defer机制, 无论函数逻辑多复杂, 都能保证在任何执行路径下, 资源被释放. 释放资源的defer应该直接跟在请求资源的语句后.

调试复杂程序时, defer机制也常被用于记录何时进入和退出函数. defer后面的函数返回一个函数. 返回函数的函数会在入口执行, 返回的函数会在出口执行.

我们知道, defer语句中的函数会在return语句更新返回值变量后在执行, 又因为在函数定义的匿名函数可以访问函数包括返回变量在内的所有变量, 所以, 对匿名函数采用defer机制, 可以使其观察函数的返回值. 被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值.

在循环体中的defer语句需要特别注意, 因为只有在函数执行完毕之后, 这些函数被延迟的函数才会被执行, 有些需要在一次循环结束后关闭, 这时可以把循环体变成一个函数, 在这个函数中使用defer.

如果试图使用延迟调用去关闭一个本地文件就会有些问题. 在许多文件系统中, 尤其是NFS, 写错误往往不是立即返回而是推迟到文件关闭的时候. 如果没有检查文件关闭时的反馈信息, 可能会导致数据丢失, 而我们还认为写入操作成功.

Panic异常

Go的类型系统会在编译时捕获很多错误, 但有些错误只能在运行时检查, 如数组访问越界, 空指针引用等. 这些运行时错误会引起panic异常.

一般而言, 当panic异常发生时, 程序会中断运行, 并立即执行在该goroutine中被延迟的函数(defer机制). 随后, 程序崩溃并输出日志信息. 日志信息包括panic value和函数调用的堆栈很跟踪信息. panic value通常是某种错误信息. 对于每个goroutine, 日志信息中都会有与之相对的, 发生panic时的函数调用堆栈跟踪信息.

不是所有的panic异常都来自运行时, 直接调用内置的panic函数也会引发panic异常; panic函数接受任何值作为参数. 当某些不应该发生的场景发生时, 我们就应该调用panic.

断言函数必须满足的前置条件时明智的做法, 但者很容易被滥用. 除非你能提供更多的错误信息, 或者能更快的发现错误, 否则不需要使用断言, 编译器在运行时会帮你检查代码.

虽然Gopanic机制类似于其他语言的异常, 但panic的适用场景有些不同. 由于panic会引起程序的崩溃, 因此panic一般用于严重的错误, 如程序内部的逻辑不一致. 勤奋的程序员认为任何崩溃都表明程序中存在漏洞, 所以对于大部分漏洞, 我们应该使用Go提供的错误机制, 而不是panic, 尽量避免程序崩溃. 在健壮的程序中, 任何可以预料到的错误, 如不正确的输入, 错误的配置或是失败的I/O操作都应该被优雅地处理, 最好的处理方式, 就是使用Go的错误机制.

当调用者明确的知道正确的输入不会引起函数错误是, 要求调用者检查这个错误时不必要和累赘的. 我们应该假设函数的输入一直合法, 就如前面的断言一样: 当调用者输入了不该出现的输入时, 触发panic异常. 函数名中的Must前缀是一种针对此类函数的命名约定.

Recover捕获异常

通常来说, 不应该对panic异常做任何处理, 但有时, 也许我们可以从异常中恢复, 至少可以在程序崩溃前, 做一些操作.

如果在deferred函数中调用了内置函数recover, 并且定义该defer语句的函数发生了panic会使程序从panic中恢复, 并返回panic value. 导致panic异常的函数不会继续运行, 但能正常返回. 在未发生panic时调用recover, recover会返回nil.

不加区分的恢复所有的panic异常, 不是可取的做法; 因为在panic之后, 无法保证包级变量的状态仍然和我们预期一致.

作为被广泛遵守的规范, 你不应该试图去恢复其他包引起的panic. 公有的API应该将函数的运行失败作为error返回, 而不是panic. 同样的, 你不应该恢复一个由他人开发的函数引起的panic, 比如说调用者传入的回调函数, 因为你无法确保这样做是安全的.

只恢复应该恢复的panic异常, 此外, 这些异常所占的比例应该尽可能的低. 为了标识某个panic是否应该被恢复, 我们可以将panic value设置成特殊类型. 在recover时对panic value进行检查, 如果发现panic value是特殊类型, 就将这个panic作为error, 如果不是, 则按照正常的panic进行处理.

常用函数

1
2
3
4
5
6
7
fmt.Printf("%*s", 数量, "字符")

# 字符串排序
sort.String()

# runtime包允许程序员输出堆栈信息
runtime.Stack()
-------------本文结束感谢您的阅读-------------