前言:Go语言函数属于“first-class”也就是说Go语言函数有以下特性:

  • 函数本身可以作为值进行传递
  • 支持匿名函数和函数闭包(closure)
  • 函数满足接口(interface)

    函数声明

    普通函数

    Go语言函数声明以func标识,后面紧跟函数名、参数列表、返回参数列表及函数体。格式如下:
    1
    2
    3
    func 函数名(参数列表)(返回参数列表) {
    函数体
    }

函数名规则:由字母、下划线、数字组成,其中,函数名第一个字母不能是数字,在同一个包内,函数名称不能重名。

函数返回值

1.同种类型的返回值
如果返回值是同种类型,则可以使用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型,使用return语句返回的时候,列表值得循序需要和函数声明的返回值类型一致。

1
2
3
4
5
6
func test() (int, int) {
return 1, 2
}

a, b := test()
fmt.Println(a, b)

纯类型的返回值对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义,所以可以使用带有变量名的返回值。
2.带有变量名的返回值
Go 语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。

1
2
3
4
5
func namedRetValue() (a, b int) {
a = 1
b = 2
return
}

当函数使用命名返回值时,可以在 return 中不填写返回值列表,如果填写也是可行的。下面代码的执行效果和上面代码的效果一样。

1
2
3
4
5
func namedRetValue() (a, b int) {
a = 1
b = 2
return a, b
}

注意: Go语言中传入和返回参数在调用和返回的时候都使用的是值传递,这里需要注意的是指针、数组、切片和map等引用型对象指向的内容在参数传递中不发生复制,而是将指针进行复制,类似创建一次新的引用。

函数变量

在 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() {
//声明一个函数变量f
var f func()
f = fire
f()
}

将变量f声明为 func() 类型,此时 f 就被俗称为“回调函数”,此时 f 的值为 nil。

匿名函数

匿名函数没有函数名,只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量的方式被传递。匿名函数经常被用于实现回调函数、闭包等。
匿名函数的格式:

1
2
3
func(参数列表)(返回参数列表){
函数体
}

1.在定义时调用匿名函数
匿名函数可以在声明后调用:

1
2
3
func(data int){
fmt.Println(data)
}(100)

}后面的(100)就是对匿名函数的调用,传递参数为100
2.将匿名函数赋值给变量
匿名函数体可以被赋值:

1
2
3
4
5
f := func(data int){
fmt.Println(data)
}
//使用f()调用
f(100)

3.匿名函数作为回调函数
对切片进行遍历操作,遍历访问每个元素的操作可以使用匿名函数来实现,这样传入不同的匿名函数体可以实现对元素的不同的遍历操作:

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() {
//使用匿名函数实现打印切片元素
a := []int{1, 2, 3, 4}
visit(a, func(v int){
fmt.Println(v)
})
}

4.使用匿名函数实现操作的封装
下面这段代码将匿名函数作为 map 的键值,通过命令行参数动态调用匿名函数,代码如下:

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
package main

import (
"fmt"
"flag"
)

var skillPara = flag.String("skill", "", "skill to perform")

func main() {
flag.Parse()

//初始化一个map映射
var skill = map[string]func() {
"fire": func() {
fmt.Println("chiken fire")
},
"run": func() {
fmt.Println("soldier run")
},
"fly": func() {
fmt.Println("angel fly")
},
}

if f, ok := skill[*skillPara]; ok {
f()
} else {
fmt.Println("skill not found")
}
}

函数类型实现接口

函数类型实现接口实际就是把函数做为接口来调用

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
package main

import "fmt"

//调用接口
type Invoker interface {
//实现一个Call方法
Call(interface {})
}

//结构体类型
type Struct struct {
}

//实现Invoker的Call
func (f FuncCaller) Call(p interface{}) {
//调用f本体
f(p)
}

func main() {
//声明接口变量
var invoker Invoker
//实例化结构体
s := new(Struct)
//将实例化的结构体赋值到接口
invoker = s
//使用接口调用实例化结构体的方法Struct.Call
invoker.Call("hello")
//将匿名函数转换为FuncCaller类型,再赋值给接口
invoker = FuncCaller(func(v interface {}) {
fmt.Println("from function", v)
})

//使用接口调用FuncCaller.Call,内部会调用函数本体
invoker.Call("hello")
}

