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会先执行