0%

Go语言学习笔记4-函数

[TOC]

函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,其可以提高应用的模块性和代码的重复利用率。

Go 语言支持普通函数、匿名函数和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。

Go 语言的函数属于“一等公民”(first-class),也就是说:

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

Go语言函数声明(函数定义)

普通函数需要先声明才能调用,一个函数的声明包含参数和函数名等,编译器通过声明才能了解函数应该怎样在调用代码和函数体之间传递参数和返回值

普通函数的声明形式

Go语言的函数声明以func 标识,后面紧接着函数名、参数列表、返回参数列表及函数体,具体形式如下:

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

下面对各个部分进行说明:

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

    包(package) 是Go源码的一种组织方式,一个包可以认为是一个文件夹

  • 参数列表:一个参数由参数变量和参数类型组成,例如

    1
    func foo(a int, b string)

    其中,参数列表中的变量作为函数的局部变量而存在。

  • 返回参数列表:可以是返回值类型列表,也可以是类似参数列表中变量名和类型名的组合。函数在声明有返回值时,必须在函数体中使用return 语句提供返回值列表

  • 函数体:能够被重复调用的代码片段

参数类型的缩写

在参数列表中,如果有多个参数变量,则以逗号,分隔;如果相邻变量时同类型,则可以将类型省略,例如:

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

以上代码中,a 和 b 的参数类型均是 int 类型,则可以省略 a 的类型,在 b 后面有类型说明,这个类型也是 a 的类型

函数的返回值

Go语言支持多返回值,多返回值能方便地取得函数执行后的多个返回参数,Go语言经常使用多返回值中的最后一个返回参数返回函数中可能发生的错误。示例如下:

1
conn, err := connectToMysql()

在这段代码中,connectToNetwork 返回两个参数,conn 表示连接对象,err 返回错误。

Go 语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更为方便。

同一类型返回值

如果返回值是同一类型,则用括号将多个返回值类型括起来,用逗号分割每个返回值的类型

使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致,示例如下:

1
2
3
4
func foo()(int, int) {
return 1, 2
}
a, b := foo()

带有变量名的返回值

Go 语言支持对返回值进行命名,这样返回值就可以和参数一样拥有参数变量名和类型

命名的返回值变量的默认值为类型的默认值,即数值为 0 ,字符串为空字符串,布尔值为false,指针为 nil 等

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

调用函数

函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行。调用前的函数局部变量都会被保存起来不会丢失;被调用的函数结束后,恢复到被调用函数的下一行继续执行代码,之前的局部变量也能继续访问。

函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。

Go语言的函数调用格式如下:

1
返回值变量列表 = 函数名(参数列表)

下面是对各个部分的说明:

  • 函数名:需要调用的函数名。
  • 参数列表:参数变量以逗号分隔,尾部无须以分号结尾。
  • 返回值变量列表:多个返回值使用逗号分隔。

函数示例-将秒转为具体时间

在本例中,使用一个数值表示时间中的“秒”值,然后使用resolveTime()函数将传入的秒数转换为天、小时和分钟等时间单位。

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

import "fmt"

var SecondsPerMinute = 60
var SecondsPerHour = 60 * SecondsPerMinute
var SecondsPerDay = 24 * SecondsPerHour

func main() {
fmt.Println(resolveTime(1000))

_, hour, minute := resolveTime(1000)
fmt.Println(hour, minute)

day, _, _ := resolveTime(1000)
fmt.Println(day)
}

func resolveTime(seconds int) (day, hour, minutes int) {
day = seconds / SecondsPerDay
hour = seconds / SecondsPerHour
minutes = seconds / SecondsPerMinute

return
}

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

函数变量-把函数作为值保存到变量中

在Go语言中,函数也是一种类型,可以和其他类型一样被保存在变量中。

1
2
3
4
5
6
7
8
9
func main() {
var f func()
f = foo
f()
}

