Go的匿名函数(闭包)

感觉今天应该是对匿名函数的使用开窍了😃

概念

《Go语言圣经》

使用匿名函数可以访问完整的词法环境,也就是在函数中定义的内部函数可以引用该函数的变量。

// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
    var x int
    return func() int {	// 使用了上一行的变量x ,虽然x不是在这个匿名函数里定义的
        x++
        return x * x
    }
}
func main() {
    f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
    fmt.Println(f()) // "16"
}

函数squares返回另一个类型为func() int的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。

squares的例子证明,函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。

通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。这就是多次调用f()后,可以看到x的值是随上一次增长的,我们可以把x看作存在与f的变量,它们有相同的生命周期。

外部函数对匿名函数的参数是指针传递的

Golang一般来说,全部是值传递;匿名函数除外,匿名函数接收一个指针

// 通常来讲,go是值传递的
// 但是闭包除外,它是引用传递
func errorUsing() {
	for i := 0; i < 3; i++ {
		defer func() {
			fmt.Println(i)
		}()
	}
}

func correctUsing() {
	for i := 0; i < 3; i+t+ {
		defer func(it int) {	// 每次迭代用新的局部变量记录i的值
			fmt.Println(it)
		}(i)
	}
}

func main() {
	errorUsing()	// 3 3 3
	correctUsing()	// 2 1 0
}

函数errorUsing()中,执行了三次defer func(),使用指针传递i,所以是指向同一个地址的,对defer func()压栈后,出栈输出时,这个地址指向的i的值为3。因为栈底到栈顶的这三个defer func()i是同一个地址,所以输出的值相同。

函数correctUsing()每次调用defer func(),将传入的指向i的指针赋值了一份,传给形参it,所以拿到的iti是两个不同的地址空间,同理,三次压栈的defer func()每个的参数it都是不同的地址空间,所以能把三个不同的i打印出来。

使用匿名函数

**让函数带着参数走,不用显示的传参。**因为有些情况下,项目开发中很多函数已经定型了,如果某个地方需要新的参数,但是又不好修改函数声明,这时候就可以考虑使用匿名函数传值。

这是我的一个理解,就像概念中的Demo,函数f()带着参数x走,而且不用显示的接收参数。

Demo1-文件的Read和Close

func doFile() {
    
    // 假设 我们定义一个方法,但是不用显示的使用 f
    // 就需要在 定义函数时 使用闭包 把 f 传进去
	f, _ := os.OpenFile("filename.txt", os.O_RDWR|os.O_CREATE, 0755)

	// 通常的,匿名函数用来直接使用函数外的变量,利用的就是这一个特性
	var (
		Close = func() error {
		return (*os.File).Close(f)	// 传入外部的 f
	}
		Read = func(data []byte) (int, error) {
			return (*os.File).Read(f, data)	// 传入外部的 f
		}
	)
	// 不再依赖 f,只要是个 *File 类型的都可以放
	Read([]byte("data"))
	Close()
}

可以看到,这里Read的声明是一个这样的函数:接收一个[]byte,返回一个(int, err)

(*os.File).Read(f, data)中的Read是这样的:

它是类型*File的一个方法,接收[]byte,返回int, err。使用时需要Read(f, data)两个参数。

使用闭包将*File传入后,新的包装后的Read([]byte)只需要一个参数就行了,f已经隐式的包含在里面了。

Demo2-依赖注入(串联http处理函数handlerFunc)

假设有这样一段:

func main() {
    server := http.Server{
        Addr : "127.0.0.1:8080",
    }
    http.HandleFunc("/post", handleRequest)
    server.ListenAndServe()
}

// 一个多路复用器
func handleRequest(w http.ResponseWriter, r *http.Request) {
    var err error
    switch r.Method {
        case "GET":
        err = handleGet(w, r)
        case "POST":
        ...
        ...
    }
}

// 对应的处理函数
func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
    ...
}

如果,现在有这么一种需求,函数handleGet(w,r)多了一个参数需求,它想变成handleGet(w, r, t),同时这个参数要在main()中传给handleRequest以便给后面所有从它复用出来的处理器使用

所以,有下面这种方法:

// 一个 使用匿名函数返回 一个 多路复用器的函数 handleRequest
func handleRequest(t Text) http.HandlerFunc {
    // 通过匿名函数 把 t 传进去
    return func(w http.ResponseWriter, r *http.Request) {
        var err error
    	switch r.Method {
        case "GET":
        	err = handleGet(w, r, t)	// 可以使用t
        case "POST":
        ...
        ...
    	}
    }
}

func main() {
    t := &Text{...}	// 创建T的实例
    
    server := http.Server{
        Addr : "127.0.0.1:8080",
    }
    
    http.HandleFunc("/post", handleRequest(t))	// line 1
    // line 1 也就等同于下面这两行
    // h := handleRequest(t)
    // http.HandleFunc("/post", h)	// 为什么不是 h() 这个和 直接使用一个 handleFunc 是一个道理,不用加 ()
    
    server.ListenAndServe()
    
}

这里就使用了匿名函数,事先把它传给handleRequest(t Text),从而没有影响http.HandleFunc()中需要调用的实现了ServerHTTP()的处理器。

Licensed under CC BY-NC-SA 4.0
自认为是幻象波普星的来客
Built with Hugo
主题 StackJimmy 设计