go之闭包及其应用
https://www.calhoun.io/5-useful-ways-to-use-closures-in-go/
5种有用的闭包
在本文中,我们将探索闭包和匿名函数的几种不同的实际用例,以便您能够更好地理解闭包何时是合适的,并了解它们如何应用于不同的情况。
1.隔离数据
假设您想创建一个函数,该函数可以访问即使在函数退出之后仍然存在的数据。例如,您想要计算函数被调用了多少次,或者您想要创建一个fibonacci数字生成器,但是您不想让任何人访问该数据(这样他们就不会意外地更改它)。您可以使用闭包来实现这一点。
1 package main 2 3 import "fmt" 4 5 func main() { 6 gen := makeFibGen() 7 for i := 0; i < 10; i++ { 8 fmt.Println(gen()) 9 } 10 } 11 12 func makeFibGen() func() int { 13 f1 := 0 14 f2 := 1 15 return func() int { 16 f2, f1 = (f1 + f2), f2 17 return f1 18 } 19 }
当然,您可以使用自定义类型来创建非常类似的东西,但是如果您希望使用多个数字生成器,您可能最终需要声明一个接口,并将其作为使用生成器的其他函数的参数接受,比如这样。
1 type Generator interface { 2 Next() int 3 } 4 5 func doWork(g Generator) { 6 n := g.Next() 7 fmt.Println(n) 8 // ... do work with n 9 }
使用闭包,您可以只要求函数作为参数传入,因为无论如何,您实际上只关心生成器接口中的一个方法。
func doWork(f func() int) { n := f() fmt.Println(n) // ... do work with n }
2.封装函数并创建中间件
函数是Go语言的一等公民,这意味着您不仅可以动态地创建匿名函数,还可以将函数作为参数传递给函数。例如,在创建web服务器时,通常提供一个函数来处理特定路由的web请求。
package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/hello", hello) http.ListenAndServe(":3000", nil) } func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "<h1>Hello!</h1>") }
虽然这段代码不需要闭包,但是如果我们想用更多的逻辑来包装处理程序,闭包是非常有用的。一个完美的例子是,当我们想要在处理程序执行之前或之后创建中间件来执行工作时。
什么是中间件:
中间件基本上是可重用函数的一个花哨术语,可在设计用于处理web requst的代码之前和之后运行代码。在Go中,这些通常是用闭包来完成的,但是在不同的编程语言中,可以用其他方法来完成。
在编写web应用程序时,使用中间件是很常见的,它们不仅仅对计时器有用(您将在下面看到一个示例)。例如,可以使用中间件编写代码来验证用户是否登录过一次,然后将其应用于所有仅限成员的页面。
package main import ( "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, "<h1>Hello!</h1>") }
注意,我们的timing()函数接受一个可以用作处理函数的函数,并返回一个类型相同的函数,但是返回的函数与传递它的函数不同。返回的闭包记录当前时间,调用原始函数,最后记录结束时间并打印出请求的持续时间。同时不知道处理函数内部实际发生了什么。
3.访问通常不可用的数据
虽然这使用了我们在本文前面看到的技术,但值得指出它本身,因为它非常有用。
闭包还可以用于将数据包装在函数内部,否则该函数通常不能访问该数据。例如,如果要在不使用全局变量的情况下提供对数据库的处理程序访问,则可以编写如下代码。
package main import ( "fmt" "net/http" ) type Database struct { Url string } func NewDatabase(url string) Database { return Database{url} } func main() { db := NewDatabase("localhost:5432") http.HandleFunc("/hello", hello(db)) http.ListenAndServe(":3000", nil) } func hello(db Database) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, db.Url) } }
现在我们可以编写处理函数,就好像它们可以访问一个Database
对象,同时仍然返回一个具有http.HandleFunc()
期望签名的函数。这允许我们绕过http.HandleFunc()
不允许我们传递自定义变量而不诉诸全局变量或任何类型的事实。
4.使用排序包进行二进制搜索
在标准库中使用包时也经常需要关闭,例如排序包。
这个包为我们提供了大量有用的函数和代码,用于排序和搜索排序列表。例如,如果要对整数切片进行排序,然后在切片中搜索数字7,则可以sort
像这样使用包。
package main import ( "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) }
但是如果要搜索每个元素是自定义类型的切片会发生什么?或者,如果要查找第一个数字7或更高的索引,而不仅仅是第一个7的索引?
为此,您需要使用sort.Search()函数,并且需要传入一个闭包,该闭包可用于确定特定索引处的数字是否符合您的条件。
5.推迟工作