从零开始学GO ---- Go中函数的方方面面
从零开始学GO —— Go中函数的方方面面
在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型
- 函数是一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行
- 函数支持多值返回
- 支持闭包
- 函数支持可变参数
函数
一个函数的定义包括如下几个部分:函数声明关键字func、函数名、参数列表、返回列表和函数体。【首字母的大小写决定了该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包才可见】
func funcName(param-list)(result-list){
function-body
}
函数可以没有参数也没有返回值(默认返回0)
func A(){
fmt.Println("a")
}
多个相邻的相同类型的参数可以简写
func add(a,b int)int{
return a+b
}
支持有名的返回值,参数名相当于函数体内最外层的局部变量,命名返回值会被初始化为类型零值,最后的return可以不带参数名直接返回
func add2(a, b int) (sum int) {
sum = a + b
return //return sum的简写
}
- 不支持函数重载
- 不支持命名函数的嵌套,但支持嵌套匿名函数
可变参数,参数可以不固定,Go语言中的可变参数通过在参数名后加
...
来标识【所有不定参数的类型必须相同,并且必须是最后一个参数,不定参数相当于切片,切片也可以直接作为参数传递给不定参数】func sum(x ...int) int {
// fmt.Println(x) //x是一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}
func main() {
ret1 := sum()
ret2 := sum(1)
ret3 := sum(1, 2)
ret4 := sum(1, 2, 3)
fmt.Println(ret1, ret2, ret3, ret4) //0 1 3 6
}
可以多值返回函数,如果有多个返回值时必须用
()
将所有返回值包裹起来func sumAndSub(x,y int) (sum,sub int) {
sum=x+y
sub=x-y
return
}
实参到形参的传递
Go函数实参到形参的传递永远是值拷贝,有时函数调用后实参指向的值发生变化,是因为参数传递的是指针值的拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址,本质上参数传递仍然是值拷贝。
package main
import "fmt"
func tpvalue(a int) int {
//传值
a = a + 1
return a
}
func tppointer(a *int) {
//传指针
*a = *a + 1
return
}
func main() {
a := 10
tpvalue(a) //实参传递给形参的是值拷贝
fmt.Println(a) //10
tppointer(&a) //实参传递给形参的是值拷贝,但是传递的是地址值
fmt.Println(a) //11
}
函数类型和函数变量
函数类型又名函数签名,一个函数的类型就是函数的定义首行去掉函数名、参数名和 {
,通过fmt.Printf
的%T
来打印函数的类型。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func main() {
fmt.Printf("add: %T\n", add) //add: func(int, int) int
fmt.Printf("sub: %T\n", sub) //sub: func(int, int) int
}
可以看到,add
和sub
的函数类型是一致的。
可以使用 type
定义函数类型,函数类型变量可以作为函数的返回值和参数
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func main() {
type cal func(int, int) int //通过type定义函数类型
var c cal //声明一个函数变量
c = add //给函数变量赋值
fmt.Printf("%d\n", c(1, 2)) //3
c = sub //给函数变量赋值
fmt.Printf("%d\n", c(2, 1)) //1
}
高阶函数
高阶函数可以满足下面的两个条件:
- 接受其他的函数作为参数传入
- 把其他的函数作为结果返回
只要满足以上任意一点便可以说该函数是一个高阶函数
问题:编写calculate函数来实现两个整数间的加减运算,但是希望两个整数和具体的操作都由该函数的调用方给出
分析可知,要实现的函数传入的参数,不止两个int类型,而且还有一个函数类型变量,用来表示具体的操作
//首先实现加减
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
type operator func(int,int) int //用来表示具体的操作
func calculate(x int, y int, op operate) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
//返回op(x,y)
return op(x, y), nil
}
先用卫述语句检查一下参数,如果operate类型的参数op为nil,那么就直接返回0和一个代表了具体错误的error类型值。如果检查无误,那么就调用op并把那两个操作数传给它,最后返回op返回的结果和代表没有错误发生的nil。
匿名函数
Go提供两种函数:有名函数和匿名函数。匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方都可以由匿名函数代替。匿名函数可以直接赋值给函数变量,可以作为实参,可以作为返回值,可以直接被调用。
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数
//匿名函数被直接赋值函数变量
var sum = func(a, b int) int {
return a + b
}
通常匿名函数多用来实现回调函数和闭包
闭包
闭包是指函数及其相关引用环境组成的实体,一般通过匿名函数中引用外部函数的局部变量或包全局变量构成。
闭包=函数+引用环境
闭包对闭包外的环境引用是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或者函数内部变量)
- 多次调用该函数,则返回的多个闭包所引用的外部变量是多个副本,因为每次调用该函数都会为局部变量分配内存
- 多次调用同一个函数变量的闭包函数,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对外部变量都有影响,因为闭包是共享外部引用的
用下面的例子说明,也就是说同一个函数变量的闭包环境是共享的:
package main
import "fmt"
func adder() func(int) int {
var x int
return func(y int) int {
//闭包
x += y
return x
}
}
func main() {
var f = adder() //创建了一个闭包f,其引用了x,因此调用f都是共享x的
fmt.Println(f(10)) //10
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60
f1 := adder() //又创建了一个新闭包f1,f1的外部环境中的x与f的x不共享
fmt.Println(f1(10)) //10
fmt.Println(f1(20)) //30
}
当然如果一个函数调用返回的闭包修改了全局变量,则每次调用都会影响全局变量。不过使用闭包就是为了减少全局变量,所以用闭包去修改全局变量不是一个好的方式。
defer
Go函数里提供了defer关键字,可以注册多个延迟调用,这些调用以先进后出的顺序在函数返回前被执行【先被defer
的语句最后被执行,最后被defer
的语句,最先被执行】。
通常defer被用于确保一些资源回收和释放
defer必须先注册才能执行,如果defer放在return后,因为没有注册,所以不会执行。也就是说,defer在注册时已经确定执行状态,只有执行时机被推迟了而已。
简单例子:
package main
import "fmt"
func main() {
defer func() {
fmt.Println("first")
}() //带参数,表示是匿名函数的运行结果
defer func() {
fmt.Println("second")
}() //带参数,表示是匿名函数的运行结果
fmt.Println("func main")
}
//func main
//second
//first
defer后面必须是函数或方法的调用,不能是语句。
defer函数的实参在注册时通过值拷贝传递进去,后续对实参的修改不影响defer的输出结果。
func main() {
a := 0
defer func(i int) {
fmt.Println(i)
}(a) //带参数,表示是匿名函数的运行结果,在此处已经传入了参数为0,后续a++不影响该参数值
a++
return
} //结果为 0
一个经典的defer题目:
package main
import "fmt"
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}
// A 1 2 3
// B 10 2 12
// BB 10 12 22
// AA 1 3 4
这里要注意的是defer注册要延迟执行的函数时该函数所有的参数都需要确定其值,因此,在注册前,参数中的函数都需要被调用执行。
defer的执行时机:在Go语言的函数中return
语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer
语句执行的时机就在返回值赋值操作后,RET指令执行前
panic和recover
panic和recover是用来处理Go的运行时错误的内置函数。panic用来主动抛出错误,recover用来捕获panic抛出的异常。 panic
可以在任何地方引发,但recover
只有在defer
调用的函数中有效
引发panic
有两种情况,一种是程序主动调用panic
函数,另一种是程序产生运行时错误,由运行时检测并抛出。
发生panic
后,程序会从调用panic
的函数位置或发生panic
的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,知道被recover
捕获或运行到最外层函数退出。
recover
用来捕获panic,阻止panic
继续向上传递,recover
只有在defer
后面的函数体内被直接调用才能捕获panic
终止异常,否则返回nil
看一个例子:
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
运行结果:
func A
panic: panic in B
goroutine 1 [running]:
main.funcB(...)
.../hello.go:10
main.main()
.../hello.go:18 +0xa5
exit status 2
程序运行期间funcB
中引发了panic
导致程序崩溃,异常退出了
修改funcB
为该函数,增加recover
func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}
使用场景:
- 程序遇到了无法正常执行下的错误,主动调用panic函数结束程序执行
- 调试程序时通过panic实现快速退出,通过打印的堆栈可以快速定位错误
还没有评论,来说两句吧...