Funky's NoteBook

Go Core Dev Function

字数统计: 2,906阅读时长: 13 min
2019/01/17 67 Share

函数

  • 声明格式
1
2
3
func funcName(param-list)(result-list){
function body
}
  • 特点

    • 可以无传入参数和返回值(默认返回0)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    func A() {
    //do something
    }

    func A() int {
    // do something
    ...
    return 1
    }
    • 多个相邻的相同类型的参数可以使用简写模式
    1
    2
    3
    func add(a, b int) int {
    return a + b
    }
    • 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量被初始化为类型零值,最后的 return 可以不带参数名直接返回
    1
    2
    3
    4
    5
    6
    7
    // sum 相当于函数内的局部变量,被初始化为零
    func add(a, b int) (sum int) {
    sum = a + b
    return // return sum 的简写
    // 如果是 sum := a + b 则相当于声明一个 sum 变量命名返回变量 sum 覆盖,
    // 最后需要显式使用 return sum
    }
    • 不支持默认值参数
    • 不支持函数重载
    • 不支持函数嵌套,严格的讲是不支持命名函数的嵌套定义,但是支持嵌套匿名函数
    1
    2
    3
    4
    5
    6
    func add(a, b int) (sum int) {
    anonymous := func (x, y int) int {
    return x + y
    }
    return anonymous(a, b)
    }

多值返回

定义多值返回的返回参数列表需要使用()包裹

1
2
3
func swap(a, b int) (int, int){
return b, a
}

习惯用法:

如果多值返回值有错误类型,则一般将错误类型作为最后一个返回值。

不定参数

  • 所有不定参数类型必须是相同的
  • 不定参数必须是函数的最后一个参数
  • 不定参数在函数体内相当于切片,对切片的操作同样适合对不定参数的操作
  • 切片可以作为参数传递给不定参数,切片名后要加上 “…”
1
2
3
4
5
6
7
8
9
10
11
12
13
func sum (arr ...int) (sum int) {
for _, v := range arr {
sum += v
}
return
}
func main(){
slice := []int{1, 2, 3, 4}
array := [...]int{1, 2, 3, 4}
// 数组不可以作为实参传递给不定参数的函数
sum(slice...)
// sum(array...) 不允许
}
  • 形参为不定参数的函数和形参为切片的函数类型不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sum1(arr ...int) (sum int) {
for v := range arr {
sum += v
}
return
}

func sum2 (arr []int) (sum int) {
for v := range arr {
sum += v
}
return
}
func main() {
fmt.Printf("%T\n", sum1)
fmt.Printf("%T\n", sum2)
}

函数签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func add (a, b int) int {
return a + b
}

type Op func (int, int) int // 定义一个函数类型

func do(f Op, a, b int) int { //定义一个函数,第一个参数是函数类型Op
return f(a, b) // 函数类型变量可以直接用来进行函数调用
}
// 函数类型、map、slice、chan 一样,时间函数类型变量和函数名都可以当作指针变量,该指针指向函数代码的初始位置。
func main() {
a := do(add, 1, 2) //函数名 add 可以当做相同函数类型形参,不需要强制类型转换
fmt.Println(a)
}
  • 函数赋值
1
2
3
4
5
6
7
8
func sum (a, b int) int {
return a + b
}
func main () {
sum(3, 4)
f := sum
f(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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var sum = func (a, b int) int {
return a + b
}

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 {
case "add":
return func (a, b int) int {
return a + b
}
case "sub":
return func (a, b int) int {
return a - b
}
default:
return nil
}
}

func main () {
//匿名函数直接被调用
defer func () {
if err := recover(); err != nil {
fmt.Println(err)
}
}()

sum(1, 2)

// 匿名函数作为实参
doinput (func (x, y int) int {
return x + y
}, 1, 2)
opFunc := wrap("add")
re := opFunc(2, 3)
fmt.Printf("%d\n", re)
}

defer

Defer 关键字可以注册做个延迟调用,这些调用以先进后出(FILO)的顺序在函数返回前被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
defer func (){
println("first")
}()

defer func () {
println("second")
}()
println("function body")
}
// 输出结果
// function body
// second
// first

defer 后面必须是函数或方法的调用,不能是语句,否则会报错

defer 函数实参在其注册时通过值拷贝进去,后续对实参的操作不好影响 defer 语句的输出结果。

1
2
3
4
5
6
7
8
9
10
func f() int {
a := 0
defer func(i int) {
println("defer i=", i)
}(a)
a++
return a
}

// 输出: defer i=0

defer 语句必须先注册后才能执行,如果 defer 位于 return 之后,则 defer 因为没有注册,不会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main () {
defer func () {
println("first")
}()
a := 0
println(a)
return
defer func () {
println("second")
}()
}
//结果:
// 0
// first

