-
函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块(Go 语言中的方法本质上也是函数)
-
Go 程序就是一组函数的集合,Go 程序的执行流本质上就是在函数调用栈中上下流动,从一个函数到另一个函数。
-
在同一个 Go 包中,函数名应该是唯一的。
-
而参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素.
-
每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例。
-
Go 语言中,函数参数传递采用是值传递的方式。所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中。对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。
-
但是像 string、切片、map 这些类型就不是了,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为“浅拷贝”。
-
对于类型为接口类型的形参,Go 编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go 编译器会将零个或多个实参按一定形式转换为对应的变长形参。
-
在 Go 中,变长参数实际上是通过切片来实现的。
-
Go 语言的函数作为“一等公民”
- 特征一:Go 函数可以存储在变量中
- 特征二:支持在函数内创建并通过返回值返回。
- 特征三:作为参数传入函数。
- 特征四:拥有自己的类型。
与其他主要编程语⾔的差异
-
可变参数
func sum(ops ...int) int { res := 0 for _, v := range ops { res += v } return res } func TestSum(t *testing.T) { t.Log(sum(1, 2, 3, 4)) // 10 t.Log(sum(1, 2, 3, 4, 5)) // 15 } -
可以有多个返回值
func returnMulti() (int, int) { rand.Seed(time.Now().UnixNano()) return rand.Intn(10), rand.Intn(20) } func TestReturnMulti(t *testing.T) { t.Log(returnMulti()) } -
函数可以作为变量的值,可以作为参数和返回值
匿名函数与闭包
-
匿名函数是没有函数名的函数,保存到某个变量或者作为立即执行函数,多用于实现回调函数和闭包。
func showfunc() { // 将匿名函数保存到变量 add := func(x, y int) { fmt.Println(x + y) } add(10, 20) // 通过变量调用匿名函数 // 自执行函数:匿名函数定义完加()直接执行 func(x, y int) { fmt.Println(x + y) }(10, 40) } -
闭包=函数+应用环境。
-
Go 闭包是在函数内部创建的匿名函数,这个匿名函数可以访问创建它的函数的参数与局部变量。
func adder() func(int) int { var x int return func(y int) int { x += y return x } } func useAdder() { f := adder() fmt.Println(f(10)) // 10 fmt.Println(f(20)) // 30 fmt.Println(f(30)) // 60 f2 := adder() fmt.Println(f2(20)) // 20 fmt.Println(f2(40)) // 60 } -
闭包本质上就是一个匿名函数或叫函数字面值,它们可以引用它的包裹函数,也就是创建它们的函数中定义的变量。然后,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。
错误处理策略
-
如何进行错误处理的?
- Go 函数增加了多返回值机制,来支持错误状态与返回信息的分离
-
使用 error 类型,而不是传统意义上的整型或其他类型作为错误类型,有什么好处呢?
- 第一点:统一错误类型。
- 第二点:错误是值。
- 第三点:易扩展,支持自定义错误上下文。
-
具体策略:
- 策略一:透明错误处理策略。这样构造出的错误值代表的上下文信息,对错误处理方是透明的,因此这种策略称为“透明错误处理策略”。
- 策略二:“哨兵”错误处理策略:我建议你尽量使用errors.Is方法去检视某个错误值是否就是某个预期错误值,或者包装了某个特定的“哨兵”错误值。
- 策略三:错误值类型检视策略:请尽量使用errors.As方法去检视某个错误值是否是某自定义错误类型的实例。errors.As函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型。
- 策略四:错误行为特征检视策略:将某个包中的错误类型归类,统一提取出一些公共的错误行为特征,并将这些错误行为特征放入一个公开的接口类型中。
-
健壮性的“三不要”原则
- 不要相信任何外部输入的参数。为了保证函数的健壮性,函数需要对所有输入的参数进行合法性的检查。
- 不要忽略任何一个错误。我们不能假定它一定会成功,我们一定要显式地检查这些调用返回的错误值。一旦发现错误,要及时终止函数执行,防止错误继续传播。
- 不要假定异常不会发生。
Go 语言中的异常:panic
-
一类是来自 Go 运行时,另一类则是 Go 开发人员通过 panic 函数主动触发的。
-
无论在哪个 Goroutine 中发生未被恢复的 panic,整个程序都将崩溃退出。
-
不过,函数 F 中已进行求值的 deferred 函数都会得到正常执行,执行完这些 deferred 函数后,函数 F 才会把控制权返还给其调用者。
-
recover 是 Go 内置的专门用于恢复 panic 的函数,它必须被放在一个 defer 函数中才能生效。如果 recover 捕捉到 panic,它就会返回以 panic 的具体内容为错误上下文信息的错误值。如果没有 panic 发生,那么 recover 将返回 nil。而且,如果 panic 被 recover 捕捉到,panic 引发的 panicking 过程就会停止。
func funcA() { fmt.Println("func A") } func funcB() { defer func() { err := recover() //如果程序出出现了panic错误,可以通过recover恢复过来 if err != nil { fmt.Println("recover in B") } }() panic("panic in B") } func funcC() { fmt.Println("func C") } func mainRecover() { funcA() funcB() funcC() // func A // recover in B // func C } -
如何应对 panic?
-
第一点:评估程序对 panic 的忍受度。
-
第二点:提示潜在 bug。在 Go 标准库中,大多数 panic 的使用都是充当类似断言的作用的。
-
第三点:不要混淆异常与错误。
- Java 的checked exception用于一些可预见的、常会发生的错误场景,Java 中对checked exception处理的本质是错误处理,虽然它的名字用了带有“异常”的字样。
- Go 中的 panic 更接近于 Java 的RuntimeException+Error,而不是checked exception。
-
defer 函数
-
defer 函数。在Go语言的函数中
return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。func Clear() { fmt.Println("clear string") } func TestDefer(t *testing.T) { defer Clear() fmt.Println("continue") panic("err") // defer 仍会执行 } -
只有在函数(和方法)内部才能使用 defer。
-
这些函数被称为 deferred 函数。defer 将它们注册到其所在 Goroutine 中,用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前,按后进先出(LIFO)的顺序被程序调度执行。
-
defer注册要延迟执行的函数时,该函数所有的参数都需要确定其值。
-
注意事项
- 第一点:明确哪些函数可以作为 deferred 函数
- 第二点:注意 defer 关键字后面表达式的求值时机
- 第三点:知晓 defer 带来的性能损耗
- defer 关键字后面的表达式,是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。