从零开始学GO ---- Go中函数的方方面面

桃扇骨 2022-12-20 02:10 237阅读 0赞

从零开始学GO —— Go中函数的方方面面

在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型

  • 函数是一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行
  • 函数支持多值返回
  • 支持闭包
  • 函数支持可变参数

函数

一个函数的定义包括如下几个部分:函数声明关键字func、函数名、参数列表、返回列表和函数体。【首字母的大小写决定了该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包才可见】

  1. func funcName(param-list)(result-list){
  2. function-body
  3. }
  • 函数可以没有参数也没有返回值(默认返回0)

    1. func A(){
    2. fmt.Println("a")
    3. }
  • 多个相邻的相同类型的参数可以简写

    1. func add(a,b int)int{
    2. return a+b
    3. }
  • 支持有名的返回值,参数名相当于函数体内最外层的局部变量,命名返回值会被初始化为类型零值,最后的return可以不带参数名直接返回

    1. func add2(a, b int) (sum int) {
    2. sum = a + b
    3. return //return sum的简写
    4. }
  • 不支持函数重载
  • 不支持命名函数的嵌套,但支持嵌套匿名函数
  • 可变参数,参数可以不固定,Go语言中的可变参数通过在参数名后加...来标识【所有不定参数的类型必须相同,并且必须是最后一个参数,不定参数相当于切片,切片也可以直接作为参数传递给不定参数】

    1. func sum(x ...int) int {
    2. // fmt.Println(x) //x是一个切片
    3. sum := 0
    4. for _, v := range x {
    5. sum = sum + v
    6. }
    7. return sum
    8. }
    9. func main() {
    10. ret1 := sum()
    11. ret2 := sum(1)
    12. ret3 := sum(1, 2)
    13. ret4 := sum(1, 2, 3)
    14. fmt.Println(ret1, ret2, ret3, ret4) //0 1 3 6
    15. }
  • 可以多值返回函数,如果有多个返回值时必须用()将所有返回值包裹起来

    1. func sumAndSub(x,y int) (sum,sub int) {
    2. sum=x+y
    3. sub=x-y
    4. return
    5. }

实参到形参的传递

Go函数实参到形参的传递永远是值拷贝,有时函数调用后实参指向的值发生变化,是因为参数传递的是指针值的拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址,本质上参数传递仍然是值拷贝。

  1. package main
  2. import "fmt"
  3. func tpvalue(a int) int {
  4. //传值
  5. a = a + 1
  6. return a
  7. }
  8. func tppointer(a *int) {
  9. //传指针
  10. *a = *a + 1
  11. return
  12. }
  13. func main() {
  14. a := 10
  15. tpvalue(a) //实参传递给形参的是值拷贝
  16. fmt.Println(a) //10
  17. tppointer(&a) //实参传递给形参的是值拷贝,但是传递的是地址值
  18. fmt.Println(a) //11
  19. }

函数类型和函数变量