函数闭包

闭包是引用了外部变量的函数,被引用的变量和函数一同存在,即使离开了变量的环境也不会被删除或者被释放,在闭包中可以继续使用该变量。

1
函数 + 应用环境 = closure

同一个函数和不同的引用环境组合,可以形成不同的闭包实例,如下图所示:
闭包与函数引用
一个函数就像是一个结构体一样,可以被实例化,函数本身是不存储任何信息,只有与引用环境结合形成的闭包才具有“记忆性”,函数是编译器静态的概念,闭包是运行期动态的概念。

在闭包内部修改被引用的变量

闭包对它的作用域上的变量的引用可以进行修改,修改引用的变量就会对变量进行实际修改:

1
2
3
4
5
6
7
8
9
10
11
//准备一个字符串
str := "Hello World"

//创建一个匿名函数
f := func(){
//匿名函数中访问str
str = "hello dude"
}

//调用匿名函数
f()

闭包的记忆效应

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包中捕获的变量,变量会跟随闭包什么周期一直存在,闭包本身就如同变量一样具有了记忆性。
累加器的实现:

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 Accumulate(value int) func() int {
//返回一个闭包
return func() int {
value++
return value
}
}

func main() {
//创建一个累加器,初始值为1
accumulator := Accumulate(1)

//累加并进行打印
fmt.Println(accumulator())
fmt.Println(accumulator())
}

闭包实现生成器

闭包的记忆性可以用于实现设计模式中的工厂模式的生成器
玩家生成器的实现:

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 playerGen(name string) func() (string, int) {
//角色血量
hp := 150
//返回创建的闭包
return func() (string, int) {
//将变量引用到闭包
return name, hp
}
}

func main() {
//创建一个玩家生成器
generator := playerGen("high noon")
//返回玩家的姓名个血量
name, hp = generator()
fmt.Println(name, hp)
}

可变参数

可变参数就是参数的数量不固定的函数形式,就像Python中的*args*kws

1
2
3
func 函数名(固定参数列表, value ... type)(返回参数列表){
函数体
}

可变参数说明:

  • 可变参数一般放置在函数列表的末尾,前面是固定参数列表,当没有固定参数的时候,所有的变量将是可变参数。
  • value是可变参数变量,类型为type,也就是拥有多个type元素的type类型切片,value和type之间有...三个点组成。
  • type为可变参数的类型,当type是interface{}的时候,传入的参数可以是任意类型。

fmt包中的例子
可变参数的形式有两种:一种是所有的参数都是可变参数的形式,如: fmt.println(),以及部分是可变参数的形式,如:fmt.Printf(),可变参数只能出现在参数的后半部分,因此不可变参数只能放在参数的前半部分。
1. fmt.Println()
fmt.Println()函数声明如下:

1
2
3
func Println(a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, a...)
}

2. fmt.Printf()
fmt.Printf()函数的第一个参数是参数列表。后面的参数时可变参数,fmt.Printf()函数格式如下:

1
2
3
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.stdout, format, a...)
}

fmt.Printf() 函数在调用时,第一个函数始终必须传入字符串,对应参数是 format,后面的参数数量可以变化。

defer延迟函数

Go 语言的 defer 语句会将其后面跟随的语句进行延迟处理。在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main 

import "fmt"

func main() {
fmt.Println("defer begin")
//将defer放入延迟调用栈
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
//最后一个放入,位于栈顶,最先调用
fmt.Println("defer end")
}

输出:

1
2
3
4
5
defer begin
defer end
3
2
1

上面的输出代码可以看出,在执行代码的时候defer语句会将被延迟执行的函数或者语句放入栈,并且不影响正常的函数运行。

*使用defer在函数退出时释放资源

处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。
defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。

1. 使用defer延迟并发解锁

1
2
3
4
5
func readValue(key string) int {
valueByKeyGuard.Lock()
defer valueByKeyGuard.Unlock() //defer后面的语句不会马上调用,而是延迟到函数结束时调用
return valueByKey[key]
}

