[go学习笔记.第六章.函数,包,错误处理] 6.闭包,defer,函数参数的传递方式

一.闭包

基本介绍

闭包就是一个函数和其他相关的引用环境组合的一个整体(实体)

//案例
package main

import "fmt"

//累加器
func AddUpper() func (int) int {
    var n int = 10
    return func(x int) int {
        n = n + x
        return n
    }
}
func main() {
    //使用前面的代码
    f := AddUpper()
    fmt.Println(f(1))    //11
    fmt.Println(f(2))    //12
    fmt.Println(f(3))    //13
}

对上面代码说明:

1.AddUpper是一个函数,返回的数据类型是func (int) int

2.闭包说明

 var n int = 10
    return func(x int) int {
        n = n + x
        return n
    }

返回的是一个匿名函数,但是这个匿名函数引用到函数外面的n,因此这个匿名函数就和n形成了一个整体,构成闭包

3.理解:闭包是类,函数是操作,n是字段.函数和它使用到的n构成闭包

4.当反复调用f函数时,因为n是初始化一次,因此每调用一次就进行累计

5.要搞清楚闭包的关键,就要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包

//案例
//1.编写一个函数makeSuffix(suffix  string) 可以接收一个文件后缀名(eg: .jpg),并返回一个闭包
//2.调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(eg:.jpg),则返回文件名.jpg
//3.要求使用闭包方式完成
//4.strings.HasSuffix():判断字符串中是否包含后缀

package main

import (
    "fmt"
    "strings"
)

//闭包函数
func makeSuffix(suffix string) func (string) string {
    //如果name没有指定后缀,则加上,否则返回原来的名字
    return if !strings.HasSuffix(name string, suffix string) {
        return name + suffix
    }
    
    return name
}

//传统函数
func makeSuffix2(suffix string, name string) string {
    if strings.HasSuffix(name string, suffix string) {
        return name + suffix
    }
    return name
}
func main() {
    //测试闭包makeSuffix的使用
    f2 := makeSuffix(".jpg)
    fmt.Println("文件名处理后=",f2("winner"))    //winner.jpg
    fmt.Println("文件名处理后=",f2("test.avi"))    //test.jpg

    //传统makeSuffix2的使用
    fmt.Println("文件名处理后=",makeSuffix2("jpg", "winner"))    //winner.jpg
    fmt.Println("文件名处理后=",makeSuffix2("pg", "test.avi"))    //test.jpg
}

上面返回代码说明:

1.返回的匿名函数和makeSuffix(suffix string)的suffix变量组合成一个闭包,因为返回的函数引用到suffix这个变量

2.闭包的好处:   如果使用传统的方法,也可以轻松实现这个功能,但是传统的方法需要每次都传入后缀名,比如:.jpg,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用


二.defer

为什么需要defer

defer是golang提供的关键字,在函数或者方法执行完成,返回之前调用,

在函数中,程序员经常需要创建资源,比如:数据库连接,锁,文件句柄等,为了在函数执行完毕后,及时地释放资源,go的设计者提供defer(延时机制) 

package main

import "fmt"

func sum(n1 int, n2 int) int {
    //当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈区(defer栈)
    //当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
    defer fmt.Println("n1 = ", n1)    // defer 3. n1 = 10
    defer fmt.Println("n2 = ", n2)    //defer 2. n2 = 20

    n3 := n1 + n2    //n3 = 30
    frm.Println("n3 = ", n3)    //1. n3 = 30
    return n3
}

func main() {
    res := sum(10, 20)
    fmt.Println("res = ", res)    //4.res = 30
}

 defer细节说明:

1.当go执行到一个defer时,不会立即执行defer后面的语句,而是将defer后面的语句压入到一个栈中(暂时称为defer栈),然后继续执行函数下一个语句

2.当函数或方法执行完毕后,再从defer栈中依次从栈顶取出执行(所以多个defer的执行顺序遵守先入后出原则)

3.在defer将语句放入到栈时,也会将相关的值拷贝同时入栈

//defer实践

func test() {
    //关闭文件资源
    file = openFile(文件名)
    defer file.close()
    //其它代码
}

func test1() {
    //释放数据库资源
    connect = openDatabase()
    defer connect.close()
    //其它代码
}
defer规则
  • 延迟函数的参数在defer语句出现时就已经确定下来了
  • 延迟函数执行按后进先出顺序执行,即先出现的defer最后执行
  • 延迟函数可能操作主函数的具名返回值

触发时机

(1).包裹着defer语句的函数返回时
(2).包裹着defer语句的函数执行到最后时
(3).当前goroutine发生Panic时

返回值执行顺序

  1. 先给返回值赋值
  2. 执行defer语句
  3. 包裹函数return返回
    因此,defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着defer开始执行一些收尾工作;最后RET指令携带返回值退出函数
defer命令的拆解

理解这些坑的关键是这条语句:

return xxx

上面这条语句经过编译之后,变成了三条指令:

1. 返回值 = xxx
2. 调用defer函数
3. 空的return

1,3步才是return 语句真正的命令,第2步是defer定义的语句,这里可能会操作返回值
下面来看两个例子,试着将return语句和defer语句拆解到正确的顺序

第一个例子:

func f() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}

拆解后:

func f() (r int) {
     t := 5
     
     // 1. 赋值指令
     r = t
     
     // 2. defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
     func() {        
         t = t + 5
     }
     
     // 3. 空的return指令
     return
}

这里第二步没有操作返回值r, 因此,main函数中调用f()得到5.

第二个例子:

func f() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return 1
}

拆解后:

func f() (r int) {
     // 1. 赋值
     r = 1
     
     // 2. 这里改的r是之前传值传进去的r,不会改变要返回的那个r值
     func(r int) { 
          r = r + 5
     }(r)
     
     // 3. 空的return
     return
}

因此,main函数中调用f()得到1


三.函数参数的传递方式

基本介绍

        在讲解函数的注意事项和使用细节时,已经讲过值类型和引用类型,再在这里系统总结一下:值类型参数默认就是值传递,引用类型参数默认就是引用传递

两种传递方式

1).值传递

2).引用传递

其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝.一般来说,地址拷贝的效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低

值类型和引用类型

1).值类型: 基本数据类型系列:int系列,float系列,string,bool,数组和结构体(struct)

2.引用类型:指针,切片(slice),管道(channel),interface,map等

值传递和引用传递使用特点

1.值类型默认是值传递:变量直接存储值,内存通常分配在栈区[go学习笔记.第六章.函数,包,错误处理] 6.闭包,defer,函数参数的传递方式_第1张图片

2.引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才是真正存储数据(值),内存通常在堆区分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收

[go学习笔记.第六章.函数,包,错误处理] 6.闭包,defer,函数参数的传递方式_第2张图片

3.如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量,从效果上看类似引用 

上一节:[go学习笔记.第六章.函数,包,错误处理] 5.init函数,匿名函数

下一节:[go学习笔记.第六章.函数,包,错误处理] 7.变量的作用域

你可能感兴趣的:(golang,#,golang基础,golang)