golang 闭包使用详解

golang 闭包使用详解

对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

你可能感兴趣的:(Golang)