函数
函数对应操作序列,是程序的基本组成元素。
Go语言中的函数有具名和匿名之分:具名函数一般对应于包级的函数,是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。
方法是绑定到一个具体类型的特殊函数,Go语言中的方法是依托于类型的,必须在编译时静态绑定。
接口定义了方法的集合,这些方法依托于运行时的接口对象,因此接口对应的方法是在运行时动态绑定的。Go语言通过隐式接口机制实现了鸭子面向对象模型。
Go语言程序的初始化和执行总是从main.main函数开始的。
但是如果main包导入了其它的包,则会按照顺序将它们包含进main包里(这里的导入顺序依赖具体实现,一般可能是以文件名或包路径名的字符串顺序导入)。
如果某个包被多次导入的话,在执行的时候只会导入一次。
当一个包被导入时,如果它还导入了其它的包,则先将其它的包包含进来,然后创建和初始化这个包的常量和变量,再调用包里的init函数,如果一个包有多个init函数的话,调用顺序未定义(实现可能是以文件名的顺序调用),同一个文件内的多个init则是以出现的顺序依次调用(init不是普通函数,可以定义有多个,所以也不能被其它函数调用)。
最后,当main包的所有包级常量、变量被创建和初始化完成,并且init函数被执行后,才会进入main.main函数,程序开始正常执行。
下图是Go程序函数启动顺序的示意图:
要注意的是,在main.main函数执行之前所有代码都运行在同一个goroutine,也就是程序的主系统线程中。
因此,如果某个init函数内部用go关键字启动了新的goroutine的话,新的goroutine只有在进入main.main函数之后才可能被执行到。
Go 语言支持普通函数、匿名函数和闭包
Go 语言的函数属于“一等公民”,也就是说
- 函数本身可以作为值进行传递
- 支持匿名函数和闭包
- 函数可以满足接口
定义
函数是结构体编程的最小模块单元。
它将复杂的算法过程分解为若干较小任务,隐藏相关细节,使得程序结构更加清晰,易于维护。
函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。因此,函数还是代码复用和测试的基本单元
关键字 func 用于定义函数。Go 中的函数有些不太方便的限制,但也借鉴了动态语言的某些优点:
- 无须前置声明
- 不支持命名嵌套定义
- 不支持同名函数重载
- 不支持默认参数
- 支持不定长变参
- 支持多返回值
- 支持命名返回值
- 支持匿名函数和闭包
函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型
在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。
函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例。
当然,Go语言中每个类型还可以有自己的方法,方法其实也是函数的一种。
1
2
3
4
5
6
7
8
9
10
11
12 | func hello() {
println("hello, world!")
}
func exec(f func()) {
f()
}
func main() {
f := hello
exec(f)
}
|
第一类对象指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。最常见的用法就是匿名函数
从阅读和代码维护的角度来说,使用命名类型更加方便
| // 定义函数类型
type FormatFunc func(string, ...interface{}) (string, error)
// 如不使用命名类型,这个参数签名会长到没法看
func format(f FormatFunc, s string, a..interface{}) (string, error) {
return f(s,a...)
}
|
函数只能判断其是否为 nil,不支持其他比较操作
| func a() {}
func b() {}
func main() {
println(a == nil)
println(a == b) // 无效操作: a == b(func can noly be compared to nil)
}
|
| // 具名函数
func Add(a, b int) int {
return a+b
}
// 匿名函数
var Add = func(a, b int) int {
return a+b
}
|
从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存
建议命名规则:
在避免冲突的情况下,函数命名要本着精简短小、望文知意的原则
- 通常是动词和介词加上名词,例如 scanWords
- 避免不必要的缩写,printError 要比 printErr 更好一些
- 避免使用类型关键字,比如 buildUserStruct 看上去会很别扭
- 避免歧义,不能有多种用途的解释造成误解
- 避免只能通过大小写区分的同名函数
- 避免与内置函数同名,这会导致误用
- 避免使用数字,除非是特定专有名次,例如 UTF8
- 避免添加作用域提示前缀
- 统一使用 camel/pascal case 拼写风格
- 使用相同术语,保持一致性
- 使用习惯用语,比如 init 表示初始化,is/has 返回布尔值结果
- 使用反义词组命名行为相反的阿函数农户,比如 get/set、min/max 等
函数和方法的命名规则稍有些不同。方法通过选择符调用,且具备状态上下文,可使用更简短的动词命名
参数
Go 对参数的处理偏向保守,不支持有默认值的可选参数,不支持命名实参。
调用时,必须按签名顺序传递指定类型和数量的实参,就算以_
命名的参数也不能忽略
在参数列表中,相邻的同类型参数可合并
| func test(x, y int, s string, _ bool) * int {
return nil
}
func main() {
test(1, 2, "abc") // 错误: not enougn arguments in call to test
}
|
参数可视作函数局部变量,因此不能在相同层次定义同名变量。
| func add(x, y int) int {
x := 100 // 错误: no new variables on left side of :=
var y int // 错误: y redeclared in this block
return x + y
}
|
形参是指函数定义中的参数,实参则是函数调用时所传递的参数。
形参类似函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数等。
不管是指针、引用类型,还是其他类型参数,都是值拷贝传递。
区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | package main
import "fmt"
func test(x *int) {
fmt.Printf("pointer: %p, target: %v\n", &x, x) // 输出形参 x 的地址
}
func main() {
a := 0x100
p := &a
fmt.Printf("pointer: %p, target: %v\n", &p, p) // 输出实参 p 的地址
test(p)
}
|
输出:
| pointer: 0xc00010e018, target: 0xc000118000
pointer: 0xc00010e028, target: 0xc000118000
|
从输出结果可以看出,尽管实参和形参都指向同一目标,但传递指针时依然被复制
表面上看,指针参数的性能要更好一些,但实际上得具体分析。
被复制的指针会延长目标对象声明周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本
其实在栈上复制小对象只须很少的指令即可完成,远比运行时进行堆内存分配要快得多。
另外,并发编程也提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。
当然,如果复制成本很高,或需要修改原对象状态,自然使用指针更好。
如果函数参数过多,建议将其重构为一个复合结构类型,也算是变相实现可选参数和命名实参功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | package main
import (
"log"
"time"
)
type serverOption struct {
address string
port int
path string
timeout time.Duration
log *log.Logger
}
func newOption() *serverOption {
return &serverOption{ // 默认参数
address: "0.0.0.0",
port: 8080,
path: "/var/text",
timeout: time.Second * 5,
log: nil,
}
}
func server(option *serverOption) {}
func main() {
opt := newOption()
opt.port = 8085 // 命名参数设置
server(opt)
}
|
将过多的参数独立成 option struct,既便于扩展参数集,也方便通过 newOption 函数设置默认配置。
这也是代码复用的一种方式,避免多处调用时烦琐的参数配置。
变参
变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部
| package main
import "fmt"
func test(s string, a ...int) {
fmt.Printf("%T, %v\n", a, a) // 显示类型和值
}
func main() {
test("abc", 1, 2, 3, 4)
}
|
输出:
将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片
1
2
3
4
5
6
7
8
9
10
11
12 | package main
import "fmt"
func test(a ...int) {
fmt.Println(a)
}
func main() {
a := [3]int{10, 20, 30}
test(a[:]...) // 转换为 slice 后展开
}
|
既然变参是切片,那么参数复制的仅是切片自身,并不包括底层数组,也因此可修改原数据。
如果需要,可用内置函数 copy 复制底层数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | package main
import "fmt"
func test(a ...int) {
for i := range a {
a[i] += 100
}
}
func main() {
a := [3]int{10, 20, 30}
test(a[:]...)
fmt.Println(a)
}
|
输出:
当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果:
| func main() {
var a = []interface{}{123, "abc"}
Print(a...) // 123 abc
Print(a) // [123 abc]
}
func Print(a ...interface{}) {
fmt.Println(a...)
}
|
第一个Print调用时传入的参数是 a...
,等价于直接调用 Print(123, "abc")
。
第二个Print调用传入的是未解包的a,等价于直接调用 Print([]interface{}{123, "abc"})
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | package main
import "fmt"
func sum(nums ...int) {
fmt.Print(nums, " ")
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}
func main() {
sum(1, 2)
sum(1, 2, 3)
nums := []int{1, 2, 3, 4}
sum(nums...)
}
|
输出:
| [1 2] 3
[1 2 3] 6
[1 2 3 4] 10
|
返回值
有返回值的函数,必须有明确的 return 终止语句
| func test(x int) int {
if x > 0 {
return 1
} else if x < 0 {
return -1
}
} // 错误: missing return at end of function
|
除非有 panic,或者无 break 的死循环,则无须 return 终止语句
| func test(x int) int {
for {
break
}
} // 错误: missing return at end of function
|
借鉴自动态语言的多返回值模式,函数得以返回更多状态,尤其是 error 模式
| import "errors"
func div(x, y int) (int, error) {
if y == 0 {
return 0, errors.New("division by zero")
}
return x / y, nil
}
|
稍有不慎的是没有元组(tuple)类型,也不能用数组、切片接收,但可用_
忽略掉不想要的返回值。
多返回值可用作其他函数调用实参,或当作结果直接返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | func div(x, y int) (int, error) {
if y == 0 {
return 0, errors.New("division by zero")
}
return x/y, nil
}
func log(x int, err error) {
fmt.Println(x, err)
}
func test() (int, error) {
return div(5, 0) // 多返回值用作 return 结果
}
func main() {
log(test()) // 多返回值用作实参
}
|
Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。
在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13 | // 多个参数和多个返回值
func Swap(a, b int) (int, int) {
return b, a
}
// 可变数量的参数
// more 对应 []int 切片类型
func Sum(a int, more ...int) int {
for _, v := range more {
a += v
}
return a
}
|
命名返回值
不仅函数的参数可以有名字,也可以给函数的返回值命名:
| func Find(m map[int]int, key int) (value int, ok bool) {
value, ok = m[key]
return
}
|
对返回值命名和简短变量定义一样,优缺点共存。
| func paging(sql string, intdex int) (count int, pages int, err error) {
}
|
从这个简单的示例可看出,命名返回值让函数声明更加清晰,同时也会改善帮助文档和代码编辑器提示
命名返回值和参数一样,可当作函数局部变量使用,最后由 return 隐式返回
| func div(x, y int) (z int, err error) {
if y == 0 {
err = errors.New("division by zero")
return
}
z = x/y
return // 相当于"return z, err"
}
|
这些特殊的“局部变量”会被不同层级的同名变量遮蔽。
好在编译器能检查到此类状况,只要改为显式 return 返回即可
| func add(x, y int) (z int) {
{
z := x + y // 新定义的同名局部变量,同名遮蔽
return // 错误: z is shadowed during return (改为 return z 即可)
}
return
}
|
除遮蔽外,我们还必须对全部返回值命名,否则编译器会搞不清状况
| func test() (int, s string, e error) {
return 0, "", nil // 错误: cannot use 0(type int) as type string in return argument
}
|
显然编译器在处理 return 语句的时候,会跳过未命名返回值,无法准确匹配
如果返回值类型能明确表明其含义,就尽量不要对其命名
| func NewUser() (*User, error)
|
匿名函数
匿名函数是指没有定义名字符号的函数
除没有名字外,匿名函数和普通函数完全相同。
最大区别是,我们可在函数内部定义匿名函数,形成类似嵌套效果。
匿名函数可直接调用,保存到变量,作为参数或返回值
直接执行:
| func main() {
func(s string) {
println(s)
}("hello, world!")
}
|
赋值给变量
| func main() {
add := func(x, y int) int {
return x + y
}
println(add(1, 2))
}
|
作为参数:
| func test(f func()) {
f()
}
func main() {
test(func() {
println("hello, world!")
})
}
|
作为返回值:
| func test() func(int, int) int {
return func(x, y int) int {
return x + y
}
}
func main() {
add := test()
println(add(1, 2))
}
|
将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本的区别。
当然,编译器会为匿名函数生成一个“随机”符号名
普通函数和匿名函数都可作为结构体字段,或经通道传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | func testStruct() {
type calc struct { // 定义结构体类型
mul func(x, y int) int // 函数类型字段
}
x := calc{
mul: func(x, y int) int {
return x * y
}
}
println(x.mul(2, 3))
}
func testChannel() {
c := make(chan func(int, int) int, 2)
c <- func(x, y int) int {
return x + y
}
println((<-c)(1, 2))
}
|
不曾使用的匿名阿函数农户会被编译器当作错误:
| func main() {
func(s string) { // 错误: func literal evaluated but not used
println(s)
} // 此处并未调用
}
|
除闭包因素外,匿名函数也是一种常见重构手段。
可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。
相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引发外部污染,更加灵活。
没有定义顺序限制,必要时可剥离,便于实现干净、清晰的代码层次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 | package main
import (
"fmt"
"strings"
)
func Index(vs []string, t string) int {
for i, v := range vs {
if v == t {
return i
}
}
return -1
}
func Include(vs []string, t string) bool {
return Index(vs, t) >= 0
}
func Any(vs []string, f func(string) bool) bool {
for _, v := range vs {
if f(v) {
return true
}
}
return false
}
func All(vs []string, f func(string) bool) bool {
for _, v := range vs {
if !f(v) {
return false
}
}
return true
}
func Filter(vs []string, f func(string) bool) []string {
vsf := make([]string, 0)
for _, v := range vs {
if f(v) {
vsf = append(vsf, v)
}
}
return vsf
}
func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}
func main() {
var strs = []string{"peach", "apple", "pear", "plum"}
fmt.Println(Index(strs, "pear"))
fmt.Println(Include(strs, "grape"))
fmt.Println(Any(strs, func(v string) bool {
return strings.HasPrefix(v, "p")
}))
fmt.Println(All(strs, func(v string) bool {
return strings.HasPrefix(v, "p")
}))
fmt.Println(Filter(strs, func(v string) bool {
return strings.Contains(v, "e")
}))
fmt.Println(Map(strs, strings.ToUpper))
}
|
输出:
| 2
false
true
false
[peach apple pear]
[PEACH APPLE PEAR PLUM]
|
闭包
闭包是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。
这种说明太学术范儿了,很难理解,我们先看一个例子:
| func test(x int) func() {
return func() {
println(x)
}
}
func main() {
f := test(123)
f()
}
|
输出:
就这段代码而言,test 返回的匿名函数会引用上下文环境变量 x。
当该函数在 main 中执行时,它依然可正确读取 x 的值,这种现象就称作闭包
闭包是如何实现的?匿名函数被返回后,为何还能读取环境变量值?修改一下代码再看
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | package main
func test(x int) func() {
println(&x)
return func() {
println(&x, x)
}
}
func main() {
f := test(0x100)
f()
}
|
输出:
| 0xc00008e000
0xc00008e000 256
|
通过输出指针,我们注意到闭包直接引用了原环境变量。
分析汇编代码,你会看到返回的不仅仅是匿名函数,还包括所引用的环境变量指针。
所以说,闭包是函数和引用环境的组合体更加确切
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓“延迟求值”的特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | package main
func test() []func() {
var s []func()
for i := 0; i < 2; i++ {
s = append(s, func() { // 将多个匿名函数添加到列表
println(&i, i)
})
}
return s
}
func main() {
for _, f := range test() { // 迭代执行所有匿名函数
f()
}
}
|
输出:
| 0xc000014070 2
0xc000014070 2
|
对这个输出结果不必惊讶。很简单,for 循环复用局部变量 i,那么每次添加的匿名函数引用自然是同一变量。
添加操作仅仅是将匿名函数放入列表,并未执行。
因此,当 main 执行这些函数时,它们读取的是环境变量 i 最后一次循环时的值。不是 2,还能是什么?
解决方法就是每次用不同的环境变量或传参复制,让各自闭包环境各不相同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | package main
func test() []func() {
var s []func()
for i := 0; i < 2; i++ {
x := i // 每次循环都重新定义
s = append(s, func() {
println(&x, x)
})
}
return s
}
func main() {
for _, f := range test() { // 迭代执行所有匿名函数
f()
}
}
|
输出:
| 0xc000014070 0
0xc000014078 1
|
多个匿名函数引用同一环境变量,也会让事情变得更加复杂。
任何的修改行为都会影响其他函数取值,在并发模式下可能需要做同步处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | package main
func test(x int) (func(), func()) {
return func() {
println(x)
x += 10 // 修改环境变量
}, func() {
println(x) // 显示环境变量
}
}
func main() {
a, b := test(100)
a()
b()
}
|
输出:
闭包让我们不用传递参数就可读取或修改环境状态,当然也要为此付出额外代价。对于性能要求较高的场合,须慎重使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | package main
import "fmt"
func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
nextInt := intSeq()
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
newInts := intSeq()
fmt.Println(newInts())
}
|
输出:
延迟调用
语句 defer 向当前函数注册稍后执行的函数调用。
这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作.
| func main() {
f, err := os.Open("./main.go")
if err != nil {
log.Fatalln(err)
}
defer f.Close() // 仅注册,直到退出前才执行
...don something...
}
|
注意,延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。
参数值在注册时被复制并缓存起来。如对状态敏感,可改用指针或闭包
1
2
3
4
5
6
7
8
9
10
11
12
13 | package main
func main() {
x, y := 1, 2
defer func(a int) {
println("defer x, y = ", a, y) // y 为闭包引用
}(x) // 注册时复制调用参数
x += 100 // 对 x 的修改不会影响延迟调用
y += 200
println(x, y)
}
|
输出:
| 101 202
defer x, y = 1 202
|
延迟调用可修改当前函数命名返回值,但其自身返回值被抛弃
多个延迟注册按 FILO(先进后出)次序执行
| package main
func main() {
defer println("a")
defer println("b")
}
|
输出:
编译器通过插入额外指令来实现延迟调用执行,而 return 和 panic 语句都会终止当前函数流程,引发延迟调用。
另外,return 语句不是 ret 汇编指令,它会先更新返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | package main
func test() (z int) {
defer func() {
println("defer:", z)
z += 100 // 修改命名返回值
}()
return 100 // 实际执行次序,z=100, call defer, ret
}
func main() {
println("test:", test())
}
|
输出:
如果返回值命名了,可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值:
| func Inc() (v int) {
defer func(){ v++ } ()
return 42
}
|
其中defer语句延迟执行了一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量v,这种函数我们一般叫闭包。
闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。
闭包的这种引用方式访问外部变量的行为可能会导致一些隐含的问题:
| func main() {
for i := 0; i < 3; i++ {
defer func(){ println(i) } ()
}
}
// Output:
// 3
// 3
// 3
|
因为是闭包,在for迭代语句中,每个defer语句延迟执行的函数引用的都是同一个i迭代变量,在循环结束后这个变量的值为3,因此最终输出的都是3。
修复的思路是在每轮迭代中为每个defer函数生成独有的变量。可以用下面两种方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | func main() {
for i := 0; i < 3; i++ {
i := i // 定义一个循环体内局部变量i
defer func(){ println(i) } ()
}
}
func main() {
for i := 0; i < 3; i++ {
// 通过函数传入i
// defer 语句会马上对调用参数求值
defer func(i int){ println(i) } (i)
}
}
|
第一种方法是在循环体内部再定义一个局部变量,这样每次迭代defer语句的闭包函数捕获的都是不同的变量,这些变量的值对应迭代时的值。
第二种方式是将迭代变量通过闭包函数的参数传入,defer语句会马上对调用参数求值。
两种方式都是可以工作的。不过一般来说,在for循环内部执行defer语句并不是一个好的习惯,此处仅为示例,不建议使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | package main
import (
"fmt"
"os"
)
func main() {
f := createFile("/tmp/defer.txt")
defer closeFile(f)
writeFile(f)
}
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
if err != nil {
panic(err)
}
return f
}
func writeFile(f *os.File) {
fmt.Println("writing")
fmt.Fprintln(f, "data")
}
func closeFile(f *os.File) {
fmt.Println("closing")
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
|
输出:
误用
千万记住,延迟调用在函数结束时才被执行。
不合理的使用方式会浪费更多资源,甚至造成逻辑错误
案例: 循环处理多个日志文件,不恰当的 defer 导致文件关闭时间延长
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | func main() {
for i := 0; i < 10000; i++ {
path := fmt.Sprintf("./log/%d.txt", i)
f, err := os.Open(path)
if err != nil {
log.Println(err)
continue
}
// 这个关闭操作在 main 函数结束时才会执行,而不是当前循环中执行
// 这无端延长了逻辑结束时间和 f 的生命周期,平白多小号了内存等资源
defer f.Close()
...do something...
}
}
|
应该直接调用,或重构为函数,将循环和处理算法分离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | func main() {
do := func(n int) {
path := fmt.Sprintf("./log/%d.txt", n)
f, err = os.Open(path)
if err != nil {
log.Println(err)
continue
}
// 该延迟调用在此匿名函数结束时执行,而非 main
defer f.Close()
...do something...
}
for i := 0; i < 10000; i++ {
do(i)
}
}
|
性能要求高且压力大的算法,应避免使用延迟调用
以切片为参数
Go语言中,如果以切片为参数调用函数时,有时候会给人一种参数采用了传引用的方式的假象:因为在被调用函数内部可以修改传入的切片的元素。
其实,任何可以通过函数参数修改调用参数的情形,都是因为函数参数中显式或隐式传入了指针参数。
函数参数传值的规范更准确说是只针对数据结构中固定的部分传值,例如字符串或切片对应结构体中的指针和字符串长度结构体传值,但是并不包含指针间接指向的内容。
将切片类型的参数替换为类似reflect.SliceHeader
结构体就很好理解切片传值的含义了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | func twice(x []int) {
for i := range x {
x[i] *= 2
}
}
type IntSliceHeader struct {
Data []int
Len int
Cap int
}
func twice(x IntSliceHeader) {
for i := 0; i < x.Len; i++ {
x.Data[i] *= 2
}
}
|
因为切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据。
除了数据之外,切片结构还包含了切片长度和切片容量信息,这2个信息也是传值的。
如果被调用函数中修改了Len或Cap信息的话,就无法反映到调用参数的切片中,这时候我们一般会通过返回修改后的切片来更新之前的切片。
这也是为何内置的append必须要返回一个切片的原因。
递归调用
Go语言中,函数还可以直接或间接地调用自己,也就是支持递归调用。
Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。
每个goroutine刚启动时只会分配很小的栈(4或8KB,具体依赖实现),根据需要动态调整栈的大小,栈最大可以达到GB级(依赖具体实现,在目前的实现中,32位体系结构为250MB,64位体系结构为1GB)。
在Go1.4以前,Go的动态栈采用的是分段式的动态栈,通俗地说就是采用一个链表来实现动态栈,每个链表的节点内存位置不会发生变化。
但是链表实现的动态栈对某些导致跨越链表不同节点的热点调用的性能影响较大,因为相邻的链表节点它们在内存位置一般不是相邻的,这会增加CPU高速缓存命中失败的几率。
为了解决热点调用的CPU缓存命中率问题,Go1.4之后改用连续的动态栈实现,也就是采用一个类似动态数组的结构来表示栈。
不过连续动态栈也带来了新的问题:当连续栈动态增长时,需要将之前的数据移动到新的内存空间,这会导致之前栈中全部变量的地址发生变化。
虽然Go语言运行时会自动更新引用了地址变化的栈变量的指针,但最重要的一点是要明白Go语言中指针不再是固定不变的了(因此不能随意将指针保持到数值变量中,Go语言的地址也不能随意保存到不在GC控制的环境中,因此使用CGO时不能在C语言中长期持有Go语言对象的地址)。
因为,Go语言函数的栈会自动调整大小,所以普通Go程序员已经很少需要关心栈的运行机制的。
在Go语言规范中甚至故意没有讲到栈和堆的概念。
我们无法知道函数参数或局部变量到底是保存在栈中还是堆中,我们只需要知道它们能够正常工作就可以了。看看下面这个例子:
| func f(x int) *int {
return &x
}
func g() int {
x = new(int)
return *x
}
|
第一个函数直接返回了函数参数变量的地址——这似乎是不可以的,因为如果参数变量在栈上的话,函数返回之后栈变量就失效了,返回的地址自然也应该失效了。
但是Go语言的编译器和运行时比我们聪明的多,它会保证指针指向的变量在合适的地方。
第二个函数,内部虽然调用new函数创建了 *int
类型的指针对象,但是依然不知道它具体保存在哪里。
对于有C/C++编程经验的程序员需要强调的是:不用关心Go语言中函数栈和堆的问题,编译器和运行时会帮我们搞定;
同样不要假设变量在内存中的位置是固定不变的,指针随时可能会变化,特别是在你不期望它变化的时候。
错误处理
返古的错误处理方式,是 Go 被谈及最多的内容之一。
官方推荐的标准做法是返回 error 状态。
| func ScanIn(a...interface{}) (n int, err error)
|
标准库将 error 定义为接口类型,以便实现自定义错误类型
| type error interface {
Error() string
}
|
按惯例,error 总是最后一个返回参数。
标准库提供了相关创建函数,可方便地创建包含简单错误文本的 error 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | var errDivByZero = erros.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 {
return 0, errDivByZero
}
return x/y, nil
}
func main() {
z, err := div(5, 0)
if err == errDivByZero {
log.Fatalln(err)
}
println(z)
}
|
应通过错误变量,而非文本内容来判定错误类别
错误变量通常以 err 作为前缀,且字符串内容全部小写,没有结束标点,以便于嵌入到其他格式化字符串中输出
全局错误变量并非没有问题,因为它们可被用户重新赋值,这就可能导致结果不匹配。
不知道以后是否会出现只读变量功能,否则就只能依靠自觉了。
与 errors.New 类似的还有 fmt.Errorf,它返回一个格式化内容的错误对象
某些时候,我们需要自定义错误类型,以容纳更多上下文状态信息。
这样的话,还可基于类型做出判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 | package main
import (
"fmt"
"log"
)
type DivError struct { // 自定义错误类型
x, y int
}
func (DivError) Error() string { // 实现 error 接口方法
return "division by zero"
}
func div(x, y int) (int, error) {
if y == 0 {
return 0, DivError{x, y} // 返回自定义错误类型
}
return x / y, nil
}
func main() {
z, err := div(5, 0)
if err != nil {
switch e := err.(type) {
case DivError:
fmt.Println(e, e.x, e.y)
default:
fmt.Println(e)
}
log.Fatalln(err)
}
println(z)
}
|
自定义错误类型通常以 Error 为名称后缀。
在用 switch 按类型匹配时,注意 case 顺序。应将自定义类型放在前面,优先匹配更具体的错误类型
在正式代码中,我们不能忽略 error 返回值,应严格检查,否则可能会导致错误的逻辑状态。
调用多返回值函数时,除 error 外,其他返回值同样需要关注
以 os.File.Read 方法为例,它会同时返回剩余内和 EOF
大量函数和方法返回 error,使得待用代码变得很难看,一堆堆的检查语句充斥在代码行间。
解决思路有:
- 使用专门的检查函数处理错误逻辑(比如记录日志),简化检查代码
- 在不影响逻辑的情况下,使用 defer 延后处理错误状态(err 退化赋值)
- 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理
panic, recover
与 error 相比,panic/recover 在使用方法上接近 try/catch 结构化异常
| func panic(v interface{})
func recover() interface{}
|
比较有趣的是,它们是内置函数而非语句。
panic 会立即中断当前函数流程,执行延迟调用。
而在延迟调用函数中,recover 可捕获并返回 panic 提交的错误对象
| func main() {
defer func() {
if err := recover(); err != nil { // 捕获错误
log.Fatalln(err)
}
}()
panic("i am dead") // 引发错误
println("exit.") // 永不会执行
}
|
因为 panic 参数是空接口类型,因此可使用任何对象作为错误状态。
而 recover 返回结果同样要做转型才能获得具体信息。
无论是否执行 recover,所有延迟调用都会被执行。
但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | package main
import "log"
func test() {
defer println("test.1")
defer println("test.2")
panic("i am dead")
}
func main() {
defer func() {
log.Println(recover())
}()
test()
}
|
输出:
| test.2
test.1
2021/03/20 21:47:00 i am dead
|
连续调用 panic,仅最后一个会被 recover 捕获
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | func main() {
defer func() {
for {
if err := recover(); err != nil {
log.Println(err)
} else {
log.Fatalln("fatal")
}
}
}()
defer func() {
panic("you are dead") // 类似重新抛出异常(rethrow)
}() // 可先 recover 捕获,包装后重新抛出
panic("i am dead")
}
|
输出:
在延迟函数中 panic,不会影响后续延迟调用执行。
而 recover 之后 panic,可被再次捕获。另外,recover 必须在延迟调用函数中执行才能正常工作
| func catch() {
log.Println("catch:", recover())
}
func main() {
defer catch() // 捕获
defer log.Println(recover()) // 失败!
defer recover() // 失败!
panic("i am dead")
}
|
输出:
考虑到 recover 特性,如果要保护代码片段,那么只能将其重构为函数调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | func test(x, y int) {
z := 0
func() { // 利用匿名函数保护
defer func() {
if recover() != nil {
z = 0
}
}()
z = x / y
}()
println("x/y=", z)
}
func main() {
test(5, 0)
}
|
调试阶段,可使用 runtime/debug.PrintStack 函数输出完整调用堆栈信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | package main
import (
"runtime/debug"
)
func test() {
panic("i am dead")
}
func main() {
defer func() {
if err := recover(); err != nil {
debug.PrintStack()
}
}()
test()
}
|
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13 | goroutine 1 [running]:
runtime/debug.Stack(0x16fffff, 0x400, 0x20300000000000)
/usr/local/go/src/runtime/debug/stack.go:24 +0x9f
runtime/debug.PrintStack()
/usr/local/go/src/runtime/debug/stack.go:16 +0x25
main.main.func1()
/Users/nocilantro/Desktop/nocilantro/test.go:14 +0x45
panic(0x1083440, 0x10af068)
/usr/local/go/src/runtime/panic.go:965 +0x1b9
main.test(...)
/Users/nocilantro/Desktop/nocilantro/test.go:8
main.main()
/Users/nocilantro/Desktop/nocilantro/test.go:18 +0x5c
|
建议:
除非是不可恢复性、导致系统无法正常工作的错误,否则不建议使用 panic。
例如:文件系统没有操作权限,服务端口被占用,数据库未启动等情况
Go 语言的函数声明以 func 标识,后面紧接着函数名、参数列表、返回参数列表及函数体,具体形式如下:
| func 函数名(参数列表) (返回参数列表) {
函数体
}
|
参数列表:一个参数列表由参数变量和参数类型组成,例如:
| func foo(a int, b string)
|
其中参数列表中的变量作为函数的局部变量而存在
返回参数列表:可以是返回值类型列表,也可以是类似参数列表中变量名和类型名的组合。函数在声明有返回值时,必须在函数体中使用return
语句提供返回值列表
参数类型的简写:
在参数列表中,如有多个参数变量,则以逗号分割;如果相邻变量是同类型,则可以将类型省略。例如
| func add(a, b int) int {
return a + b
}
|
以上代码中,a
和b
的参数均为int
类型,因此可以省略a
的类型,在b
后面有类型说明,这个类型也是a
的类型
函数的返回值:
Go 语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,Go 语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误。示例代码如下
| conn, err := connectToNetwork()
|
在这段代码中,connectToNetwork
返回两个参数,conn
表示连接对象,err
返回错误
C/C++ 语言中只支持一个返回值,在需要返回多个数值时,则需要使用结构体返回结果,或者在参数中使用指针变量,然后在函数内部修改外部传入的变量值,实现返回计算结果。C++ 语言中为了安全性,建议在参数返回数据时使用“引用”替代指针
Go 语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更为方便
同一种类型返回值
1
2
3
4
5
6
7
8
9
10
11
12
13 | package main
import "fmt"
func typedTwoValues() (int, int) {
return 1, 2
}
func main() {
a, b := typedTwoValues()
fmt.Println(a, b)
}
// 1 2
|
纯类型的返回值对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义
带有变量名的返回值:
Go 语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型
命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。
下面代码中的函数拥有两个整型返回值,函数声明时将返回值命名为 a 和 b,因此可以在函数体中直接对函数返回值进行赋值。
在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | package main
import "fmt"
func namedRetValues() (a, b int) {
a = 1
b = 2
return
}
func main() {
a, b := namedRetValues()
fmt.Println(a, b)
}
// 2 1
|
示例:将秒解析为时间单位:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | package main
import "fmt"
const (
SecondsPerMinute = 60
SecondsPerHour = SecondsPerMinute * 60
SecondsPerDay = SecondsPerHour * 24
)
func resolveTime(seconds int) (day int, hour int, minute int) {
day = seconds / SecondsPerDay
hour = seconds / SecondsPerHour
minute = seconds / SecondsPerMinute
return
}
func main() {
fmt.Println(resolveTime(1000))
_, hour, minute := resolveTime(180)
fmt.Println(hour, minute)
day, _, _ := resolveTime(90000)
fmt.Println(day)
}
0 0 16
0 3
1
|
函数中的参数传递效果测试:
Go 语言中传入和返回参数在调用和返回时都使用值传递,这里需要注意的是指针
、切片
和map
等引用型对象指向的内容在参数传递中不会发生复制,而是将指针进行复制,类似于创建一次引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | package main
import (
"fmt"
"strings"
)
// 传递字符串的值
func removePrefix(str string) {
str = strings.TrimPrefix(str, "go")
}
func main() {
str := "go lalalal"
removePrefix(str)
fmt.Println(str)
}
// go lalalal
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | package main
import (
"fmt"
"strings"
)
// 传递字符串的引用
func removePrefix(str *string) {
*str = strings.TrimPrefix(*str, "go")
}
func main() {
str := "go lalalal"
removePrefix(&str)
fmt.Println(str)
}
lalalal
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 | package main
import "fmt"
type InnerData struct {
a int
}
type Data struct {
complax []int // 测试切片在参数传递中的效果
instance InnerData // 实例分配的 innerData
ptr *InnerData // 将 ptr 声明为 InnerData 的指针类型
}
// 值传递测试函数
func passByValue(inFunc Data) Data {
// 输出参数的成员情况
fmt.Printf("inFunc value: %+v\n", inFunc)
// 打印 inFunc 的指针
fmt.Printf("inFunc ptr: %p\n", &inFunc)
return inFunc
}
func main() {
// 准备传入函数的结构
in := Data{
complax: []int{1, 2, 3},
instance: InnerData{
5,
},
ptr: &InnerData{1},
}
// 输入结构的成员情况
fmt.Printf("in value: %+v\n", in)
// 输入结构的指针地址
fmt.Printf("in ptr: %p\n", &in)
// 传入结构体,返回同类型的结构体
out := passByValue(in)
// 输出结构的成员情况
fmt.Printf("out value: %+v\n", out)
// 输出结构的指针地址
fmt.Printf("out ptr: %p\n", &out)
}
// in value: {complax:[1 2 3] instance:{a:5} ptr:0xc000012060}
// in ptr: 0xc000062180
// inFunc value: {complax:[1 2 3] instance:{a:5} ptr:0xc000012060}
// inFunc ptr: 0xc000062210
// out value: {complax:[1 2 3] instance:{a:5} ptr:0xc000012060}
// out ptr: 0xc0000621e0
|
从运行结果中发现:
- 所有的 Data 结构的指针地址发生了变化,意味着所有的结构都是一块新的内存,无论是将 Data 结构传入函数内部,还是通过函数返回值传回 Data 都会发生复制行为
- 所有的 Data 将结构中的成员值都没有发生变化,原样传递,意味着所有参数都是值传递
- Data 结构的 ptr 成员在传递过程中保持一致,表示指针在函数参数值传递中传递的只是指针,不会复制指针指向的部分
函数变量:把函数作为值保存到变量中
在 Go 语言中,函数也是一种类型,可以和其他类型一样被保存在变量中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | package main
import "fmt"
func fire() {
fmt.Println("fire")
}
func main() {
var f func()
f = fire
f()
}
// fire
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | package main
import (
"fmt"
s "strings"
)
var p = fmt.Println
func main() {
p("Contains: ", s.Contains("test", "es"))
p("Count: ", s.Count("test", "t"))
p("HasPrefix: ", s.HasPrefix("test", "te"))
p("HasSuffix: ", s.HasSuffix("test", "st"))
p("Index: ", s.Index("test", "e"))
p("Join: ", s.Join([]string{"a", "b"}, "-"))
p("Repeat: ", s.Repeat("a", 5))
p("Replace: ", s.Replace("foo", "o", "0", -1))
p("Replace: ", s.Replace("foo", "o", "0", 1))
p("Split: ", s.Split("a-b-c-d-e", "-"))
p("ToLower: ", s.ToLower("TEST"))
p("ToUpper: ", s.ToUpper("test"))
p()
p("Len: ", len("hello"))
p("Char:", "hello"[1])
}
|
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | Contains: true
Count: 2
HasPrefix: true
HasSuffix: true
Index: 1
Join: a-b
Repeat: aaaaa
Replace: f00
Replace: f0o
Split: [a b c d e]
ToLower: test
ToUpper: TEST
Len: 5
Char: 101
|
示例:字符串的链式处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 | package main
import (
"fmt"
"strings"
)
// 移除前缀的处理函数
func removePrefix(str string) string {
return strings.TrimPrefix(str, "go")
}
// 移除后缀的处理函数
func removeSuffix(str string) string {
return strings.TrimSuffix(str, "r")
}
// 字符串处理函数,传入字符串切片和处理链
func stringProccess(list []string, chain []func(string) string) {
// 遍历每一个字符串
for index, str := range list {
// 第一个要处理的字符串
result := str
// 遍历每一个处理链
for _, proc := range chain {
// 输入一个字符串进行处理,返回数据作为下一个处理链的输入
result = proc(result)
}
// 将结果放回切片
list[index] = result
}
}
func main() {
// 待处理的字符串列表
list := []string{
"go scanner",
"go parser",
"go compiler",
"go printer",
"go formater",
}
// 处理函数链
chain := []func(string) string{
removePrefix,
removeSuffix,
strings.TrimSpace,
strings.ToUpper,
}
// 处理字符串
stringProccess(list, chain)
// 输出处理好的字符串
for _, str := range list {
fmt.Println(str)
}
}
// SCANNE
// PARSE
// COMPILE
// PRINTE
// FORMATE
|
匿名函数
Go 语言支持匿名函数,即在需要时使用函数时,再定义函数,匿名函数没有函数名,只有函数体,函数可以被作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式被传递。
匿名函数经常被用于实现回调函数、闭包等
匿名函数的定义格式如下:
| func (参数列表) (返回参数列表) {
函数体
}
|
匿名函数的定义就是没有名字的普通函数定义。
匿名函数可以在声明后调用,例如:
| func (data int) {
fmt.Println("hello", data)
}(100)
|
注意第 3 行"}
"后的(100)
,表示对匿名函数进行调用,传递参数为 100
匿名函数可以被赋值,例如:
| // 将匿名函数保存到 f() 中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用 f() 调用
f(100)
|
匿名函数的用途非常广泛,匿名函数本身是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。
下面的代码实现对切片的遍历操作,遍历中访问每个元素的操作使用匿名函数来实现。
用户传入不同的匿名函数体可以实现对元素不同的遍历操作,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | package main
import (
"fmt"
)
// 遍历切片的每个元素,通过给定函数进行元素访问
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
// 使用匿名函数打印切片内容
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}
|
匿名函数作为回调函数的设计在 Go 语言的系统包中也比较常见,strings 包中就有如下代码:
| func TrimFunc(s string, f func(rune) bool) string {
return TrimRigthFunc(TrimLeftFunc(s, f), f)
}
|
函数类型实现接口
函数和其他类型一样都属于“一等公民”,其他类型能够实现接口,函数也可以
有如下一个接口:
| // 调用器接口
type Invoker interface {
// 需要实现一个 Call() 方法
Call(interface{})
}
|
这个接口需要实现 Call() 方法,调用时会传入一个 interface{} 类型的变量,这种类型的变量表示任意类型的值
接下来,使用结构体进行接口实现
结构体实现 Invoker 接口的代码如下:
| // 结构体类型
type Struct struct {
}
// 实现 Invoker 的 Call
func (s *Struct) Call(p interface{}) {
fmt.Println("from struct", p)
}
|
将定义的 Struct 类型实例化,并传入接口中进行调用,代码如下:
| // 声明接口变量
var invoker Invoker
// 实例化结构体
s := new(Struct)
// 将实例话的结构体赋值到接口
invoker = s
// 使用接口调用实例化结构体的方法Struct.Call
invoker.Call("hello")
|
代码输出如下: