Go中闭包使用的5中方式

本文将探索现实世界中go中闭包和匿名函数使用的不同方式,通过这些使用场景使你能更好的理解闭包的使用场景,及在不同场景的应用。 废话不多说,直接上干货。

1 隔离数据

这里要讨论的第一个场景就是数据隔离。假定你要创建一个函数,它要访问在函数退出后持久化的数据。例如你想计算函数被调用了多少次,或者你想创建一个fibonacci数生成器,但是你并不想任何人都能访问到这些数据。你可以使用闭包来完成这些想法:

package main

import "fmt"

func main() {
    gen := makeFibGen()
    for i := 0; i < 10; i++ {
        fmt.Println(gen())
    }
}

func makeFibGen() func() int {
    f1 := 0
    f2 := 1
    return func() int {
        f2, f1 = (f1 + f2), f2
        return f1
    }
}

当你可以使用自定义类型来创建相似的东西,如果你想与多个数字生成器工作,就可能需要声明一个接口,并将接口作为参数传递给其他使用生成器的函数,如:

type Generator interface {
    Next() int
}

func doWork(g Generator) {
    n := g.Next()
    fmt.Println(n)
    // ... do work with n
}

使用闭包可以将函数作为参数进行传递,因为你只关心Generator接口的方法。

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", nill)
}

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "<h1>Hello!</h1>")
}

上面的情况中hello()函数传递给函数http.HandleFunc(),当路由匹配到后,函数被调用。 上面的代码并没有使用闭包,当你需要对处理器包装更多逻辑的时候,闭包就会非常有用。这种场景最适合的例子就是,当我们想利用中间件在处理器之前前后做很多事情。

什么是中间件

中间件基本上是对可复用函数的美称。它们在你处理web请求的的代码之前或之后运行。在golang中,通常通过闭包来实现,在不同的编程语言中实现方式不同。 在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>")
}

需要注意的是我们的timed()函数接受一个处理器函数作为参数,并返回相同类型的函数。但返回的函数的执行结果与接受的函数的执行结果是不同的。闭包返回是会打印当前时间,调用原始的函数(即作为函数参数的处理器函数),最后打印最终时间。这样就输出了请求的耗时。

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 使用sort包实现二分法查找

必要通常也用于标准库,如sort包。这个包给我们提供了很多有的函数和用于排序和查找有序列表的代码。比如,你想对一个元素类型为整数的切片进行排序,并在切片中找到数字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()函数,传递闭包给函数,闭包用来决定指定索引位置的数字是否满足你的要求。 比如我们想查找列表中大于等于7的第一个数字的索引位置:

package main

import (
  "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])
}

这个例子中,我们的闭包是一个很剪短的函数,作为sort.Search()的第二个参数:

func(i int) bool {
  return numbers[i] >= 7
}

上面的闭包访问numbers切片,尽管这个切片并没有传入函数中。对于任何大于等于7的数都返回true.这样就允许sort.Search()在不知道你底层使用的数据类型或需要满足什么标准的前提下就能正常工作。它仅需知道特定索引位置的值是否满足你的标准。

5 Deferring work

如果你使用过javascript,那你很可能运行过如下代码:

doWork(a, b, function(result) {
    // use the result here
});
console.log("hi!");

上面的javascript代码示例就是大家熟知的回调。我们要告诉程序的是运行使用变量a,b运行doWork()函数,然后我们的最后一个参数是一个函数,我们想让它在函数结束是运行,当doWork()函数结束后,会使用doWork()的结果作为function的参数进行调用function. 这种方法的好处是doWork()是一个异步函数,这就是说调用doWork()后,我们可以继续执行我们的代码,在doWork()结束运行前,在屏幕上输出"hi!".当doWork()结束运行后就之一知道接下来该运行什么代码了。 上面的示例并不复杂,但是如果有多层嵌套的函数将让人头疼,比如下面的例子:

doWork1(a, b, function(result) {
  doWork2(result, function(result) {
    doWork3(result, function(result) {
      // use the final result here
    });
  });
});
console.log("hi!");

Golang中创建回调,使用goroutine结合闭包,将变得更加容易,代码可读性更强:

go func() {
  result := doWork1(a, b)
  result = doWork2(result)
  result = doWork3(result)
  // Use the final result
}()
fmt.Println("hi!")

这种方式有两个基本好处:第一个好处是代码非常清晰。可以清晰看到doWork3()doWork1()doWork2()结束后运行,并且是并发执行,因为我们使用了go关键字。 第二个好处是:doWork1()的作者没必要担心写一个异步版的函数。如果你想让函数并发执行,你仅仅需要将这个函数放到另一个goroutine中运行即可。

5 原文链接

https://www.calhoun.io/5-useful-ways-to-use-closures-in-go/