主动调用 os.Exit(int) 退出进程时,defer 将不再执行(即使 defer 已经提前注册)

1
2
3
4
5
6
7
8
9
10
11
12
13
package main 
import "os"
func main () {
defer func() {
println("defer")
}()
println("func body")

os.Exit(1)
}
// 结果:
// func body
// exit status 1

defer 的好处是可以在一定程度上避免资源泄露,特别是在很多 return 语句,有很多资源场景需要关闭的时候,漏掉资源关闭的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func CopyFile(dst, src string) (w int 64, err error) {
src, err := os.Open(src)
if err != nil {
return
}
defer src.Close()

dst, err := os.Create(dst)

if err != nil {
return
}

defer dst.Close()

w, err = io.Copy(dst, src)
return
}

defer 语句位置不当可能导致 panic,一般 defer 语句放在错误检查语句之后。

defer 也有明显的副作用: defer 会推迟资源释放,因此 defer 尽量不要放到循环语句里,将大函数内部的 defer 语句单独拆分成一个小函数是一种很好的实践方式。

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
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)
// 此时 f、g 引用的闭包环境中的 a 不是同一个,而是 2 次函数调用产生的副本
println(f(1))
// 多次调用 f 引用的是同一个副本
println(f(1))
// g 中的 a 的值仍然是 1
println(g(1))
println(g(1))
}
/*
输出结果:
0xc00006e000 1
2
0xc00006e000 2
3
0xc00006e008 1
2
0xc00006e008 2
3
*/

f、g 引用的是不同的 a。

如果函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。

使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。

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
var (
a = 0
)
func fa () func(i int) int {
return func (i int) int {
println(&a, a)
a = a + i
return a
}
}

func main() {
f := fa() // f 引用的外部的闭包环境包括全局变量 a
g := fa()
// 此时 f、g 引用的闭包环境中的 a 是同一个
println(f(1))
println(g(1))
println(g(1))
println(g(1))
}
/* 结果
0x10d5af0 0
1
0x10d5af0 1
2
0x10d5af0 2
3
0x10d5af0 3
4
*/

同一个函数返回的多个闭包共享该函数的局部变量。

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
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)

// s、k 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值
s, k := fa(0)

// f、g 和 s、k 引用不同的闭包变量,这是由于 fa 每次调用都要重新分配形参

println(f(1), g(2))
println(s(1), k(2))
}
/*
0xc00006e000 0
0xc00006e008 0
0xc00006e000 1
0xc00006e000 -1
1 -1
0xc00006e008 1
0xc00006e008 -1
1 -1
*/

闭包的价值

减少全局变量,函数调用过程中隐式传递共享变量,但是这种方式的坏处是不够直接,不够清晰,除非是非常有价值的地方,一般不建议用闭包。

对象是附有行为的数据,而闭包是附有数据的行为,类在定义时显式集中定义了行为,但是闭包中的数据没有显示的集中声明的地方,这种数据和行为耦合的模型不是一种推荐的编程模型,仅仅锦上添花,不是不可缺少。

panic & recover

这两个内置函数用于处理 go 运行时错误。panic 用于主动抛出错误,recover 用来捕获 panic 抛出的错误。

  • panic

发生 panic 后,程序会从调用调用 panic 函数位置或发生 panic 地方立即返回,逐层向上执行 defer 语句,然后逐层打印函数调用堆栈,直到被 recover 捕获或运行到最外层函数而退出。

panic 函数参数为一个空接口类型 interface {},所以任意类型的变量都可以传递给 panic,调用 panic 的方法非常简单:panic(xxx)

panic 不但可以在函数正常流程中抛出, 在 defer 逻辑里也可以再次调用 panic 或抛出 panic。defer 里面的 panic 能够被后续执行的 defer 捕获。

  • recover

recover() 用于捕获 panic,阻止 panic 继续向上传递。recover() 和 defer 一起使用,但是 recover() 只有在 defer 后面的函数体内被直接调用才能捕获 panic 终止异常,否则返回 nil,异常继续向外传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 捕获失败案例
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 才能被捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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")
}

包中的 init 函数引发的 panic 只能在 init 函数中捕获,在 main 中无法捕获,原因是 init 函数先于 main 执行,函数并不能捕获内部新启动的 goroutine 所抛出的 panic。

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 (
"fmt"
"time"
)

func do() {
// 这里并不能捕获 da 函数中的 panic
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
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)
}
}
func main() {
do()
}

使用场景:

  • 程序遇到无法正常执行下去的错误,主动调用 panic 结束程序运行
  • 调试程序时,主动调用 panic 实现 快速退出,panic 打印出的堆栈能够更快速定位错误。
CATALOG
  1. 1. 函数