文章目录
高级语言都支持函数或类函数的编程结构:
- 现代计算机进程执行模型大部分是基于“栈堆”的,而编译器不需要对函数做过多的转换就能让其在栈上运行(只需要处理好参数和返回值的传递即可);
- 函数对代码的抽象程度适中,就像胶水,很容易将编程语言的不同层级的抽象提“黏结”起来
- 函数性语言其数值不变形在高并发的场景备受青睐
Go 不是一门纯函数式的编程语言,但是函数在 Go 中是“第一公民”,表现在:
- 函数是一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数和返回值,也可以直接调用执行
- 支持多返回值
- 支持闭包
- 支持可变参数
Go 是通过编译成本地代码且基于“堆栈”式执行的。
2.1 基本概念
2.1.1 函数定义
函数是 Go 程序源代码的基本构造单位。
包含以下几个部分:
- 函数声明关键字 func
- 函数名:遵循标识符命名规则,首字母的大小写决定该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问
- 参数列表:使用“()”包裹
- 返回列表:使用“()”包裹,只有一个返回值,且为非命名的参数,可以省略“()”
- 函数体:使用“{}”包裹,并且“{”必须位于函数返回值同行的行尾
func funcName (param_list) (result-list) {
function-body
}
函数的特点
1)函数可以没有输入参数,也可以没有返回值。
func A(){ // 无返回值,不能通过参数接收 a := A()
// do something
...
}
// command-line-arguments
// A() used as value
func A()int{ // 有返回值,可以通过参数接收 a := A()
// do something
...
return 1
}
2)多个相邻的相同类型的参数可以使用简写模式。
func add(a, b int) int { // a int, b int 简写为 a, b int
return a + b
}
3)支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的 return 可以不带参数名直接返回。
// sum 相当于函数内的局部变量,被初始化为零
func add(a, b int) (sum int) {
sum = a + b
return // return sum 的简写模式
}
func add(a, b int) int {
sum := a + b // 如果是 sum := a + b,则相当于新声明一个 sum 变量命名返回变量 sum 覆盖
return sum // 最后需要显式地调用 return sum
}
4)不支持默认值参数
5)不支持函数重载
6)不支持函数嵌套,严格地说是不支持命名函数的嵌套定义,但支持嵌套匿名函数
func add(a, b int) (sum int) {
anonymous := func(x, y int) int {
return x + y
}
return anonymous(a, b)
}
2.1.2 多值返回
Go 函数支持多值返回,定义多值返回的返回参数列表时要使用“()”包裹,支持命名参数返回。
func swap(a, b int) (int, int) {
return b, a
}
如果多值返回有错误类型,一般讲错误类型作为最后一个返回值。
2.1.3 实参到形参的传递
Go 函数实参到形参的传递永远都是值传递,有时函数调用后实参指向的值发生了变化,那是因为参数传递的是指针值得拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址,本质上参数传递仍然是值拷贝。
package main
import (
"fmt"
)
func chvalue(a int) int {
a = a + 1
return a
}
func chpointer(a *int) {
*a = *a + 1
return
}
func main() {
a := 10
chvalue(a) // 实参传递给形参是值拷贝
fmt.Println(a) // 10
chpointer(&a) // 实参传递给形参仍然是值拷贝,只不过复制的是a的地址值
fmt.Println(a) // 11
}
2.1.4 不定参数
Go 函数支持不定数目的形式参数,不定参数声明使用 param …type 的语法格式。
以下几个特点:
1)所有的不定参数类型必须是相同的。
2)不定参数必须是函数的最后一个参数。
3)不定参数名在函数体内相当于切片,对切片的操作同样适合对不定参数的操作。
func sum(slice ...int) (sum int) {
for _, v := range slice { // 此时 arr 就相当于切片,可以使用 range 访问
sum += v
}
return
}
4)切片可以作为参数传递给不定参数,切片名后要加上“…”。
func sum(slice ..int) (sum int) {
for _, v := range slice {
sum += v
}
return
}
func main() {
slice := []int{1, 2, 3, 4}
array := [...]int{1, 2, 3, 4}
// 数组不可以作为实参传递给不定参数的函数
sum(slice...)
}
5)形参为不定参数的函数和形参为切片的函数类型不同。
func suma(slice ...int) (sum int) {
for v := range slice {
sum += v
}
return
}
func sumb(slice []int) (sum int) {
for v := range slice {
sum += v
}
return
}
// suma 和 sumb 的类型并不一样
fmt.Printf("%T\n", suma) // func(...int) int
fmt.Printf("%T\n", sumb) // func([]int) int
2.2 函数签名和匿名函数
2.2.1 函数签名
函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名、参数名和{,可以使用 fmt.Printf 的 “%T” 格式化参数打印函数的类型。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
fmt.Printf("%T", add) // func(int, int) int
}
两个函数类型相同的条件是:
- 拥有相同的形参类别和返回值列表(列表元素的次序、个数和类型都相同)
- 形参名可以不同
// 以下两个函数的函数类型完全一样
func add(a, b int) int { return a + b }
func sub(x int, y int) (c int) { c = x - y; return c }
可以使用 type 定义函数类型,函数类型变量可以作为函数的参数或返回值。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
// 定义一个函数类型,输入的是两个 int 类型,返回值是一个 int 类型
type Op func(int, int) int
// 定义一个函数,第一个参数是函数类型 Op,函数类型变量可以直接用来进行函数调用
func do(f Op, a, b int) int {
return f(a, b)
}
func main() {
// 函数名 add/sub 可以当做相同函数类型形参,不需要强制类型转换
a := do(add, 1, 2)
fmt.Println(a) // 3
s := do(sub, 1, 2)
fmt.Println(s) // -1
}
函数类型和 map、slice、chan 一样,实际函数类型变量和函数名都可以当做指针变量,该指针指向函数代码的开始位置。通常说函数类型变量是一种引用类型,未初始化的函数类型的变量的默认值是nil。
Go 中函数是“第一等公民”(first class)。有名函数的函数名可以看做函数类型的常量,可以直接使用函数名调用函数,也可以直接赋值给函数类型变量,后续通过该变量来调用该函数。
package main
func sum(a, b int) int {
return a + b
}
func main() {
sum(3, 4) // 直接调用
f := sum // 有名函数可以直接赋值给变量
f(1, 2)
}
2.2.2 匿名函数
匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方可以由匿名函数代替。
匿名函数:
- 可以直接赋值给函数变量,
- 可以当做实参,
- 可以作为返回值,
- 可以直接被调用。
package main
import (
"fmt"
)
func add(a, b int) int {
return a + b
}
func diff(a, b int) int {
return a - b
}
// 匿名函数被直接赋值给函数变量
var sum = func(a, b int) int {
return a + b
}
// 匿名函数被直接赋值给函数变量
var sub = func(a, b int) int {
return a - b
}
// 定义一个函数类型
type function func(int, int) int
// 函数类型 function 作为参数
func do(f function, a, b int) int {
return f(a, b)
}
// 函数类型 func(int, int) int 作为参数
func doInput(f func(int, int) int, a, b int) int {
return f(a, b)
}
// 匿名函数作为返回值
func wrap(op string) func(int, int) int {
switch op {
default:
return nil
case "add":
// 返回匿名函数
return func(a, b int) int {
return a + b
}
case "diff":
// 返回匿名函数
return func(a, b int) int {
return a - b
}
}
}
func main() {
fmt.Println("-------函数类型----------")
fmt.Printf("%T\n", add)
fmt.Printf("%T\n", diff)
fmt.Printf("%T\n", sum)
fmt.Printf("%T\n", sub)
fmt.Println("-------函数直接调用----------")
fmt.Printf("%d\n", add(1, 2))
fmt.Printf("%d\n", diff(1, 2))
fmt.Printf("%d\n", sum(1, 2))
fmt.Printf("%d\n", sub(1, 2))
fmt.Println("-------函数类型实参调用----------")
fmt.Println(do(add, 1, 2))
fmt.Println(do(diff, 1, 2))
fmt.Println(do(sum, 1, 2))
fmt.Println(do(sub, 1, 2))
fmt.Println("-------函数类型实参调用----------")
fmt.Println(doInput(add, 1, 2))
fmt.Println(doInput(diff, 1, 2))
fmt.Println(doInput(sum, 1, 2))
fmt.Println(doInput(sub, 1, 2))
fmt.Println("-------匿名函数作为实参----------")
// 匿名函数作为实参
fmt.Println(doInput(func(x, y int) int {
return x + y
}, 1, 2))
fmt.Println("-------匿名函数作为返回值----------")
opFunc := wrap("add")
re := opFunc(2, 3)
fmt.Printf("%d\n", re)
}
// 执行结果
-------函数类型----------
func(int, int) int
func(int, int) int
func(int, int) int
func(int, int) int
-------函数直接调用----------
3
-1
3
-1
-------函数类型实参调用----------
3
-1
3
-1
-------函数类型实参调用----------
3
-1
3
-1
-------匿名函数作为实参----------
3
-------匿名函数作为返回值----------
5
2.3 defer
Go 函数里提供了 defer 关键字,可以注册多个延迟调用,这些调用以先进后出(FILO)的顺序在函数返回前被指向。defer 常用于保证一些资源最终一定能够得到回收和释放。
package main
// 先进后出
func main() {
defer func() {
println("first")
} ()
defer func() {
println("second")
} ()
println("function body")
}
// 执行结果(先注册后执行)
function body
second
first
- defer 后面必须是函数或方法的调用,不能是语句,否则会报 expression in defer must be function call 错误。
- defer 函数的实参在注册时通过值拷贝传递进去。
package main
func f() int {
a := 0
defer func(i int) {
println("defer i=", i) // defer i= 0
}(a)
a++
defer func(i int) {
println("defer i=", i) // defer i= 1
}(a)
return a
}
func main(){
f()
}
// 实参 a 的值在 defer 注册时通过值拷贝传递进去,后续语句 a++ 并不会影响 defer 语句最后的输出结果。
// 打印结果
defer i= 1
defer i= 0
- defer 语句必须先注册后执行,如果 defer 位于 return 之后,则 defer 因为没有注册,不会执行。
package main
func main() {
defer func() {
println("first")
} ()
a := 0
println(a)
return
// 不会执行
defer func() {
println("seconde")
} ()
}
// 结果
0
first
- 主动调用 os.Exit(int) 退出进程时,defer 将不再被指向(即使 defer 已经提前注册)。
package main
import "os"
func main() {
defer func() {
println("defer")
} ()
println("func body")
os.Exit(1)
}
// 结果
func body
exit status 1
- defer 的好处是可以在一定程度上避免资源泄露,特别是在很多 return 语句,有多个资源需要关闭的场景中,很容易漏掉资源的关闭操作。
func CopyFile(dst, src string) (w int64, err error) {
srcFilePointer, err := os.Open(src)
if err != nil {
return
}
dstFilePointer, err := os.Create(dst)
if err != nil {
// src 很容易被忘记关闭
srcFilePointer.Close()
return
}
w, err = io.Copy(dstFilePointer, srcFilePointer)
dstFilePointer.Close()
srcFilePointer.Close()
return
}
使用 defer 改写后,在打开资源无报错后直接调用 defer 关闭资源,一旦养成这样的编程习惯,则很难会忘记资源的释放。
func CopyFile(dst, src string) (w int64, err error) {
srcFilePointer, err := os.Open(src)
if err != nil {
return
}
defer srcFilePointer.Close()
dstFilePointer, err := os.Create(dst)
if err != nil {
// src 很容易被忘记关闭
srcFilePointer.Close()
return
}
defer dstFilePointer.Close()
w, err = io.Copy(dstFilePointer, srcFilePointer)
return
}
- defer 语句位置不当,有可能导致 panic,一般 defer 语句放在错误检查语句之后。
- defer 也有明显的副作用:defer 会推迟资源的释放,defer 尽量不要放到循环语句里面,将大函数内部的 defer 语句单独拆分成一个小函数是一种很好的实践方式。另外,defer 相对于普通的函数调用需要间接的数据结构的支持,相对于普通函数调用有一定的性能损耗。
- defer 中最好不要对有名返回值参数进行操作,否则会引发匪夷所思的结果。
2.4 闭包
2.4.1 概念
闭包是由函数及其相关引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
闭包 = 函数 + 引用环境
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):
1)多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存。
2)用一个闭包函数多次,如果闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。
package main
func fa(a int) func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
f := fa(1) // f 引用的外部的闭包环境包括本次函数滴啊用的形参 a 的值为 1
g := fa(1) // g 引用的外部的闭包环境包括本次函数滴啊用的形参 a 的值为 1
// 此时 f、g 引用的闭包环境中的 a 的值并不是同一个,而是两次函数调用产生的副本
println(f(1))
// 多次调用 f 引用的同一个副本 a
println(f(1))
// g 中 a的值仍然是 1
println(g(1))
println(g(1))
}
// 程序运行结果
0xc00008e000 1
2
0xc00008e000 2
3
0xc00008e008 1
2
0xc00008e008 2
3
- 如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。
- 使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的的编程方式。
- 同一个函数返回的多个闭包共享该函数的局部变量。
package main
func fa(base int) (func(int) int, func(int) int) {
println(&base, base)
add := func(i int) int {
base += i
println(&base, base)
return base
}
sub := func(i int) int{
base -= i
println(&base, base)
return base
}
return add, sub
}
func main() {
// f、g 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值
f, g := fa(0) // base 地址是 0xc0000180a0
// s、k 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值
s, k := fa(0) // base 地址是 0xc0000180a8
// f、g 和 s、k 引用不同的闭包变量,这是由于 fa 每次调用都要重新分配形参
println(f(1), g(2))
println(s(1), k(2))
}
// 程序运行结果
0xc0000180a0 0
0xc0000180a8 0
0xc0000180a0 1
0xc0000180a0 -1
1 -1
0xc0000180a8 1
0xc0000180a8 -1
1 -1
2.4.2 闭包的价值
闭包最初的目的是减少全局变量,在函数调用的过程中隐式地传递共享变量,有其有用的一面;但是这种隐秘的共享变量的方式带来的坏处是不够直接,不够清晰,除非是非常有价值的地方,一般不建议使用闭包。
**对象是附有行为的数据,而闭包是附有数据的行为,**类在定义时已经显式地集中定义了行为,但是闭包中的数据没有显式地集中声明的地方,这种数据和行为耦合的模型不是一种推荐的编程模型,闭包仅仅是锦上添花的东西,不是不可缺少的。
2.5 panic 和 recover
- panic 用来主动抛出错误
- recover 用来捕获 panic 抛出的错误
2.5.1 基本概念
panic 和 recover 的函数签名如下:
panic(i interface{})
recover() interface{}
引发 panic 有两种情况:
- 程序主动调用 panic 函数
- 程序产生运行时错误,由运行时检测并抛出
发生 panic 后,程序会从调用 panic 的函数位置或发生 panic 的地方立即返回,逐层向上执行函数的 defer 语句,然后逐层打印函数调用堆栈,直到被 recover 捕获或运行到最外层函数而退出。
panic 的参数是一个空接口类型 interface{},所以任意类型的变量都可以传递给 panic(空接口)。调用 panic 的方法:panic(xxx)。
panic 不但可以在函数正常流程中抛出,在 defer 逻辑里也可以再次调用 panic 或抛出 panic。defer 里面的 panic 能够被后续执行的 defer 捕获。
recover()用来捕获 panic,阻止 panic 继续向上传递。 recover()和 defer 一起使用,但是 recover()只有在 defer 后面的函数体内被直接调用才能捕获 panic 终止异常,否则返回 nil,异常继续向外传递。
// 这个会捕获失败
defer recover()
// 这个会捕获失败
defer fmt.Println(recover())
// 这个嵌套两层也会捕获失败
defer func() {
func() {
println("defer inner")
recover() // 无效
}()
}()
// 如下场景会捕获成功
defer func() {
println("defer inner")
recover()
}()
func except() {
recover()
}
func test() {
defer except()
panic("test panic")
}
可以有连续多个 panic 被抛出,连续多个 panic 的场景只能出现在延迟调用里面,否则不会出现多个 panic 被抛出的场景。但只有最后一次 panic 能被捕获。
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
// 只有最后一次 panic 调用能够被捕获
defer func() {
panic("first defer panic")
}()
defer func() {
panic("second defer panic")
}()
panic("main body panic")
}
// 结果
first defer panic
包中 init 函数引发的 panic 只能在 init 函数中捕获,在 main 中无法被捕获,原因是 init 函数先于 main 执行。函数并不能捕获内部新启动的 goroutine 所抛出的 panic。
package main
import (
"fmt"
"time"
)
func do() {
// 这里并不能捕获 da 函数的 panic
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
// 函数不能捕获内部新启动的 goroutine 所抛出的 panic
go da()
go db()
time.Sleep(3 * time.Second)
}
func da() {
panic("panic da")
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
func db() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
2.5.2 使用场景
- 程序遇到了无法正常执行下去的错误,主动调用 panic 函数结束程序运行。
- 在调试程序时,通过主动调用 panic 实现快速退出, panic 打印出的堆栈能够更快地定位错误。
- 为了保证程序的健壮性,需要主动在程序的分支流程上使用 recover() 拦截运行时错误。
2.6 错误处理
2.6.1 error
Go 语言内置错误接口类型 error。任何类型只有实现 Error() string 方法,都可以传递 error 接口类型变量。Go 语言典型的错误处理方式是将 error 作为函数最后一个返回值。在调用函数时,通过检测其返回的 error 值是否为 nil 来进行错误处理。
type error interface {
Error() string
}
Go 语言标准库提供的两个函数返回实现了 error 接口的具体类型实例,一般的错误可以使用这两个函数进行封装。遇到复杂的错误,可以自定义错误类型,只要实现 error 接口即可。
func Errorf(format string, a ...interface{}) error {
return errors.New(sprintf(format, a...))
}
func New(text string) error {
return &errorString{text}
}
错误处理的最佳实践
- 在多个返回值的函数中,error 通常作为函数最后一个返回值。
- 如果一个函数返回 error 类型变量,则先用 if 语句处理 error != nil 的异常场景,正常逻辑放到 if 语句块的后面,保持代码平坦。
- defer 语句应该放到 err 判断的后面,不然有可能产生 panic。
- 在错误逐级向上传递的过程中,错误信息应该不断地丰富和完善,而不是简单地抛出下层调用的错误。这在错误日志分析时非常有用和友好。
2.6.2 错误和异常
广义上的错误: 发生非期望的行为
狭义的错误: 发生非期望的已知行为,这里的已知是指错误的类型是预定义好的。
异常: 发生非期望的未知行为。这里的未知是指错误类型不在预定义的错误,程序编译器和运行时都没有及时将其捕获处理。
- 错误(errors)
- 未捕获错误-异常(untrapped error)
- 可捕获错误(trapped error)
- complie errors
- runtime errors
- logic errors
Go 程序错误:
- 一类是运行时错误(runtime errors),此类错误语言的运行时能够捕获,并采取措施——隐式或显式地抛出 panic。
- 一类是程序逻辑错误:程序执行结构不符合预期,但不会引发运行时错误。
Go 错误处理机制:
- 通过函数返回错误类型的值来处理错误。
- 通过 panic 打印程序调用栈,终止程序执行来处理错误。
error 和 panic 使用原则:
- 程序发生的错误导致程序不能容错继续执行,此时程序应该主动调用 panic 或有运行时抛出 panic
- 程序虽然发生错误,但是程序能够容错继续执行,此时应该使用错误返回值的方式处理错误,或者在可能发生运行时错误的非关键分支上使用 recover 捕获 panic。
2.7 底层实现
基于堆栈式的程序执行模型决定了函数是语言的一个核心元素。
研究底层实现由两种方法:
- 看语言编译器源码,分析其对函数的各个特性的处理逻辑。
- 另一种是反汇编,将可执行程序反汇编出来。
2.7.1 函数调用规约
Go 函数使用的是 caller-save 的模式,即由调用者负责保护寄存器,所以在函数的头尾不会出现 push ebp; mov esp ebp 这样的代码,相反其实在主调函数调用被调函数的前后有一个保存现场和恢复现场的动作。
主调函数保存和恢复现场的通用逻辑如下
// 开辟栈空间,压栈 PB 保存现场
SUBQ $x, SP // 为函数开辟栈空间
MOVQ BP, y(SP) // 保存当前函数 BP 到 y(SP)位置,y 为相对 SP 的偏移量
LEAQ y(SP), BP // 重置 BP,使其指向刚刚保存 BP 旧值得位置,这里主要是方便后续 BP 的恢复
// 弹出栈,恢复 BP
MOVQ y(SP), BP // 恢复 BP 的值为调用前的值
ADDQ $x, SP // 恢复 SP 的值为函数开始时的值
2.7.2 汇编基础
基于AT&T 风格的汇编格式,Go 编译器产生的汇编代码是一种中间抽象态,它不是对机器码的映射,而是和平台无关的一个中间态汇编描述。所以汇编代码中有些寄存器是真是的,有些是抽象的。
以下是抽象寄存器:
SB (Static base pointer): 静态地址寄存器,它和全局符号一起表示全局变量的地址。
FP (Frame pointer): 栈帧寄存器,该寄存器指向当前函数调用栈帧的栈底位置。
PC (Program counter): 程序计数器,存放下一条指令的执行地址,很少直接操作该寄存器,一般是 CALL、RET 等指令隐式的操作。
SP (Stack pointer): 栈顶寄存器,一般在函数调用前由主调函数设置 SP 的值对栈空间进行分配或回收。
Go 汇编简介
1)Go 汇编器采用 AT&T 风格的汇编,早期的实现来之 plan9 汇编器:源操作数在前,目的操作数在后。
2)Go 内嵌汇编和反汇编产生的代码并不是一一对应的,汇编编译器对内嵌汇编程序自动做了调整,主要差别就是增加了保护现场,以及函数调用前的保持 PC、SP 偏移地址重定位等逻辑。反汇编代码更能反映程序的真实执行逻辑。
3)Go 的汇编代码并不是和具体硬件体系结构的机器码一一对应的,而是一种半抽象的描述,寄存器可能是抽象的,也可能是具体的。