对GO闭包的特性,用法做一些归纳。
简单地说, 闭包就是函数行为 + 环境数据。
var a = 1 // global
func test(){
a = 12
}
这里函数 test 访问了它作用域之外的变量,这就可以看成是最简单的闭包。
go 官方文档里对闭包的解释是
Go supports anonymous functions, which can form closures.
(用匿名函数可以形成闭包)
官方样例
package main
import "fmt"
func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
nextInt := intSeq()
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
newInts := intSeq()
fmt.Println(newInts())
}
//output:
1
2
3
1
nextInt公用一个环境变量i,每次nextInt()执行完毕,nextInt所指向的变量 i 还在intSeq函数里保存着,每次的操作自增 1,形成一个类似于python 中yeild语句的生成器。
闭包的用法可以分为这几种
import(
"fmt"
"strings"
)
func main {
s := "hello world"
wrapper(sayHello)(s)
}
func sayHello(s string) {
fmt.Println(s)
}
func wrapper(f func(string)) func(string) {
return func(s string) {
a := strings.ToUpper(s)
f(a)
fmt.Println("middleware executed")
}
}
//output
HELLO WORLD
middleware executed
wrapper函数作为中间件,完成了将字符串转为大写的功能,将预处理和打印进行了解耦。
再看一个http 服务的实例
import (
"fmt"
"html"
"log"
"net/http"
)
func main() {
http.HandleFunc("/hello", writeIntoDB(greet))
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
func greet(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello, %q", html.EscapeString(r.URL.Path))
}
func writeIntoDB(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
//write to db
// ...
fmt.Println("wirte to db success")
f(w, r)
}
}
这里做了一个写入DB的中间件,重点是闭包,写入数据库的代码就不写了。这种作用点类似于装饰器模式。
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)
}
}
这个例子里,通过闭包让hello函数也能访问到 main 函数中的局部变量
闭包可以将接受不同参数的函数统一成 参数相同的函数(类似于适配器模式),在代码复用的时候很有用
func main() {
a := do(wrapper("+"), 1, 2, 3, 4)
fmt.Println(a)
}
// 已有代码 -----------------------------
type calTwoInt func(i, j int) int
func do(f calTwoInt, params ...int) int {
result := 0
for _, val := range params {
result = f(result, val)
}
return result
}
func operate(i, j int, op string) int {
if op == "+" {
return i + j
} else if op == "-" {
return i - j
}
return 0
}
// -----------------------------------------
// 想尽量复用代码,将operate塞到do里面。这里通过闭包统一参数,返回一个calTwoInt类型的函数
func wrapper(op string) func(i, j int) int {
return func(i, j int) int {
return operate(i, j, op)
}
}
func main(){
for i := 0; i < 5; i++ {
// closure over value of i
go func() {
fmt.Printf("in goroutine, %d\n", i)
}()
}
time.Sleep(2 * time.Second)
}
//output:
in goroutine, 5
in goroutine, 5
in goroutine, 5
in goroutine, 4
in goroutine, 5
结果为什么不是0 、1 … 4?
实际上由于闭包读取for循环里的i, 这里是一个地址传递,主线程中for循环的执行速度要比发起和执行go协程块,所以当各个协程读取i的时候,for循环已经跑了很久了,i已经不是它发起时的值了。
当然, 也不是一定要所有的协程都发起了,才会有开始执行的协程,可以看到,在i = 4的时候,已经有协程执行结束了,把 i 调到 100,这个现象会更明显。要让每个协程引用不同的i,可以定义额外的局部变量。
func main(){
for i := 0; i < 5; i++ {
// closure over value of i
i := i
go func() {
fmt.Printf("in goroutine, %d\n", i)
}()
}
time.Sleep(2 * time.Second)
}
//output:
in goroutine, 4
in goroutine, 2
in goroutine, 3
in goroutine, 0
in goroutine, 1
这里多了一句 i := i, 在每一个for循环内部,都定义一个新的局部变量,每个协程都引用自己的i 变量。
或者显式地通过值传递捕捉 i
func main(){
for i := 0; i < 5; i++ {
go func(int i) {
fmt.Printf("in goroutine, %d\n", i)
}(i)
}
time.Sleep(2 * time.Second)
}
//output
in goroutine, 4
in goroutine, 2
in goroutine, 3
in goroutine, 0
in goroutine, 1
推荐后一种方式,比如在用defer 做延迟调用时,将当时的环境参数传入闭包是一个很好的选择。
func main(){
for i := 0; i < 3; i++ {
// closure over value of i
defer func(i int) {
fmt.Println(i)
}(i)
}
time.Sleep(2 * time.Second)
}
//output
2
1
0