func foo(){
fmt.Println("foo func")
}

Go语言匿名函数

Go 语言支持匿名函数,即在需要使用函数时在定义函数,匿名函数没有函数名,只有函数体,函数可以被作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式被传递

匿名函数经常被用于实现回调函数、闭包等

定义一个匿名函数

匿名函数的定义格式如下:

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

匿名函数的定义就是没有名字的普通函数定义

在定义时调用匿名函数

匿名函数可以在声明后调用:

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

将匿名函数赋值给变量

匿名函数体可以被赋值:

1
2
3
4
f := func(data int) {
fmt.Println(data)
}
f(100)

匿名函数的用途非常广泛,匿名函数本身是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。

匿名函数用作回调函数

下面的代码实现对切片的遍历操作,遍历中访问每个元素的操作使用匿名函数来实现。用户传入不同的匿名函数体可以实现对元素不同的遍历操作,代码如下:

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 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 包中就有如下代码:

1
2
3
func TrimFunc(s string, f func(rune) bool) string {
return TrimRightFunc(TrimLeftFunc(s, f), f)
}

Go语言函数类型实现接口

函数和其他类型一样都属于“一等公民”,其他类型能够实现接口,函数也可以,本节将分别对比结构体函数实现接口的过程。

TODO

Go语言闭包

闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此简单的说:

1
函数 + 引用环境 = 闭包

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

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

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

1
2
3
4
5
6
7
8
9
10
str := "hello"

// 创建一个匿名函数
foo := func() {

// 匿名函数中访问str
str = "world"
}
foo()
fmt.Println(str); // world

闭包的记忆效应

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func addValue(value int) func() int {
return func() int {
value++
return value
}
}
func main() {
// 创建一个累加器,初始值1
value := addValue(1)
fmt.Println(value()) // 2
fmt.Println(value()) // 3
// 打印累加器的函数地址
fmt.Printf("%p\n" , value)

// 创建一个累加器,初始值为1
otherValue := addValue(1)
fmt.Println(otherValue()) // 2
}

value 与 otherValue的函数地址不同,因此它们是两个不同的闭包实例

闭包的记忆效应进程被用于实现类似于设计模式中工厂模式的生成器。

Go语言可变参数(变参函数)

所谓可变参数,是指参数数量不固定的函数形式。
Go语言支持可变参数特性,函数声明和调用时没有固定数量的参数,同时也提供了一套方法进行可变参数的多级传参

Go语音的可变参数格式如下:

1
2
3
func 函数名(固定参数列表,v ... T)(返回参数列表){
函数体
}
  • 可变参数一般被放置在函数列表的末尾,前面是固定参数列表,当没有固定参数时,所有变量将是可变参数
  • v 为可变参数变量,类型为[]T,也就是拥有多个T 元素的 T 类型的切片v 和 T 之前由...组成
  • T 为可变参数的类型,当T 为interface{}时,传入的可以使任意类型

fmt包中的例子

可变参数有两种类型:所有参数都是可变参数的形式,如fmt.Println,以及部分是可变参数的形式,如 fmt.Printf,可变参数只能出现在参数的后半部分,因此不可变的参数只能放在参数的前半部分。

所有参数都是可变参数:fmt.Println

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

fmt.Println 在使用时,传入的值类型不收限制,例如:

1
fmt.Println(1, 2, 3, "string", true);

部分参数是可变参数: 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,后面的参数数量可以变化

Go语音defer(延迟执行语句)

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

多个延迟执行语句的处理顺序

1
2
3
4
5
6
7
8
fmt.Println("第一行")
defer fmt.Println("第二行")
defer fmt.Println("第三行")
fmt.Println("最后一行")
// 第一行
// 最后一行
// 第三行
// 第二行
  • 代码的延迟顺序与最终执行顺序是反向的
  • 延迟调用是在 defer 所在函数结束时执行,函数结束可以是正常返回,也可以是出错时返回

