分享优质笔记:go闭包的使用学习
下面由go语言教程栏目给大家介绍关于go 闭包的学习使用,希望对需要的朋友有所帮助!
go闭包
在 Golang 中,闭包是一个可以引用其作用域之外变量的函数。
换句话说,闭包是一个内部函数,它可以访问创建它的范围内的变量。即使外部函数完成执行并且作用域被破坏,依然可以访问。
在深入研究闭包之前,需要了解什么是匿名函数。
匿名函数
顾名思义,匿名函数就是没有名字的函数。
举个例子,我们创建一个一般函数和匿名函数
package mainimport ( "fmt")func sayhi1() { // 一般函数 fmt.Println("hello golang, I am Regular function")}func main() { sayhi1() sayhi2 := func() { //匿名函数 fmt.Println("hello golang, I am Anonymous function") } sayhi2()}
结果输出:
hello golang, I am Regular functionhello golang, I am Anonymous function
通过创建一个返回函数的函数来使用匿名函数。
package mainimport ( "fmt")func sayhello(s string) func() { return func() { fmt.Println("hello", s) }}func main() { sayhello := sayhello("golang") sayhello()}
结果输出:
hello golang
常规函数和匿名函数唯一区别是匿名函数不是在包级别声明的,它们被动态地声明,通常要么被使用,要么被遗忘,要么被分配给一个变量供以后使用。
闭包的本质
闭包是包含自由变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。由于自由变量包含在代码块中,所以只要闭包还被使用,那么这些自由变量以及它们引用的对象就不会被释放,要执行的代码为自由变量提供绑定的计算环境。
闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
Golang中的闭包同样也会引用到函数外的变量,闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。从形式上看,匿名函数都是闭包。
我们来看个闭包例子:
package mainimport ( "fmt")func caller() func() int { callerd := 1 sum := 0 return func() int { sum += callerd return sum }}func main() { next := caller() fmt.Println(next()) fmt.Println(next()) fmt.Println(next())}
结果输出:
1 2 3
该例子中called 和sum 是自由变量,caller函数返回的匿名函数为自由变量提供了计算环境,匿名函数和自由变量组成的代码块其实就是闭包。在闭包函数中,只有匿名函数才能访问自由变量called和sum,而无法通过其他途径访问,因此闭包自由变量的安全性。
按照命令式语言的规则,caller函数只是返回了匿名函数的地址,但在执行匿名函数时将会由于在其作用域内找不到sum和called变量而出错。而在函数式语言中,当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)返回。闭包的使用和正常的函数调用没有区别。
现在我们给出引用环境的定义:在程序执行中的某个点所有处于活跃状态的约束所组成的集合,其中的约束指的是一个变量的名字和其所代表的对象之间的联系。
所以我们说:闭包=函数+引用环境
其实我们可以将闭包函数看成一个类(C++),一个闭包函数调用就是实例化一个对象,闭包的自由变量就是类的成员变量,闭包函数的参数就是类的函数对象的参数。在该例子中,next可以看作是实例化的一个对象,next()可以看做是对象函数调用的返回值。
这让我们想起了一句名言:对象是附有行为的数据,而闭包是附有数据的行为
闭包使用的一些例子
-
利用闭包实现数据隔离
假设你想创建一个函数,该函数可以访问即使在函数退出后仍然存在的数据。举个例子,如果你想统计函数被调用的次数,但不希望其他任何人访问该数据(这样他们就不会意外更改它),你就可以用闭包来实现它:
package mainimport ( "fmt")func caller() func() int { callerd := 0 return func() int { callerd++ return callerd }}func main() { next := caller() fmt.Println(next()) fmt.Println(next()) fmt.Println(next())}
结果输出:
1 2 3
-
利用闭包包装函数和创建中间件
Go 中的函数是一等公民。这意味着您不仅可以动态创建匿名函数,还可以将函数作为参数传递给函数。例如,在创建 Web 服务器时,通常会提供一个功能来处理Web 请求到特定的路由。
package mainimport ( "fmt" "net/http")func main() { http.HandleFunc("/hello", hello) http.ListenAndServe(":3000", nil)}func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello!
")}
在上面例子中,函数 hello() 被传递给 http.HandleFunc() 函数,并在该路由匹配时调用。
虽然这段代码不需要闭包,但如果我们想用更多逻辑包装我们的处理程序,闭包是非常有用的。一个完美的例子是我们可以通过创建中间件来在我们处理程序执行之前或之后做一些其它的工作。
什么是中间件?
中间件基本上是可重用功能的一个奇特术语,它可以在设计用于处理 Web 请求的代码之前和之后运行代码。在 Go 中,这些通常是通过闭包来实现的,但在不同的编程语言中,可以通过其他方式来实现。
在编写 Web 应用程序时使用中间件很常见,而且它们不仅可用于计时器(您将在下面看到一个示例)。例如,中间件可用于编写代码验证用户是否登录过一次,然后将其应用到你的所有会员专页。
让我们看看一个简单的计时器中间件在 Go 中是如何工作的。
package mainimport ( "fmt" "net/http" "time")func main() { http.HandleFunc("/hello", timed(hello)) http.ListenAndServe(":3000", nil)}func timed(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { start := time.Now() f(w, r) end := time.Now() fmt.Println("The request took", end.Sub(start)) }}func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello!
")}
timed() 函数接受一个可以用作处理函数的函数,并返回一个相同类型的函数,但返回的函数与传递它的函数不同。返回的闭包记录当前时间,调用原始函数,最后记录结束时间并打印出请求的持续时间。同时对我们的处理程序函数内部实际发生的事情是不可知的。
现在我们要做的就是将我们的处理程序包装在 timed(handler) 中并将闭包传递给 http.HandleFunc() 函数调用。
-
利用闭包使用 sort 二分搜索
使用标准库中的包也经常需要闭包,例如 sort 包。这个包为我们提供了大量有用的功能和代码,用于排序和搜索排序列表。例如,如果您想对一个整数切片进行排序,然后在切片中搜索数字 7,您可以像这样使用 sort 包。
package mainimport ( "fmt" "sort")func main() { numbers := []int{1, 11, -5, 7, 2, 0, 12} sort.Ints(numbers) fmt.Println("Sorted:", numbers) index := sort.SearchInts(numbers, 7) fmt.Println("7 is at index:", index)}
结果输出:
Sorted: [-5 0 1 2 7 11 12]7 is at index: 4
如果要搜索的每个元素都是自定义类型的切片会发生什么?或者,如果您想找到第一个等于或大于 7 的数字的索引,而不仅仅是 7 的第一个索引?
为此,您可以使用 sort.Search() 函数,并且您需要传入一个闭包,该闭包可用于确定特定索引处的数字是否符合您的条件。
sort.Search() is a binary search
sort.Search 函数执行二分搜索,因此它需要一个闭包,该闭包在满足您的条件之前对任何索引返回 false,在满足后返回 true。
让我们使用上面描述的示例来看看它的实际效果;我们将搜索列表中第一个大于或等于 7 的数字的索引。
package mainimport ( "fmt" "sort")func main() { numbers := []int{1, 11, -5, 8, 2, 0, 12} sort.Ints(numbers) fmt.Println("Sorted:", numbers) index := sort.Search(len(numbers), func(i int) bool { return numbers[i] >= 7 }) fmt.Println("The first number >= 7 is at index:", index) fmt.Println("The first number >= 7 is:", numbers[index])}
结果输出:
Sorted: [-5 0 1 2 8 11 12]The first number >= 7 is at index: 4 The first number >= 7 is: 8
在这个例子中,我们的闭包是作为第二个参数传递给 sort.Search() 的简单函数。
这个闭包访问数字切片,即使它从未被传入,并为任何大于或等于 7 的数字返回 true。通过这样做,它允许 sort.Search() 工作而无需了解什么您使用的基础数据类型是什么,或者您试图满足什么条件。它只需要知道特定索引处的值是否符合您的标准。
- 用闭包+defer进行处理异常
package mainimport ( "fmt")func handle() { defer func() { err := recover() if err != nil { fmt.Println("some except had happend:", err) } }() var a *int = nil *a = 100}func main() { handle()}
结果输出:
some except had happend: runtime error: invalid memory address or nil pointer dereference
recover函数用于终止错误处理流程。一般情况下,recover应该在一个使用defer关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用恢复过程(调用recover函数),会导致该goroutine所属的进程打印异常信息后直接退出
对于第三方库的调用,在不清楚是否有panic的情况下,最好在适配层统一加上recover过程,否则会导致当前进程的异常退出,而这并不是我们所期望的。
以上就是分享优质笔记:go闭包的使用学习的详细内容,更多请关注毛票票其它相关文章!