函数
定义
函数是结构话编程的最小模板单元。它将复杂的算法过车分解为若干较小任务,隐藏相关细节,使得程序结构更加清晰,易于维护。函数设计成相对对立,通过接收输入参数完成一段算法指令,输出或存储相关结果。函数是代码复用和测试的基本单元。
关键字func用于定义函数。Go中的函数有些不太方便的限制,但也借鉴了动态语言的某些优点。
- 无须前置声明
- 不支持命名嵌套
- 不支持同名函数重载
- 不支持默认参数
- 支持不定长变参
- 支持多返回值
- 支持命名返回值
- 支持匿名函数和闭包
函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。
**第一类对象(first-class object)指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。**最常用的就是匿名函数。
函数只能判定其是否为nil,不支持其他比较操作。
从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存。
函数内联(inline)对内存分配有一定影响。
当前编译器并未实现尾递归优化(tail-call optimization).尽管Go执行栈的上限是GB,轻易不会出现堆栈溢出(stack overflow)错误,但依然需要注意拷贝栈的复制成本。
参数
Go对参数的处理偏向保守,不支持有默认值的可选参数,不知命名实参;调用时,必须按签名顺序传递指定类型和数量的实参,就算以“
—”命名的参数也不能忽略。
在参数列表中,相邻的同类型参数可合并。
func test(x , y int ,s string ,_ bool) *int{
return nil;
}
参数可视作函数局部变量,因此不能在相同层次定义同名变量。
形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量,变量,表达式或函数等。
不管是指针,引用类型,还是其他类型参数,都是值拷贝传递(pass-by-value).区别无非是拷贝目标对象,还是拷贝指针。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
如果函数参数过多,建议将其重构为一个复合结构类型。
type zxt struct {
name string
sex string
age int
}
func newOption *zxt{
return &zxt{
name: "zxt"
sex: "nan"
age: 18
}
}
变参
变参本质上就是一个切片。只能接收一到多个类型参数,且必须放在参数列表尾部。
将切片作为变参是,须进行展开操作。如果是数组,先将其转换为切片。
func test(a ...int){
fmt.Println(a)
}
func mian(){
a := [3]int{1,2,3}
test(a[:]...)
}
既然变参是切片,那么参数复制的仅是切片自身,并不包括底层数组,也因此可修改原数据,如果需要,可用内置函数copy复制底层数据。
返回值
有返回值的函数,必须有明确的return终止语句。
除非有paint,或者无break的死循环,则无须return终止语句。
借鉴动态语言的多返回值模式,函数得以返回更多状态,尤其是error模式。
不方便的是没有元组类型,也不能用数组,切片接收,但可以“—”忽略掉不想要的返回值。多返回值可用作其他函数调用实参,或当作结果直接返回。
func div(x , y int) (int ,error){
if y = 0 {
return 0,errors.New("0")
}
return x/y ,nil
}
func log(x int , e error){
fmt.Println(x,error)
}
func test()(int ,error){
return div(5,0)
}
func mian(){
log(test())
}
命名返回值
对返回值命名和简短变量定义一样,优缺点共存。
命名返回值让函数声明更加清晰,可以改善帮助文档和代码编译器提示。
命名返回值和参数一样,可当作函数局部变量使用,最后有return隐式返回。
func div (x ,y int ) (z int,err error){
if y =0 {
err =errors.New("0")
return
}
z = x/y
return
}
这些特殊的“局部变量”会被不同层级的同名变量遮蔽。但是编译器能检查到此类状况,只要改为显示调用即可。
func add (x , y int)(z int){
{
z := x /y // 新定义的同名局部变量;同名遮蔽
return //错误 改成 return z
}
return
}
除遮蔽外,还要对全部返回值命名,否则编译器会搞不懂清楚情况。
如果返回值类型能明确表明其含义,就尽量不要对其命名。
匿名函数
匿名函数是指没有定义名字符号的函数。
除了没有名字外,匿名函数和普通函数完全相同。最大区别是,可以在函数内部定义匿名函数,形成类似嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。
直接执行
func main(){
func(s string){
println(s)
}("zxt hello")
}
赋值给变量
func main(){
add := func(x,y int)int {
return x+y
}
println(add(1,2))
}
作为参数
func test( f func() ) {
}
func mian() {
test(func(){
println("zzz")
})
}
作为返回值
func test() func(int , int) int {
return func(x,y int){
retrun x+y
}
}
func mian(){
add := test()
println(add(1,2))
}
将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本区别。
普通函数和匿名函数都可作为结构体字段,或经通道传递。
不曾使用的匿名函数会被编译器当作错误。
除闭包因素外,匿名函数也是一种常见重构手段,可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,已实现框架和细节。
相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引用外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净,清晰的代码层次。
闭包
闭包(closure)是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。
闭包是函数和引用环境的组合体。
闭包通过指针引用环境变量,可能导致生命周期延长,甚至被分配到堆内存。
每次用不同的环境变量或传参复制,让各自闭包环境不同。
多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何的修改行为都会影响其他函数取值,在并发模式下可能需要作同步处理。
闭包可以不用参数就可以读取或修改环境状态。
延迟调用
语句defer向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放,解除锁定,以及错误处理等操作。
注意:延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并缓存起来。
多个延迟注册按照FILO次序执行。(栈)
编译器通过插入额外指令来实现延迟调用,而return和panic语句度会终止当前函数流程,引发延迟调用。另外,return语句不是ret汇编指令,会先更新返回值。
误用 延迟调用在函数结束时才被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。
错误处理
error
官方推荐的标准做法是返回error状态。
func zxt(a ...interface{})(n int , e error){
}
标准库将error定义为接口类型,以便实现自定义错误类型。
type error interface {
Error() string
}
error总是最后一个返回参数。标准库提供了相关创建函数,可以方便地创建包含简单错误文本的error对象。
err := errors.New("zzz")
应通过错误变量,而非文本内容来判定错误类别。
错误变量通常以er r作为前缀,且字符串内容全部小写,没有结束标点,以便于嵌入到其他格式化字符串中输出。
大量函数和方法 返回error,使得调用代码变得很难看,一堆堆的检查语句充斥在代码执行积案。解决思路有:
- 使用专门的检查函数处理错误逻辑(如记录日志),简化检查代码。
- 在不影响逻辑的情况下,使用defer延后处理错误状态(err退化赋值)。
- 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时在处理。
panic,recover
与error相比,panic/recover在使用方法上更类似于try/catch结构化异常。
func panic(v interface{})
func recoer() interface{}
panic和recover是内置函数并非语句。panic会立即中断当前函数流程,执行延迟调用。而在延迟调用函数中,recover可捕获并返回panic提交的错误对象。
func main(){
defer func(){
if err :=recover(); err != nil { //捕获错误
log.fatalln(err)
}
}()
panic("中断") //引发错误
println("exit") // 永远不会被执行
}
因为panic参数是空接口类型,因此可使用任何对象作为错误状态。而recover返回结果同样要做转型才能获取到具体信息。
无论是否执行recover,所有延迟调用都会被执行。但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃。
func test(){
defer println("test 1") // 遵循 栈的顺序 先进后出
defer println("test 2")
panic("中断") //中断当前函数 触发延迟调用
}
func main(){
defer func(){
log.Println(recover())
}()
test() //先执行test 函数
}
// 输出结果
// test 2
// test 1
// 中断
连续调用panic仅最后一个会被recover捕获。
func main() {
defer func() {
for {
if err := recover(); err != nil {
log.Panicln(err)
} else {
log.Fatalln("fatal")
}
}
}()
defer func() {
panic("抛出异常1")
}()
panic("抛出异常2")
}
在延迟函数中Panic,不会影响后续延迟调用执行,而recover之后panic,可被在此捕获。
除非是不可恢复性导致系统无法正常工作的错误,否则不建议使用panic.
参考资料
<Go语言学习笔记> 雨痕