zoukankan      html  css  js  c++  java
  • [golang]语法基础之匿名函数和闭包函数

    说明

    在很多的资料中,对于闭包函数都解释的晦涩难懂,本篇博文尽可能的以简单的说法来解释闭包函数。

    在说明闭包函数之前,需要了解一些其他的内容。

    作用域

    在go中,以包为单位,在包内,主要的单位是函数。所以在一个包内,主要的作用域可以分为函数内部作用域和包内作用域(也就是函数外部作用域)。

    例如:

    package main 
    
    // 函数外部作用域
    
    func main() {
    	// 函数内部作用域
    }
    

    当我们在函数作用域内声明一个变量,在函数的外部是无法使用的。

    例如:

    package main 
    import "fmt"
    func fn1() {
    // 在fn1 内部创建一个变量s1
    	s1 := "hello,world" 
    }
    
    func main() {
    	fmt.Println(s1) //  undefined: s1
    }
    

    在上面的示例中,s1在fn1中定义,属于fn1内部的函数作用域,在main函数的作用域当中去使用,没有办法找到s1,所以在编译的时候就会出现undefined s1

    需要注意的是,在函数外部声明的变量在当前的包内是可以在任何位置使用的。

    例如:

    package main 
    import "fmt"
    // 在函数外部定义个变量
    var s1 = "hello,world"
    
    func main() {
    	// 在函数内部使用这个变量
    	fmt.Println(s1) // hello,world
    }
    

    在上面的代码中,在函数外部作用域定义了一个变量,是处于包内作用域,包内作用域的变量可以在当前包的任何位置去使用,所以s1可以在main函数中使用。

    匿名函数

    在go中,不允许在一个函数当中直接嵌套另外一个函数,什么意思呢,如下:

    func main () {
    	func fn1() {} // func literal evaluated but not used
    }
    

    例如在上面的例子中,在main函数中创建了一个函数fn1,这个时候是没有办法编译成功的,一般情况下,在编译之前你的ide就会出现类似上面的提示。

    如果想要在函数当中在创建一个函数,可以创建一个匿名函数。

    package main 
    
    import "fmt"
    
    func main() {
    	f1 := func() {
    		fmt.Println("hello,world")
    	}
    	fmt.Println(f1) // 0x10995b0
    	f1() // hello,world
    }
    

    在上面代码中,我们在main函数中创建了一个函数,但是这个函数并没有函数名,这样的函数就是go当中的匿名函数(没有函数名的函数)。

    我们都知道,调用一个函数需要一个函数名,但是我们创建的函数是匿名函数,不存在函数名,所以就没有办法按照普通函数的调用方式来实现这个匿名函数。

    想要调用一个匿名函数,可以通过上面的形式来调用匿名函数,将匿名函数存储到一个变量当中,那么此时这个变量就代表了这个匿名函数,如果想要使用函数,直接在这个变量名的后面加上() 就可以实现调用匿名函数的需求。

    调用匿名函数,除了上面的这种方式以外,还可以采用下面的这种形式。

    package main 
    
    func main() {
    	func(data int) {
    		fmt.Println(data)
    	}(1)
    }
    

    在上面的代码中,我们在函数的最后加了一个括号,这样做的结果就是当程序运行到这个函数时,这个匿名函数就会自动的执行。并且这个我们还可以像上面那样,在用来调用的括号里传入一个值,这个值就会传递给函数的形参,通过形参就可以将这个值传递到函数的内部来使用。

    生命周期

    什么是生命周期呢?

    指的是程序在执行的过程中变量存在的时间段。一般来说,包内变量和函数内变量的生命周期是不一样的。

    1. 包内变量会一直常驻在内存中,直到程序结束才会被垃圾回收机制回收。换言之,包内变量的生命周期是整个程序的执行时间。
    2. 在函数内部声明的变量,具备动态的生命周期。每一次执行声明语句就会创建一个新的实体。变量一直生存到它变得不可访问(例如没有外部指针指向它,函数退出我们没有路径能访问到这个变量),这时它占用的存储空间就会被回收。

    例如:

    package main 
    
    var s1 = "hello,world" // 创建一个包内变量,这个全局变量会一直到程序结束才会被垃圾回收
    
    func main() {
    	s2 := "this is test" // 在函数的内部创建一个变量,这个变量会在函数结束之后并且一直到无人访问为止才会被垃圾回收机制回收
    }
    

    在函数内部的一个变量,如果最后被函数以返回值的形式返回到函数的外部,那么这个变量就会发生变量逃逸,由内存的栈中逃逸到内存的堆中。

    package main 
    import "fmt"
    
    func fn1() int {
    	i1 := 10 
    	return i1 
    }
    
    func main () {
    	fmt.Println(fn1()) // 此时函数fn1当中的返回值i1 会发生逃逸,虽然函数执行完毕,但是变量却逃逸出来,跑到了堆中,这样在函数的外部也可以访问这个变量
    }
    

    那么如果我们在一个函数的最后返回一个函数会怎么样呢?

    package main 
    
    import "fmt"
    
    func fn1() {
    	return func() { // 在函数内部创建一个返回值,返回值为一个匿名函数
    		fmt.Println("hello,world")
    	}
    }
    
    func main() {
    	f1 := fn1() // 调用fn1函数,并且使用变量f1来接收fn1函数的返回值 
    	f1() // 通过f1 调用fn1返回的匿名函数
    }
    

    在上面的代码中,函数fn1设置了一个返回值,返回一个匿名函数。在main函数中调用fn1函数,并且使用变量来接收fn1的返回值,那么此时函数fn1的返回值就可以在main函数中进行使用。

    闭包函数

    简单点说,可以把闭包函数理解为在一个函数当中,声明一个函数类型的变量,并且在这个变量当中存储一个匿名函数,这样就实现了函数的嵌套,而这种写法就是闭包函数(closure)。

    那么我们为什么要使用闭包呢?

    我们先来看下面的这段示例:

    package main 
    
    import "fmt"
    
    var s = "hello"
    
    func fn1() {
    	var s = "world"
    	fmt.Println(s)
    }
    
    func main() {
    	fn1()
    }
    

    上面的这段代码输出的结果是world

    输出world的原因是,当在函数作用域内部使用一个变量的时候,程序会现在函数的内部去找这个变量,如果找不到这个变量就会去函数的外部去找这个变量。

    而上面的这个代码很明显,虽然在函数的外部也有一个变量s,但是程序会优先的在函数的内部去找s这个变量,所以输出world。

    而如果在函数当中没有这个变量s,那么就会输出hello。

    如下:

    package main 
    
    import "fmt"
    
    var s = "hello"
    
    func fn1() {
    	fmt.Println(s) // hello
    }
    
    func main() {
    	fn1()
    }
    

    到了这,我们可以对上述的代码进行一个总结,当我们在函数的内部去使用一个变量的时候,程序会优先在函数内部去找这个变量,如果这个变量不存在,就会去函数的外部去找。

    也就是说,在函数内部是可以使用上一层作用域的变量的。

    假设此时有fn1和fn2两个函数,如果我想在fn2函数中访问fn1函数中的变量,该如何去做呢?

    答案很简单,直接将函数fn1中的值以返回值的形式返回到函数的外部,然后在函数fn2中调用,不就可以了吗。

    如下:

    package main 
    
    import "fmt"
    
    func fn1() {
    	s1 := "hello,world"
    	return s1
    }
    
    func fn2() {
    	s := fn1()
    	fmt.Println(s) // hello,world
    } 
    func main() {
    	fn2() 
    }
    

    虽然上面的代码实现了我们在fn2中获取fn1中变量值得需求,但是需要注意的是,此时在fn2中拿到的值是fn1中值得拷贝。当fn1执行完毕之后,fn1中的变量就会被垃圾回收机制销毁。

    如果我们想要以fn1中的值作为参考,对他进行持续性的改变,上面的代码就不能完成我们的操作了。

    例如此时我们需要一份计数器。每调用一次函数就在原有的基础上加1,该如何实现呢?

    示例:

    package main 
    
    import "fmt"
    
    func fn1() {
    	n1 := 1 
    }
    
    func main() {
    	// 此时需要以fn1中的n1的值作为基数,用来计数
    }
    

    如果想要实现上面的需求,可以采用下面的方式:

    package main 
    
    import "fmt"
    
    func fn1() func() int {
    	n1 := 1 
    	return func () int {
    		n1 ++ 
    		return n1 
    	}
    }
    
    func main() {
    	n := fn1() 
    	fmt.Println(n()) // 2
    	fmt.Println(n()) // 3
    	fmt.Println(n()) // 4 
    }
    

    上面的代码中,我们通过在fn1中返回一个匿名函数的形式实现了计数的功能。

    此时因为fn1的返回值是一个函数,在这个函数中始终使用着fn1中的变量,这样就会保证这个变量不会被垃圾回收机制所回收。也就是说,后面每一次执行fn1的返回值匿名函数,都会依据上一次记录的值进行更改。这就是闭包函数的作用。能够让变量免于垃圾回收。

    闭包引用

    在上面的代码中,我们通过闭包的形式实现了一个计数器。那么如果我们将代码改成下面这样,结果就变得和上面不再一样。

    示例:

    package main 
    
    import "fmt"
    
    func fn1() func() int {
    	n1 := 1 
    	return func () int {
    		n1 ++ 
    		return n1 
    	}
    }
    
    func main() {
    	fmt.Println(fn1()()) // 2
    	fmt.Println(fn1()()) // 2
    	fmt.Println(fn1()()) // 2
    }
    

    我们将最后的调用方式改变了一下,结果就全部变为了 2 。

    原因在于上面的三个代码相当于执行了三次的fn1,形成了三个独立的闭包,在这三个独立的闭包中都分别引用了三个独立的n1.所以三个打印的结果都为2 。

    循环闭包引用

    闭包有时也会用在循环当中,如下:

    var funcSlice []func()
    for i := 0; i < 3; i++ {
    	funcSlice = append(funcSlice, func() {
    		println(i)
    	})
    
    }
    for j := 0; j < 3; j++ {
    	funcSlice[j]() // 3, 3, 3
    }
    

    上面的代码输出的结果为3 3 3 。

    原因在于上面的代码引用的都是同一个i的地址,所以随着i的递增,切片当中的结果也就自然而然的都变成了3。

    我们可以将上面的代码做一些修改,如下:

    var funcSlice []func()
    for i := 0; i < 3; i++ {
    	func(i int) {
    		funcSlice = append(funcSlice, func() {
    			println(i)
    		})
    	}(i)
    
    }
    for j := 0; j < 3; j++ {
    	funcSlice[j]() // 0, 1, 2
    }
    

    在上面的代码中,每一次循环的i值都被传递到了匿名函数中,每一轮循环都相当于创建了一个新的变量,这样对于切片来说,每一个值都是独立的引用结果,所以最终的输出为0 1 2 。

  • 相关阅读:
    C语言博客作业03--函数
    Java 图书馆系统
    DS博客作业05--查找
    DS博客作业04--图
    DS博客作业03--树
    C博客作业05--2019-指针
    C语言博客作业04--数组
    C语言博客作业03--函数
    面向对象设计大作业-图书馆查书、借书、还书
    5-互评-OO之接口-DAO模式代码阅读及应用
  • 原文地址:https://www.cnblogs.com/liujunhang/p/12534508.html
Copyright © 2011-2022 走看看