2. 使用defer延迟释放文件句柄
文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源。
在下面的例子中将实现根据文件名获取文件大小的函数,函数中需要打开文件、获取文件大小和关闭文件等操作。由于每一步系统操作都需要进行错误处理,而每一步处理都会造成一次可能的退出,因此就需要在退出时释放资源,而我们需要密切关注在函数退出处正确地释放文件资源。参考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//根据文件名获取文件的大小
func fileSize(filename string) int64 {
file, err := os.Open(filename)
if err != nil {
return 0
}

info, err = file.Stat()
if err != nil {
file.close
return 0
}

size := info.Size()
file.Close()

return size
}

使用defer延迟进行代码的简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func fileSize(filename string) int64 {
file, err := os.Open(filename)
if err != nil {
return 0
}

defer file.Close()

info, err := file.Stat()
if err != nil {
//defer机制触发,调用Close关闭文件
return 0
}

size := info.Size()

//defer机制触发,调用Close关闭文件
return size
}

异常处理

Go 语言的错误处理思想及设计包含以下特征:

  • 一个可能造成错误的函数,需要返回值中返回一个错误接口(error)。如果调用是成功的,错误接口将返回 nil,否则返回错误。
  • 在函数调用后需要检查错误,如果发生错误,进行必要的错误处理。

错误接口的定义格式

error是Go系统声明的接口类型,格式如下:

1
2
3
type error interface {
Error() string
}

Error()方法返回错误的具体描述,使用者可以通过字符串知道发生了什么错误。

自定义错误

返回错误前,需要定义会产生哪些可能错误,在Go语言中,使用errors包进行错误的定义:

1
var err = errors.New("This is an error")

错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用 errors.New 返回。

宕机(panic)

宕机不是一件很好的事情,可能造成体验停止、服务中断,就像没有人希望在取钱时遇到 ATM 机蓝屏一样。但是,如果在损失发生时,程序没有因为宕机而停止,那么用户将会付出更大的代价,这种代价可以是金钱、时间甚至生命。因此,宕机有时是一种合理的止损方法。

手动触发宕机

Go语言可以实现手动触发宕机,让程序崩溃。Go 语言程序在宕机时,会将堆栈和 goroutine 信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置。如果在编译时加入的调试信息甚至连崩溃现场的变量值、运行状态都可以获取,那么如何触发宕机呢?例如下面的代码:

1
2
3
4
5
package main 

func main() {
panic("crash")
}

代码运行崩溃并输出如下:

1
2
3
4
5
panic: crash

goroutine 1 [running]:
main.main()
F:/src/tester/main.go:5 +0x6b

上述代码使用了内置函数panic()手动造成程序崩溃,panic()声明如下:

1
func panic(v interface{})

宕机恢复(recover)

无论是代码运行错误由 Runtime 层抛出的 panic 崩溃,还是主动触发的 panic 崩溃,都可以配合 defer 和 recover 实现错误捕捉和恢复,让代码在发生崩溃后允许继续运行。
在其他语言里,宕机往往以异常的形式存在。底层抛出异常,上层逻辑通过 try/catch 机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。
Go 没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,那么 recover 的宕机恢复机制就对应 try/catch 机制。

让程序在崩溃时继续执行

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
package main

import (
"fmt"
"runtime"
)

//崩溃时需要传递的上下文信息
type panicContext struct {
function string //所在函数
}

func ProtectRun(entry func()) {
//延迟处理函数
defer func() {
//发生宕机时,获取panic传递的上下文并打印
err := recover()
witch err. (type) {
case runtime.Error:
fmt.Println("runtime error:", err)
default:
fmt.Println("error:", err)
}
}()

entry()
}

func main() {
fmt.Println("运行前")
// 允许一段手动触发的错误
ProtectRun(func() {
fmt.Println("手动宕机前")
// 使用panic传递上下文
panic(&panicContext{
"手动触发panic",
})
fmt.Println("手动宕机后")
})
// 故意造成空指针访问错误
ProtectRun(func() {
fmt.Println("赋值宕机前")
var a *int
*a = 1
fmt.Println("赋值宕机后")
})
fmt.Println("运行后")
}

panic & recover

panic 和 defer 的组合有如下特性:

  • 有 panic 没 recover,程序宕机。
  • 有 panic 也有 recover 捕获,程序不会宕机。执行完对应的 defer 后,从宕机点退出当前函数后继续执行。

提示
虽然 panic/recover 能模拟其他语言的异常机制,但并不建议代表编写普通函数也经常性使用这种特性。
在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛直到程序整体崩溃。
如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。