函数类型又名函数签名,一个函数的类型就是函数的定义首行去掉函数名、参数名和 { ,通过fmt.Printf%T来打印函数的类型。

  1. package main
  2. import "fmt"
  3. func add(a, b int) int {
  4. return a + b
  5. }
  6. func sub(a, b int) int {
  7. return a - b
  8. }
  9. func main() {
  10. fmt.Printf("add: %T\n", add) //add: func(int, int) int
  11. fmt.Printf("sub: %T\n", sub) //sub: func(int, int) int
  12. }

可以看到,addsub 的函数类型是一致的。

可以使用 type 定义函数类型,函数类型变量可以作为函数的返回值和参数

  1. func add(a, b int) int {
  2. return a + b
  3. }
  4. func sub(a, b int) int {
  5. return a - b
  6. }
  7. func main() {
  8. type cal func(int, int) int //通过type定义函数类型
  9. var c cal //声明一个函数变量
  10. c = add //给函数变量赋值
  11. fmt.Printf("%d\n", c(1, 2)) //3
  12. c = sub //给函数变量赋值
  13. fmt.Printf("%d\n", c(2, 1)) //1
  14. }

高阶函数

高阶函数可以满足下面的两个条件:

  • 接受其他的函数作为参数传入
  • 把其他的函数作为结果返回

只要满足以上任意一点便可以说该函数是一个高阶函数

问题:编写calculate函数来实现两个整数间的加减运算,但是希望两个整数和具体的操作都由该函数的调用方给出

分析可知,要实现的函数传入的参数,不止两个int类型,而且还有一个函数类型变量,用来表示具体的操作

  1. //首先实现加减
  2. func add(a, b int) int {
  3. return a + b
  4. }
  5. func sub(a, b int) int {
  6. return a - b
  7. }
  8. type operator func(int,int) int //用来表示具体的操作
  9. func calculate(x int, y int, op operate) (int, error) {
  10. if op == nil {
  11. return 0, errors.New("invalid operation")
  12. }
  13. //返回op(x,y)
  14. return op(x, y), nil
  15. }

先用卫述语句检查一下参数,如果operate类型的参数op为nil,那么就直接返回0和一个代表了具体错误的error类型值。如果检查无误,那么就调用op并把那两个操作数传给它,最后返回op返回的结果和代表没有错误发生的nil。

匿名函数

Go提供两种函数:有名函数和匿名函数。匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方都可以由匿名函数代替。匿名函数可以直接赋值给函数变量,可以作为实参,可以作为返回值,可以直接被调用。

匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数

  1. //匿名函数被直接赋值函数变量
  2. var sum = func(a, b int) int {
  3. return a + b
  4. }

通常匿名函数多用来实现回调函数和闭包

闭包

闭包是指函数及其相关引用环境组成的实体,一般通过匿名函数中引用外部函数的局部变量或包全局变量构成。

闭包=函数+引用环境

闭包对闭包外的环境引用是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。

如果函数返回的闭包引用了该函数的局部变量(参数或者函数内部变量)

  1. 多次调用该函数,则返回的多个闭包所引用的外部变量是多个副本,因为每次调用该函数都会为局部变量分配内存
  2. 多次调用同一个函数变量的闭包函数,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对外部变量都有影响,因为闭包是共享外部引用的

用下面的例子说明,也就是说同一个函数变量的闭包环境是共享的:

  1. package main
  2. import "fmt"
  3. func adder() func(int) int {
  4. var x int
  5. return func(y int) int {
  6. //闭包
  7. x += y
  8. return x
  9. }
  10. }
  11. func main() {
  12. var f = adder() //创建了一个闭包f,其引用了x,因此调用f都是共享x的
  13. fmt.Println(f(10)) //10
  14. fmt.Println(f(20)) //30
  15. fmt.Println(f(30)) //60
  16. f1 := adder() //又创建了一个新闭包f1,f1的外部环境中的x与f的x不共享
  17. fmt.Println(f1(10)) //10
  18. fmt.Println(f1(20)) //30
  19. }

当然如果一个函数调用返回的闭包修改了全局变量,则每次调用都会影响全局变量。不过使用闭包就是为了减少全局变量,所以用闭包去修改全局变量不是一个好的方式。

defer

Go函数里提供了defer关键字,可以注册多个延迟调用,这些调用以先进后出的顺序在函数返回前被执行【先被defer的语句最后被执行,最后被defer的语句,最先被执行】。

通常defer被用于确保一些资源回收和释放

defer必须先注册才能执行,如果defer放在return后,因为没有注册,所以不会执行。也就是说,defer在注册时已经确定执行状态,只有执行时机被推迟了而已。

简单例子:

  1. package main
  2. import "fmt"
  3. func main() {
  4. defer func() {
  5. fmt.Println("first")
  6. }() //带参数,表示是匿名函数的运行结果
  7. defer func() {
  8. fmt.Println("second")
  9. }() //带参数,表示是匿名函数的运行结果
  10. fmt.Println("func main")
  11. }
  12. //func main
  13. //second
  14. //first

defer后面必须是函数或方法的调用,不能是语句。

defer函数的实参在注册时通过值拷贝传递进去,后续对实参的修改不影响defer的输出结果。

  1. func main() {
  2. a := 0
  3. defer func(i int) {
  4. fmt.Println(i)
  5. }(a) //带参数,表示是匿名函数的运行结果,在此处已经传入了参数为0,后续a++不影响该参数值
  6. a++
  7. return
  8. } //结果为 0

一个经典的defer题目:

  1. package main
  2. import "fmt"
  3. func calc(index string, a, b int) int {
  4. ret := a + b
  5. fmt.Println(index, a, b, ret)
  6. return ret
  7. }
  8. func main() {
  9. x := 1
  10. y := 2
  11. defer calc("AA", x, calc("A", x, y))
  12. x = 10
  13. defer calc("BB", x, calc("B", x, y))
  14. y = 20
  15. }
  16. // A 1 2 3
  17. // B 10 2 12
  18. // BB 10 12 22
  19. // 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

看一个例子:

  1. func funcA() {
  2. fmt.Println("func A")
  3. }
  4. func funcB() {
  5. panic("panic in B")
  6. }
  7. func funcC() {
  8. fmt.Println("func C")
  9. }
  10. func main() {
  11. funcA()
  12. funcB()
  13. funcC()
  14. }

运行结果:

  1. func A
  2. panic: panic in B
  3. goroutine 1 [running]:
  4. main.funcB(...)
  5. .../hello.go:10
  6. main.main()
  7. .../hello.go:18 +0xa5
  8. exit status 2

程序运行期间funcB中引发了panic导致程序崩溃,异常退出了

修改funcB为该函数,增加recover

  1. func funcB() {
  2. defer func() {
  3. err := recover()
  4. //如果程序出出现了panic错误,可以通过recover恢复过来
  5. if err != nil {
  6. fmt.Println("recover in B")
  7. }
  8. }()
  9. panic("panic in B")
  10. }

使用场景:

  • 程序遇到了无法正常执行下的错误,主动调用panic函数结束程序执行
  • 调试程序时通过panic实现快速退出,通过打印的堆栈可以快速定位错误

发表评论

表情:
评论列表 (有 0 条评论,237人围观)

还没有评论,来说两句吧...

相关阅读

    相关 学习Go (2)

    [Go 语言][Go]从发布 1.0 版本以来备受众多开发者关注并得到广泛使用,Go 语言的简单、高效、并发特性吸引了众多传统语言开发者的加入,而且人数越来越多。 使