文章

golang学习记录(8)

函数

go语言中的函数支持普通函数、匿名函数、闭包函数、方法等

在go语言中函数是一等公民,函数可以作为参数、函数可以作为返回值、函数可以赋值给变量、函数可以作为闭包函数等

函数可以满足接口

1、函数的定义

函数的基本定义方式如下:

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

参数列表:参数列表可以为空,也可以有多个参数,多个参数之间用逗号分隔

返回值列表:返回值列表可以为空,也可以有多个返回值,多个返回值之间用逗号分隔,返回值列表不为空时,函数中必须有return

举个简单的例子:

1
2
3
func add(a, b int, c float32) (int, error) {
    return a + b, nil
}

在go语言中参数的传递时值传递,即传递的是值的副本,而不是值的引用,因此在函数中修改参数的值不会影响到原变量的值

但是关于切片的传递,由于切片的底层原理,在函数中修改切片的值会影响到原切片的值,因此在go语言中切片的传递需要注意。

当且切片发生扩容时,会返回一个新的切片,此时在函数中修改切片的值不会影响到原切片的值

除了上面讲到的函数定义的方法,还可以在返回列表中执行返回参数的名称(相当于定义一个参数名称),这个参数在函数体中不需要再进行定义

下面是一个例子

1
2
3
4
5
func add(a, b int) (sum int, err error) {
    sum = a + b
    return sum, err
    //也可以直接写return
}

2、函数的可变参数

函数在进行参数传递的时候没可能不知道要传多少个值,因此go语言中函数的参数列表中可以使用可变参数

可变参数的基本定义方式如下:

1
2
3
func 函数名(参数列表...类型) (返回值列表) {
    函数体
}

举一个简单的例子:

1
2
3
4
5
6
7
8
9
func add(a ...int)(sum int, err error) {
    for _, value := range a {
        sum += value
    }
}
c := 1
d := 2
sum, _ := add(a, b, 3, 4)
fmt.Println(sum)

其中a是一个int类型的切片,在函数中可以使用a[0]、a[1]等方式访问切片中的元,也可以通过for循环进行访问

3、函数一等公民特性

函数的一等公民特性,能够当作参数进行传递、作为返回值进行返回、赋值给变量等操作

大大提升了函数的灵活性和复用性

3.1、将函数作为变量赋值

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func add(a ...int)(sum int, err error) {
    for _, value := range a {
        sum += value
    }
}

func main(){
    //这里将add函数作为参数赋值给变量funcVar
    funcVar := add
    c := 1
    d := 2
    sum, _ := funcVar(a, b, 3, 4)
    fmt.Println(sum)
}

3.2、将函数作为返回值

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func cal(op string) func(){
    switch op {
    case "+":
        return func(){
            fmt.Println("这是加法")
        }

    case "-":
        return func(){
            fmt.Println("这是减法")
        }
    default:
        return func(){
            fmt.Println("这不是加法也不是减法")
        }
    }
}

//因为返回的是函数,所以在调用的时候需要加上()
cal("+")()

3.3、将函数作为参数进行传递

举个例子(没啥意义,只是说明用法):

1
2
3
4
5
6
7
8
9
10
11
func add(a, b int) int{
    fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}

cal(1, add)

//输出结果为sum is 3
//调用cal函数传递add函数成为myfunc, 然后1+2

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
//匿名函数作为参数进行传递
func add(a, b int) int{
    fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}

cal(1, func(a, b int){
    fmt.Printf("total is %d\n", a + b)
})

//输出结果为total is 3
//调用cal函数传递临时定义的匿名函数函数成为myfunc, 然后1+2


//匿名函数作为变量进行赋值
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}
localFunc := func(a, b int){
    fmt.Printf("local is %d\n", a + b)
}

cal(1, localFunc)

//输出结果为local is 3
//调用cal函数传递localFunc成为myfunc, 然后1+2,跟上边的效果是一样的

4、go中函数的闭包特性

有一个需求,希望有一个函数每次调用返回的结果值都是增加一次之后的值

实现这个需求通常的方法是设置一个全局变量,每次调用函数的时候,将全局变量的值加1,然后返回全局变量的值

全局变量方式实现:

1
2
3
4
5
6
7
8
9
var local int
func add() int{
    local += 1
    return local
}

for i := 0; i < 5; i++ {
    fmt.Println(add())
}

但是这种情况下会出现一个问题,想让local归零很难实现,而且被迫声明了一个全集变量

因此go中设计了闭包的特性,下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func auotoAdd() func() int {
    local := 0
    return func() int {
        return local
    }
}

nextFunc := auotoAdd()
for i := 0; i < 5; i++ {
    fmt.Println(nextFunc())
}
//输出结果为0 1 2 3 4
//想要归零的话,只需要重新调用auotoAdd()函数即可

根据上面的例子可以得出闭包的定义:是指一个函数能够访问其外部作用域中的变量,即使外部函数已经结束执行。

