Go语言学习-函数
Posted windsun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
", 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
", *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来进行快速报错,这也是一种有效的调试手段。
以上是关于Go语言学习-函数的主要内容,如果未能解决你的问题,请参考以下文章
解决go: go.mod file not found in current directory or any parent directory; see ‘go help modules‘(代码片段