使用延迟执行语句在函数退出时释放资源

处理业务或逻辑中涉及成对的操作是一件比较繁琐的事情,比如打开和关闭文件、和解锁接受请求和回复请求、加锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。

defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理释放资源的问题

使用 defer 延迟释放资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func fileSize(filename string) int64 {
f, err := os.Open(filename)
if err != nil{
return 0
}
// 延迟调用close ,此时close 不会被调用
defer f.Close()

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

defer 后的语句(f.Close())将会在函数返回前被调用,自动释放资源

Go语言处理运行时错误

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

  • 一个可能造成错误的函数,需要返回值中返回一个错误接口(error) 。如果调用是成功的,错误接口将返回nil , 否则返回错误

  • 在函数调用后需要检查错误,如果发生错误,需要进行必要的错误处理

Go 语言没有类似 Java或 .NET 中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做。Go 语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源。同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。

Go 语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数。同时,Go 语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。

Go语言宕机(panic),程序终止执行

手动触发宕机

Go 语言中可以在程序中手动触发宕机,让程序崩溃,这样使开发者可以及时的发现错误,同时减少可能的损失

Go 语言宕机时,会将堆栈和goroutine 信息输出到控制台,所以宕机也可以方便的知晓发生错误的位置

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

func main() {
panic("crash")
/**
panic: crash

goroutine 1 [running]:
main.main()
/Users/zhimma/go/src/awesomeProject/0312.go:4 +0x39
*/
}

以上代码中只用了一个内建的函数 panic() 就可以造成崩溃,panic() 的声明如下:

1
func panic(v interface{})

panic() 的参数可以是任意类型,后文将提到的 recover 参数会接收从 panic() 中发出的内容。

在宕机时触发延迟执行语句

panic() 触发的宕机发生时, panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行的defer语句依然会在宕机时发生作用,例如下面的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
defer fmt.Println("宕机后执行的事情1")
panic("crash")
/**
宕机后执行的事情1
panic: crash

goroutine 1 [running]:
main.main()
/Users/zhimma/go/src/awesomeProject/0312.go:8 +0xf1
}
*/
}

宕机时,defer 语句会优先被执行

测试了下,结果好像不一定,有时候先panic,有时候先defer,以后了解了再来补充吧

Go语言恢复(recover)宕机,防止程序崩溃

无论是代码运行错误,还是由Runtime层抛出的 panic 奔溃,还是主动出发的 panic 奔溃,都可以配合deferrecover 实现错误捕捉和恢复,让代码在发生奔溃后允许继续运行

在其他语言中,宕机往往以异常的形式存在。底层抛出异常,上层逻辑通过 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
package main

import (
"fmt"
"runtime"
)
type panicContext struct {
function string
}

func main() {
fmt.Println("开始运行")
recoverDemo(func() {
fmt.Println("手动宕机前-----")
panic((&panicContext{
"手动触发panic",
}))
})
fmt.Println("手动宕机后")

recoverDemo(func() {
fmt.Println("赋值宕机前")
var a *int
*a = 1
fmt.Println("赋值宕机后")
})
fmt.Println("结束宕机")
}

func recoverDemo(entry func()) {
defer func() {
// 发生宕机时,获取panic传递的上下文并打印
err := recover()

switch err.(type) {
case runtime.Error: // 运行时错误
fmt.Println("runtime error", err)
default:
fmt.Println("error", err)
}
}()
entry()
}


1
2
3
4
5
6
7
开始运行
手动宕机前-----
error &{手动触发panic}
手动宕机后
赋值宕机前
runtime error runtime error: invalid memory address or nil pointer dereference
结束宕机

panic和recover的关系

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

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

提示

虽然 panic/recover 能模拟其他语言的异常机制,但并不建议代表编写普通函数也经常性使用这种特性。

在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛直到程序整体崩溃。

如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。