比如:在一个函数中的匿名函数能够访问这个函数中的局部变量,这个匿名函数称为闭包。

5、defer的应用场景

defer可以理解为其它语言中的finally,在函数执行完毕之后执行,通常用于释放资源、关闭文件、关闭数据库连接等操作

连接数据库、打开文件、开始锁等场景下,无论执行是否成功都要记得进行关闭操作,否则会造成资源泄漏等问题

1
2
3
4
5
6
7
8
// 一个在Java中的例子
try {
    // 可能会发生异常的代码
} catch (Exception e) {
    // 异常处理代码
} finally {
    // 无论是否发生异常,都会执行的代码
}

在Java中try和finally之间的距离可能很远,容易忘记关闭资源,而在go中可以使用defer来解决这个问题

在go中defer的使用方式如下:

1
2
3
4
var mu sync.Mutex

mu.lock()
defer mu.unlock() //defer后面的代码会在return之前执行

defer语句成对出现,可以防止忘记关闭资源,机制更好用,代码更简洁

1
2
3
4
5
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")
//输出结果为4 3 2 1

defer的执行顺序是先进后出的,类似于栈的概念

defer的应用可以修改返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在这个例子中,defer 推迟执行的是一个匿名函数
// deferReturn 函数被调用,准备返回值 10。
// 在返回之前,defer 推迟执行的匿名函数被执行。
// 匿名函数中,result 被增加 1,因此 result 的值从 10 变为 11。
// deferReturn 函数返回修改后的 result 值,即 11。
// 因此,尽管 deferReturn 函数的返回语句是 return 10,但由于 defer 推迟执行的函数在返回之前修改了 result 的值,所以最终的输出结果是 11。这也展示了在 Go 语言中,defer 可以修改函数的返回值。

// 搞不懂就看源码

func deferReturn() (result int){
    defer func(){
        result++
    }()
    return 10
}

ret := deferReturn()
fmt.Println(ret) //输出结果为11

6、go的error、recover和panic

在go语言中关于出错处理最重要的概念有三个:

1、error

2、panic

3、recover

go中关于语言错误处理的理念:不应该使用异常处理控制流程,而应该使用错误值。

其他语言在使用异常处理控制流程时,通常会使用try catch来捕获异常,类似于包住函数

在go语言中开发函数的人需要有一个返回值告诉调用者是否成功,go的设计者要求我们必须处理这个err,在go的代码中,会大量出现”if err != nil”这样的语句

go的设计者认为必须处理这个err,继续防御性编程

所以go中使用panic来处理错误,panic会中断当前的函数执行,然后在调用函数中查找是否有recover,如果有recover则会执行recover,否则会一直向上查找,直到找到recover或者程序崩溃

6.1、error

在go中error有专门的包,下面举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
    "fmt"
    "errors"
)

func A() (int, error) {
    return 1, errors.New("这是一个错误")
}

func main() {
    if _, err := A(); err!= nil {
        fmt.Println(err)
    }
}

6.2、panic

panic是go中的内置函数,这个函数会导致程序退出,使用的场景不是很多

再举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
    "fmt"
    "errors"
)

func A() (int, error) {
    panic("this is a panic")
    fmt.Println("能执行到这吗?")
    return 1, errors.New("这是一个错误")
}

func main() {
    if _, err := A(); err!= nil {
        fmt.Println(err)
    }
}

// 在上面的例子中,panic会导致程序退出,所以fmt.Println("能执行到这吗?")不会被执行,只会打印错误栈

panic会导致程序的退出,注意平时开发中不能随便使用,下面是它的一些应用场景:

在一个服务启动的过程中,开始必须要准备好一些依赖服务,日志文件是否存在、数据库是否能连接、配置文件有没有问题等等,准备好后才能启动服务

如果在进行服务启动检查的过程中,发现任何一项需求不能被满足,就主动调用panic,让程序退出

一旦服务器启动了,某行代码中不小心调用了panic,程序挂了就是非常严重的事故

在go中一些地方也会被动触发panic:

1、数组越界

2、空指针

……

为了能够应对被动触发panic的情况,go中提供了recover函数,recover函数可以捕获panic,并且让程序继续执行,下一节介绍recover函数

panic后会返回一个interface{}类型的值,这个值就是panic的值

6.3、recover

下面是一个recover使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在go中map的使用需要进行初始化,否则会报错,这是一个会被动触发panic的场景

func A() {
    defer func(){
        if err := recover(); err!= nil {
            fmt.Println("recoverd is A(): ", err)
        }
    }()
    var names map[string]string
    names["xiaoming"] = "go工程师"
    return 0, errors.New("这是一个错误")
}
// 上面的代码会输出recoverd is A(): assignment to entry nil map

还有一些使用的注意事项:

1、recover只能在defer函数中使用才能生效

2、defer需要在panic之前进行定义

3、recover处理异常后,逻辑并不会恢复到panic的点继续执行

4、多个defer会形成栈,后定义的defer会先执行

本文由作者按照 CC BY 4.0 进行授权