函数
1. 基本概念
1.1 函数定义
一个函数的定义包括如下几个部分:函数声明关键字 func、函数名、参数列表、返回列表和函数体。函数名首字母大小写决定了该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问;函数返回值需要使用
()
包裹,如果只有一个返回值,而且使用的是非命名的函数,则返回的参数的()
可以省略。函数体使用{}
包裹,并且{
必须位于函数返回值同行的行尾。
func 函数名(形参列表)(返回值列表){
执行语句 ...
return 返回值列表
}
举例:
函数的特点:
- 多个相邻的相同类型的参数可以使用简写模式
func getSum(a, b int)int{ // a int, b int 简写为 a, b int
return a + b
}
- 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的 return 可以不带参数名直接返回。
func getSumAndSub(a, b int)(sum , sub int){
sum = a + b
sum = a - b
return // return 是 return sum , sub 的简写
}
- 不支持类似py中的默认值参数
- 不支持函数重载
- 不支持类似py中的函数嵌套,严格说不支持命名函数的嵌套定义,但支持嵌套匿名函数。
func add(a, b int)(sum int){
anoymous := func(x, y int) int{
return x + y
}
sum = anoymous(a, b)
return
}
1.2 多值返回
Go 函数支持多汁返回,定义多值返回的返回参数列表时要使用
()
包裹,支持命名参数的返回。
func getSumAndSub(a, b int)(sum , sub int){
sum = a + b
sum = a - b
return // return 是 return sum , sub 的简写
}
如果多值返回值有错误类型,则习惯用法是将错误类型作为最后一个返回值。
多值返回的实质是:主调函数预先分配好空间来存放返回值,被调函数执行时将返回值复制到该返回位置来实现的。
1.3 实参到形参的传递
Go 函数实参到形参的传递永远是值拷贝,有时我们可以更改实参的值是传入的是指针的拷贝。
1.4 可变参数
Go 支持数目不确定的形式参数,使用
param ... type
的语法个数。
可变参数的特点:
- 所有不定参数类型必须相同
- 可变参数必须是函数的最后一个参数
- 可变参数在函数体内相当于切片,对切片的操作同样适合对可变参数的操作。例如:
func sum(arr ... int)(sum int){
for _, v := range arr{
sum += v
}
return
}
- 切片可以作为参数传递给不定参数,切片名后要加上
...
- 形参为可变参数的函数和形参为切片的函数类型并不一致
func sumA(arr ... int){
}
func sumB(arr []int){
}
func main(){
a := sumA
b := sumB
fmt.Printf("a type = %T\n", a)
fmt.Printf("b type = %T\n", b)
}
- 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
- 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
2. 函数签名和匿名函数
2.1 函数签名
函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名、参数名和 { ,可以使用
fmt.Printf
的%T
格式化参数函数的类型。
package main
import (
"fmt"
)
func add(a, b int) int{
return a + b
}
func main(){
fmt.Printf("%T\n", add)
}
两个函数类型相同的条件是:拥有相同的形参列表和返回值列表(列表元素的次序、个数和类型都相同),形参名可以不同。
可以使用 type
定义函数类型,函数类型变量可以作为函数的参数或返回值。
基本语法:type 自定义数据类型名 数据类型
案例:type myint int
这时,myint 等价 int 来使用了
案例:type mySum func(int, int) int
//这时,mySum 等价一个函数类型 func(int ,int) int
package main
import (
"fmt"
)
func add(a, b int) int{
return a + b
}
func sub(a, b int) int{
return a - b
}
type Op func(int, int) int // 定义一个函数类型,输入的是两个 int 类型,
// 返回值是一个 int 类型
func do(f Op, a, b int) int {// 定义一个函数,第一个参数是函数类型 Op
return f(a, b) // 函数类型兵力可以直接用来进行函数调用
}
func main(){
a := do(add, 1, 2) // 函数名 add 可以当作相同函数类型形参,不需要强制类型转换
fmt.Println(a)
b := do(sub, 1, 2)
fmt.Println(b)
}
2.2 匿名函数
匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方都可以由匿名函数代替。匿名函数可以直接赋值给函数变量,可以当作实参,也可以当作返回值,还可以直接调用。
3. defer
defer 用于注册多个延迟调用,这些调用以先进后出的顺序在函数退出前被执行。类似于 Java 中的 finally,defer 常用于保证一些资源最终一定能够得到回收和释放。
defer 后面必须是函数或方法的调用,不能是语句。
defer 函数的实参在注册时通过值拷贝传递进去。
defer 语句必须先注册后才能执行,如果 defer 位于 return 之后,则 defer 因为没有注册,不会执行。
主动调用 os.Exit(int)
退出进程时, defer 将不再被执行,即使 defer 已经提前注册。
defer 最主要的价值是在 ,当函数执行完毕后,可以及时的释放函数创建的资源。
func test(){
file = openFile("文件名")
// 关闭文件资源
// 这样可以避免忘记资源没有被释放
defer file.close()
// 其他代码
}
4. 闭包
4.1 概念
闭包是由函数及其相关引用环境组合而成的实体,一般通过匿名函数在引用外部函数的局部变量或包全局变量构成。
闭包 = 函数 + 引用环境
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):
- 多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存。
- 用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。
func fa(a int) func(i int) int{
return func(i int) int{
fmt.Println(&a, a)
a = a + i
return a
}
}
可以看到,fa
函数内部的匿名函数引用到了 fa
的参数 a
,根据闭包的定义,符合闭包的定义,所以构成了一个闭包。
func main() {
f := fa(1) // f 引用的外部的闭包环境包括本次函数调用的形参 a 的值是 1
g := fa(1) // g 引用的外部的闭包环境包括本次函数调用的形参 a 的值是 1
// 此时 f、g 引用的外部的闭包环境中的 a 的值并不是同一个,而是两次函数调用产生的副本
fmt.Println(f(1))
// 多次调用 f 引用的是同一个副本 a
fmt.Println(f(1))
// g 中的 a 的值仍然是 1
fmt.Println(g(1))
fmt.Println(g(1))
}
如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。
package main
import (
"fmt"
)
var (
a = 0
)
func fa() func(i int) int{
return func(i int) int{
fmt.Println(&a, a)
a = a + i
return a
}
}
func main() {
f := fa() // f 引用的外部的闭包环境包括全局变量 a
g := fa() // g 引用的外部的闭包环境包括全局变量 a
// 此时 f、g 引用的外部的闭包环境中的 a 的值是同一个
fmt.Println(f(1))
fmt.Println(f(1))
fmt.Println(g(1))
fmt.Println(g(1))
}
同一个函数返回的多个闭包共享该函数的局部变量。
package main
import (
"fmt"
)
func fa(base int) (func(int) int, func(int) int){
fmt.Println(&base, base)
add := func(i int) int{
base += i
fmt.Println(&base, base)
return base
}
sub := func(i int) int{
base -= i
fmt.Println(&base, base)
return base
}
return add, sub
}
func main() {
// f、g 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值
f, g := fa(0) // base 的地址是 0xc04204a058
fmt.Println(f(1), g(2))
// s、k 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值
s, k := fa(0) // base 的地址是 0xc04204a0c0
fmt.Println(s(1), k(2))
// f、g 和 s、k 引用不同的闭包变量,这是由于 fa 每次调用都要重新分配形参
}
4.2 闭包的价值
闭包最初的目的是减少全局变量,在函数调用的过程中隐式地传递共享变量。但是这种隐秘地共享变量方式不够直观、不易理解。一般不建议使用闭包。
5. panic 和 recover
5.1 基本概念
panic
和recover
是用来处理运行时错误的内置函数。panic
用来主动抛出错误,recover
用来捕获panic
抛出的错误。
发生 panic
后,程序会从调用 panic
的函数位置或者发生panic
的地方立即返回,逐行向上执行函数的defer
语句,然后逐层打印函数调用堆栈,直到被 recover
捕获或运行到最外层函数而退出。
panic
的函数签名如下:
panic(i interface{})
panic
的参数是一个空接口类型interface{}
,所以任意类型的变量都可以传递给 panic
。调用 panic
的方法:panic(xxx)
package main
import "fmt"
func test() {
fmt.Println("panic before")
panic("this is a panic")
fmt.Println("panic after")
}
func main() {
test()
}
recover()
用来捕获 panic
,阻止 panic
继续向上传递。recover()
和 defer
一起使用,但是 recover()
只有在 defer
后面的函数体内被直接调用才能捕获 panic
终止异常,否则返回 nil
,异常继续向外传递。
// 这个会捕获失败
defer recover()
// 这个会捕获失败
defer fmt.Println(recover())
// 嵌套的两层也会捕获失败
defer func(){
func () {
fmt.Println("defer inner")
recover() // 无效
}()
}
// 如下场景会捕获成功
defer func(){
fmt.Println("defer inner")
recover()
}
func except() {
recover()
}
func test() {
defer except()
panic("test panic")
}
可以有连续多个 panic
被抛出,但是只有最后一次 panic
能被捕获。
package main
import "fmt"
func main() {
defer func () {
if err := recover(); err != nil{
fmt.Println(err)
}
}()
defer func(){
panic("first defer panic")
}()
defer func(){
panic("second defer panic")
}()
panic("main body panic")
}
包中 init
函数引发的 panic
只能在 init
函数中被捕获,因为 init
函数先于 main
函数执行。
函数并不能捕获内部新启动协程goroutine
所抛出的 panic
。
package main
import (
"fmt"
"time"
)
func main() {
defer func () {
if err := recover(); err != nil{
fmt.Println("捕获到 panic")
fmt.Println(err)
}
}()
go da() // 开启一个协程,执行 da 函数
go db() // 开启一个协程,执行 db 函数
time.Sleep(3 * time.Second) // 这里 Sleep 的原因是因为如果 main 函数先执行完,则并不能够等待协程的执行完。
}
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)
}
}
5.2 使用场景
panic
主动调用场景:
- 程序遇到了无法正常执行下去的错误,主动调用
panic
函数结束程序运行。 - 在调试程序时,通过主动调用
panic
实现快速退出,panic
打印出的堆栈能够更快地定位错误。
为保证程序地健壮性,需要主动在程序的分支流程使用 recover()
拦截运行时错误。
6. 错误处理
在 Java 中,我们通过
try ... catch ... finally
的方式处理程序中可能出现的错误。Go 语言追求简洁优雅,所以,Go 语言并不支持try ... catch ... finally
这种处理。Go 中引入的处理方式为:defer, pacic, recover
6.1 error
Go 语言内置错误接口类型
error
。任何类型只要实现Error() string
方法,便可以传递error
接口类型变量。Go 语言典序错误处理方式是将error
作为函数最后一个返回值,在调用函数时,通过检测其返回的error
值是否为nil
进行错误处理。
type error interface{
Error() string
}
在 Go 中,也支持自定义错误,使用 errors.New
和 panic
内置函数。
errors.New("错误说明")
,会返回一个 error 类型的值,表示一个错误- panic 内置函数,接收一个 interface() 类型的值(即任何值)作为参数,可以接收一个 error 类型的变量,输出错误信息,并退出程序。
6.2 错误和异常
广义上的错误:发生非期望的行为。
狭义上的错误:发生非期望的已知行为,这里的已知是指错误类型是预料并定义好的。
异常:发生非期望的未知行为。这里的未知是指错误的类型不在预先定义的范围内。异常又被称为未捕获的错误(untrapped error)。程序在执行时发生未预先定义的错误,程序编译器和运行时都没有及时将其捕获处理。而是由操作系统进行异常处理。
Go 在运行时不会出现编译器和运行时都无法捕获的异常,即不会出现 untrapped error。即 Go 语言不存在所谓的异常。
Go 程序需要处理的错误分为两类:
- 运行时错误,此类错误在运行时能够被捕获。
- 程序逻辑错误:程序执行结果不符合预期,但不会引发运行时错误。
Go 对于错误提供了两种处理机制:
- 通过函数返回错误类型的值来处理错误。
- 通过
panic
打印程序调用栈,终止程序执行来处理错误。
编程中,error
和 panic
的使用应遵循如下原则:
- 程序发生的错误但是程序不能容错继续执行,此时程序应主动调用
panic
或由运行时抛出panic
。 - 程序虽然发生错误,但是程序能够容错继续执行,此时应该使用错误返回值的方式处理错误。或者在可能发生运行时错误的非关键分支上使用
recover
捕获panic
。