Go语言学习-函数
函数
Go不是一门纯函数式的编程语言,但是函数在Go中是“第一公民”,表现在:
- 函数是一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行。
- 函数支持多值返回。
- 支持闭包。
- 函数支持可变参数。
Go是通过编译成本地代码且基于“堆栈”式执行的,Go的错误处理和函数也有千丝万缕的联系。
函数定义
函数是Go程序源代码的基本构造单位,一个函数的定义包括如下几个部分:
1.函数声明关键字func
2.函数名
3.参数列表
4.返回列表
5.函数体。
函数名遵循标识符的命名规则,首字母的大小写决定该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问;
函数的参数和返回值需要使用“()”包裹,如果只有一个返回值,而且使用的是非命名的参数,则返回参数的“()”可以省略。
函数体使用“{}”包裹,并且“{”必须位于函数返回值同行的行尾。
func funcName(param-list) (result-list) { function-body }
函数的特点
- 函数可以没有输入参数,也可以没有返回值(默认返回0)。
func A() { //do something //... } func A() int { //do something //... return 1 )
- 多个相邻的相同类型的参数可以使用简写模式。
func add(a, b int) int { //a int, b int简写为a, b int return a+b }
- 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的return可以不带参数名直接返回。
//sum 相当于函数内的局部变量,被初始化为0 func add(a, b int) (sum int) { //sum 是命名返回值变量 sum = a + b return //return sum 简写 //sum := a + b //如果是sum := a + b,则相当于新声明一个sum变量命名返回变量sum覆盖 //return sum //需要显式的调用return sum }
- 不支持默认值参数。
- 不支持函数重载。
- 不支持命名函数的嵌套定义,但支持嵌套匿名函数。
func add(a, b int) (sum int) { anonymous := fumc(x, y int) int { return x + y } return anonymous(a, b) }
多值返回
Go函数支持多值返回,定义多值返回的返回参数列表时要使用“()”包裹,支持命名参数的返回。
fune swap(a,b int) (int, int){ return b, a }
注意:
如果多值返回值有错误类型,则一般将错误类型作为最后一个返回值。
实参?形参
Go函数实参到形参的传递永远是值拷贝,有时函数调用后实参指向的值发生了变化,那是因为参数传递的是指针值的拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址,本质上参数传递仍然是值拷贝。
func chvalue(a int) int { a = a + 1 return a } func chpointer(a *int) { *a = *a + 1 } func main() { a := 10 chvalue(a) fmt.Println(a) //10 ,实参传递给形参是值拷贝 chpointer(&a) fmt.Println(a) //11,仍然是值拷贝,但是复制的是地址值 }
不定参数
Go函数支持不定数目的形式参数,不定参数声明使用param ...type
的语法格式。
函数的不定参数有如下几个特点:
- 所有的不定参数类型必须是相同的。
- 不定参数必须是函数的最后一个参数。
- 不定参数名在函数体内相当于切片,对切片的操作同样适合对不定参数的操作。
func sum(arr ...int) (sum int) { for _, v := range arr { //此时 arr 就相当于切片,可以使用 range 访问 sum += v } return }
- 切片可以作为参数传递给不定参数,切片名后要加上“...”。
func sum(arr ...int) (sum int) { for _, v := range arr { //此时 arr 就相当于切片,可以使用 range 访问 sum += v } return } func main() { slice := []int{1, 2, 3, 4} //切片 array := [...]int{1, 2, 3, 4} //数组 //数组不可以作为实参传递给不定参数的函数 sum(slice...) }
- 形参为不定参数的函数和形参为切片的函数类型不相同。
func suma(arr ...int) (sum int) { for _, v := range arr { //此时 arr 就相当于切片,可以使用 range 访问 sum += v } return } func sumb(arr []int) (sum int) { for v := range arr { sum += v } return } func main() { fmt.Printf("%T", suma) //func(...int) int fmt.Printf("%T", sumb) //func([]int) int }
函数签名
函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名、参数名和"{",可以使用fimt.Printf的“%T“格式化参数打印函数的类型。如上程序。
两个函数类型相同的条件是:拥有相同的形参列表和返回值列表(列表元素的次序、个数和类型都相同),形参名可以不同。以下2个函数的函数类型完全一样。
func add(a,b int) int { return a+b } func sub(x int, y int)(c int) { c = x-y; return c }
可以使用type定义函数类型,函数类型变量可以作为函数的参数或返回值
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 { return f(a, b) //函数类型变量可以直接用来进行函数调用 } func main() { a := do(add, 1, 2) //函数名add可以当作相同函数类型形参 fmt.Println(a) //3 s := do(sub, 1, 2) fmt.Println(s) //-1 }
函数类型和map、slice、chan一样,实际函数类型变量和函数名都可以当作指针变量,该指针指向函数代码的开始位置。
通常说函数类型变量是一种引用类型,未初始化的函数类型的变量的默认值是nil。
Go中有名函数的函数名可以看作函数类型的常量,可以直接使用函数名调用函数,也可以直接赋值给函数类型变量,后续通过该变量来调用该函数。
package main func sum(a, b int) int { return a + b } func main() { sum(3, 4) //1.直接调用 f := sum //2.有名函数可以直接赋值给变量 f(1, 2) }
匿名函数
匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方都可以由匿名函数代替。匿名函数可以直接赋值给函数变量,可以当作实参,也可以作为返回值,还可以直接被调用。
package main import "fmt" //匿名函数被直接赋值函数变量 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) //5 fmt.Printf("%d\n", re) }
defer
- Go函数里提供了defer关键字,可以注册多个延迟调用,这些调用以先进后出(FILO)的顺序在函数返回前被执行。这有点类似于Java语言中异常处理中的finally子句。defer常用于保证一些资源最终一定能够得到回收和释放。
package main func main() { //先进后出 defer func() { println("first") }() defer func() { println("second") }() println("function bldy") } //结果(先注册,后执行) //function bldy //second //first
- defer 后面必须是
函数
或方法
的调用,不能是语句,否则会报expression in defer must be function call错误。 - defer 函数的实参在注册时通过值拷贝传递进去。下面示例代码中,实参a的值在defer注册时通过值拷贝传递进去,后续语句a++并不会影响defer语句最后的输出结果。
func f() int { a := 0 defer func(i int) { println("defer i=", i) //defer i= 0 }(a) //a注册时是以值拷贝传递,所以先执行a++也不会影响注册时的a值 a++ return a } func main() { a := f() fmt.Println(a) //1 }
- defer语句必须先注册后才能执行,如果defer位于return之后,则defer因为没有注册,不会执行。
- 主动调用
os.Exit(int)
退出进程时,defer将不再被执行(即使defer已经提前注册)。
func main() { defer func() { println("defer") }() println("func body") os.Exit(1) } //运行结果 //func body
defer的好处是可以在一定程度上避免资源泄漏,特别是在有很多return语句,有多个资源需要关闭的场景中,很容易漏掉资源的关闭操作。例如:
func CopyFile(dst, src string) (w int64, err error) { src, err := os.Open(src) if err != nil { return } dst, err := os.Create(dst) if err != nil { src.Close() //src 很容易忘记关闭 return } w, err = io.Copy(dst, src) dst.Close() src.Close() return }
使用defer改写后,在打开资源无报错后直接调用defer关闭资源,一旦养成这样的编程习惯,则很难会忘记资源的释放。
func CopyFile(dst, src string) (w int64, 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相对于普通的函数调用需要间接的数据结构的支持,相对于普通函数调用有一定的性能损耗。
defer中最好不要对有名返回值参数进行操作,否则会引发匪夷所思的结果。
闭包
闭包是由函数及其相关引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
闭包=函数+引用环境
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):
(1)多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存。
(2)用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。
示例如下:
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) //g引用的外部的闭包环境包括本次函数调用的形参a的值1 println(f(1)) println(f(1)) println(g(1)) println(g(1)) } /* 运行结果: 0xc00004a000 1 2 0xc00004a000 2 3 0xc00004a008 1 2 0xc00004a008 2 3 */
f和g引用的是不同的a。
如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。
如果函数返回的闭包引用的是全局变量a,则多次调用该函数返回的多个闭包引用的都是同一个a。同理,调用一个闭包多次引用的也是同一个a。此时如果闭包中修改了a值的逻辑,则每次闭包调用都会影响全局变量a的值。使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。
var ( a = 0 ) func fa() func(i int) int { return func(i int) int { println(&a, a) a = a + 1 return a } } func main() { f := fa() //f引用外部的闭包环境包括全局变量a g := fa() //g引用的外部环境闭包环境包括全局变量a println(f(1)) //1 println(g(1)) //2 println(f(1)) //3 println(g(1)) //4 } /* 运行结果: 0x4e49f8 0 1 0x4e49f8 1 2 0x4e49f8 2 3 0x4e49f8 3 4 */
用一个函数返回的多个闭包共享该函数的局部变量。
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 := fa(0) //f、g 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值 s, k := fa(0) //s、k 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值 println(f(1), g(2)) println(s(1), k(2)) }
闭包最初的目的是减少全局变量,在函数调用的过程中隐式地传递共享变量,有其有用的一面;但是这种隐秘的共享变量的方式带来的坏处是不够直接,不够清晰,除非是非常有价值的地方,一般不建议使用闭包。
对象是附有行为的数据,而闭包是附有数据的行为,类在定义时已经显式地集中定义了行为,但是闭包中的数据没有显式地集中声明的地方,这种数据和行为耦合的模型不是一种推荐的编程模型,闭包仅仅是锦上添花的东西,不是不可缺少的。
panic & recover
但在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。当程序发生 panic 时,使用 recover 可以重新获得对该程序的控制。
可以认为 panic 和 recover 与其他语言中的 try-catch-finally 语句类似,只不过一般我们很少使用 panic 和 recover。而当我们使用了 panic 和 recover 时,也会比 try-catch-finally 更加优雅,代码更加整洁。
什么时候应该使用 panic?
(需要注意的是:你应该尽可能地使用错误,而不是使用 panic 和 recover。只有当程序不能继续运行的时候,才应该使用 panic 和 recover 机制)
panic 有两个使用情况:
发生了一个不能恢复的错误,此时程序不能继续运行。 主动调用panic函数结束程序运行。
例如: web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。发生了一个编程上的错误。 在调试程序中,通过主动调用panic来实现快速推出,painc打印出的堆栈能更快的定位错误。
例如:我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。
为了保证程序的健壮性,需要主动在程序的分支流程上使用recover()
拦截运行时错误。
panic和recover的函数签名如下:
panic(i interface{}) recover()interface{}
panic用法挺简单的, 其实就是throw exception。panic是golang的内建函数,panic会中断函数F的正常执行流程, 从F函数中跳出来, 跳回到F函数的调用者。 对于调用者来说, F看起来就是一个panic, 所以调用者会继续向上跳出, 直到当前goroutine返回。在跳出的过程中, 进程会保持这个函数栈。 当goroutine退出时, 程序会crash。
要注意的是, F函数中的defer定义的函数会正常执行, 按照defer的规则。
同时引起panic除了我们主动调用panic之外, 其他的任何运行时错误, 例如数组越界都会造成panic
调用panic的方法非常简单:panic(xxx)
。
package main import ( "fmt" ) func main() { test() } func test() { defer func() { fmt.Println("打印前") }() defer func() { fmt.Println("打印中") }() defer func() { fmt.Println("打印后") }() panic("触发异常") fmt.Println("test") } /* 运行结果: 打印后 打印中 打印前 panic: 触发异常 goroutine 1 [running]: main.test() C:/Users/WINDSUN/Desktop/Golang/CloseFunc.go:15 +0xd1 main.main() C:/Users/WINDSUN/Desktop/Golang/CloseFunc.go:8 +0x27 Process finished with exit code 2 */
panic与defer
panic 其实是一个终止函数栈执行的过程,但是在函数退出前都会执行defer里面的函数,直到所有的函数都退出后,才会执行panic。发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer 语句,然后逐层打印函数调用堆栈,直到被recover 捕获或运行到最外层函数而退出。
func fullName(firstName *string, lastName *string) { defer fmt.Println("deferred call in fullName") if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName") } func main() { defer fmt.Println("deferred call in main") firstName := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main") } /* 运行结果: deferred call in fullName deferred call in main panic: runtime error: last name cannot be nil goroutine 1 [running]: main.fullName(0xc00007df20, 0x0) C:/Users/WINDSUN/Desktop/Golang/CloseFunc.go:14 +0x255 main.main() C:/Users/WINDSUN/Desktop/Golang/CloseFunc.go:23 +0xe0 Process finished with exit code 2 */
panic不但可以在函数正常流程中抛出,在defer逻辑里也可以再次调用panic或抛出panic。如果defer中也有panic 那么会依次按照发生panic的顺序执行。
recover也是golang的一个内建函数, 其实就是try catch。
不过需要注意的是:
1. recover如果想起作用的话, 必须在defer函数中使用。
2. 在正常函数执行过程中,调用recover没有任何作用, 他会返回nil。如这样:fmt.Println(recover())
。
3. 如果当前的goroutine panic了,那么recover将会捕获这个panic的值,并且让程序正常执行下去。不会让程序crash。
package main import "fmt" func main() { fmt.Println("c") defer func() { // 必须要先声明defer,否则不能捕获到panic异常 fmt.Println("d") if err := recover(); err != nil { fmt.Println(err) // 这里的err其实就是panic传入的内容 } fmt.Println("e") }() f() //开始调用f fmt.Println("f") //这里开始下面代码不会再执行 } func f() { fmt.Println("a") panic("异常信息") fmt.Println("b") //这里开始下面代码不会再执行 } /* 运行结果: c a d 异常信息 e Process finished with exit code 0 */
可以有连续多个panic被抛出,连续多个panic的场景只能出现在延迟调用里面,否则不会出现多个panic被抛出的场景。但只有最后一次panic能被捕获。
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") } /* 运行结果: first defer panic */
错误处理
Go的错误处理涉及接口的相关知识。
error
Go 语言内置错误接口类型error。任何类型只要实现Error() string
方法,都可以传递error接口类型变量。Go语言典型的错误处理方式是将error作为函数最后一个返回值。在调用函数时,通过检测其返回的error值是否为nil来进行错误处理。
type error interface { Error() string }
Go语言标准库提供的两个函数返回实现了error接口的具体类型实例,一般的错误可以使用这两个函数进行封装。遇到复杂的错误,用户也可以自定义错误类型,只要其实现error接口即可。例如:
//http://golang. org/src/pkg/fmt/print. go //ErrOFf formats according to a format specifier and returns the string //as a value that satisfies error. func Errorf(format string, a... interface{}) errort { return errors.New(Sprintf(format,a...)) } //http://golang. org/src/pkg/errors/errors. go //New returns an error that formats as the given text. func New(text string) error { return &errorString{ text } }
错误处理的最佳实践:
- 在多个返回值的函数中,error通常作为函数最后一个返回值。
- 如果一个函数返回error类型变量,则先用if语句处理
error!=nil
的异常场景,正常逻辑放到if语句块的后面,保持代码平坦。 - defer 语句应该放到err判断的后面,不然有可能产生panic。
- 在错误逐级向上传递的过程中,错误信息应该不断地丰富和完善,而不是简单地抛出下层调用的错误。这在错误日志分析时非常有用和友好。
错误和异常
广义上的错误:发生非期望的行为
狭义的错误:发生非期望的已知行为,这里的已知是指错误的类型是预料并定义好的。
异常:发生非期望的未知行为,未知是指错误的类型不在预先定义的错误。异常又被称为未捕获的错误。未捕获由操作系统进行异常处理,如C语言中的段错误。
错误关系如图:
Go是一门类型安全的语言,其运行时不会出现这种编译器和运行时都无法捕获的错误,也就是说,不会出现untrapped error,所以从这个角度来说,Go语言不存在所谓的异常,出现的“异常”全是错误。
Go程序需要处理的这些错误可以分为两类:
- 一类是运行时错误(runtime errors),此类错误语言的运行时能够捕获,并采取措施---隐式或显式地抛出panic。
- 一类是程序逻辑错误:程序执行结果不符合预期,但不会引发运行时错误。
对于运行时错误,程序员无法完全避免其发生,只能尽量减少其发生的概率,并在不影响程序主功能的分支流程上“recover”这些panic,避免其因为一个panic引发整个程序的崩溃。
Go对于错误提供了两种处理机制:
(1)通过函数返回错误类型的值来处理错误。
(2)通过panic打印程序调用栈,终止程序执行来处理错误。
error和panic使用遵循如下规则:
(1)程序发生的错误导致程序不能容错继续执行,此时程序应该主动调用panic或由运行时抛出panic。
(2)程序虽然发生错误,但是程序能够容错继续执行,此时应该使用错误返回值的方式处理错误,或者在可能发生运行时错误的非关键分支上使用recover 捕获panic。
go的整个错误处理过程如图:
Go程序的有些错误是在运行时进行检测的,运行期的错误检测包括空指针、数组越界等。
如果运行时发生错误,则程序出于安全设计会自动产生panic。另外,程序在编码阶段通过主动调用panic来进行快速报错,这也是一种有效的调试手段。