golang特性4

本文介绍了函数式编程中的闭包概念,展示了如何在Go语言中使用闭包作为参数、返回值以及在中间件设计中的应用。同时指出了在for循环中使用闭包可能导致的问题,即闭包捕获的是循环结束时的变量值,而非迭代时的值,提供了解决方法。
摘要由CSDN通过智能技术生成

函数式编程与闭包

1.函数概念

  1. 函数是程序中为了执行特定任务而存在的一系列执行代码,接收输入参数返回输出结果
  2. 头等函数(first class functions):支持头等函数的开发语言允许 将函数分配给变量;作为参数传递给其他函数;作为其他函数的返回值。go支持头等函数
    赋值给变量: 在 Go 中,可以将一个函数赋值给一个变量,然后像调用函数一样调用这个变量。这使得可以将函数存储在数据结构中,或者将其作为参数传递给其他函数。
func add(a, b int) int {
    return a + b
}

// 将函数赋值给变量
var sumFunc func(int, int) int = add //创建一个sumFunc变量,类型是 func(int, int) int,它指向了 add 函数。允许程序运行时动态地选择要调用的函数

// 调用存储在变量中的函数
result := sumFunc(3, 4) // result 现在是 7

作为参数传递给其他函数:可以将函数作为参数传递给其他函数,这样可以灵活地组合函数并执行不同的操作。

func operation(a, b int, op func(int, int) int) int {
    return op(a, b)
}

// 调用函数并传递另一个函数作为参数
result := operation(3, 4, add) // result 现在是 7

operation 函数接受三个参数:两个整数 a 和 b,以及一个函数 op,该函数接受两个整数并返回一个整数。在函数体内部,它调用传入的 op 函数,并将参数 a 和 b 传递给它。然后,operation 函数将 op 函数的结果作为自己的返回值返回。

作为其他函数的返回值 :函数可以作为另一个函数的返回值,这使得可以根据不同的条件返回不同的函数。

func chooseOperation(opType string) func(int, int) int {
    if opType == "add" {
        return add
    } else if opType == "subtract" {
        return subtract
    } else {
        panic("Unknown operation")
    }
}

// 调用返回函数的函数
addFunc := chooseOperation("add")
result := addFunc(3, 4) // result 现在是 7

函数 chooseOperation 接受一个字符串参数 opType,根据这个参数的值返回一个函数。具体地说,它返回一个接受两个整数并返回一个整数的函数。
如果 opType 的值是 “add”,那么 chooseOperation 返回 add 函数;如果 opType 的值是 “subtract”,那么返回 subtract 函数。否则,函数会抛出一个 panic,表示操作未知。

函数式编程的应用

闭包

在 Go 中,函数可以像其他类型的值一样被传递和赋值。当一个函数内部定义了另一个函数,并且这个内部函数引用了外部函数的变量时,就形成了一个闭包。闭包中的内部函数可以访问外部函数的局部变量,即使外部函数已经执行完毕,这些变量依然可以被内部函数访问和操作。

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    a := adder()

    fmt.Println(a(1)) // 输出:1
    fmt.Println(a(2)) // 输出:3
    fmt.Println(a(3)) // 输出:6
}

闭包的工作原理:

  1. 定义外部函数:首先,我们定义了一个外部函数 adder,它返回一个函数类型。
  2. 定义局部变量:在外部函数 adder 内部,我们定义了一个局部变量 sum,并将其初始化为 0。
  3. 返回匿名函数:在外部函数 adder 内部,我们返回了一个匿名函数。这个匿名函数接收一个 int 类型的参数,并返回一个 int 类型的结果。
  4. 引用外部变量:在匿名函数内部,它引用了外部函数 adder 中的变量 sum。这意味着匿名函数可以在调用时访问和修改 adder 函数中的 sum 变量。
  5. 调用匿名函数:在 main 函数中,我们调用了外部函数 adder 并将返回的匿名函数赋值给变量 a。然后,我们可以通过变量 a 来调用匿名函数,从而执行它。
  6. 保持状态:每次调用匿名函数时,它都会修改外部函数 adder 中的 sum 变量,并返回更新后的结果。由于闭包的存在,adder 函数中的 sum 变量的状态被保持了下来,即使 adder函数已经执行完毕。
    内部函数在定义时会捕获外部函数的变量,并保持对这些变量的引用,使得这些变量的状态在函数返回后仍然可以被访问和修改。

中间件

中间件是一种常见的软件设计模式,通常用于在不修改原始函数定义的情况下,为函数添加额外的功能或逻辑。在Web开发中,中间件经常用于处理HTTP请求,例如身份验证、日志记录、性能监控等。

package main

import "fmt"

// 目标函数
func greet(name string) string {
	return fmt.Sprintf("Hello, %s!", name)
}

// 中间件函数
func withLogging(next func(string) string) func(string) string {
	return func(name string) string {
		fmt.Println("Calling withLogging")
		result := next(name)
		fmt.Println("Finished withLogging")
		return result
	}
}

func main() {
	greetWithLogging := withLogging(greet)
	message := greetWithLogging("Alice")
	fmt.Println(message)
}

定义了一个名为 withLogging 的中间件函数。这个函数接收一个函数作为参数,并返回一个新的函数,该新函数会在调用原始函数之前和之后执行特定的操作,比如打印日志。
在示例中,我们将 greet 函数传递给 withLogging 函数,得到了一个新的函数 greetWithLogging。当我们调用 greetWithLogging 函数时,实际上是调用了经过中间件处理后的函数。
这种方式的好处在于,我们不需要修改 greet 函数的定义,只需要使用中间件函数包装一下即可。这样做使得代码更加模块化和可维护,也更容易扩展和修改功能。
中间件的工作原理:

  1. 定义目标函数(原始函数): 我们首先定义一个需要添加额外功能的目标函数。在示例中,目标函数是 greet,它接收一个名字作为参数,然后返回一个问候消息。
  2. 定义中间件函数: 然后我们定义一个中间件函数,比如 withLogging。这个函数接收一个函数作为参数,通常是目标函数,然后返回一个新的函数。
  3. 中间件函数内部操作: 在中间件函数内部,我们可以执行一些额外的操作,比如记录日志、验证输入、修改返回值等。在示例中,withLogging 函数内部打印了一条日志,然后调用了传入的函数。
  4. 返回新函数: 最后,中间件函数返回一个新的函数,通常是一个闭包。这个新函数执行了中间件函数内部的操作,然后调用了传入的函数,或者调用传入的函数的结果。
  5. 使用中间件: 最后,我们将目标函数传递给中间件函数,得到一个新的函数。我们可以像调用原始函数一样调用这个新函数,它会自动执行中间件函数内部的操作,然后调用原始函数。

函数类型对象

函数类型对象指的是可以对函数进行操作的对象。在 Go 中,函数也是一种类型,可以被赋值给变量、作为参数传递给其他函数、作为函数的返回值等。通过函数类型对象,我们可以对函数进行扩展,为其增加额外的功能。
例如可以定义一个函数类型对象,该对象的功能是在调用目标函数之前执行某些操作,然后再调用目标函数并返回其结果。这种方式可以实现一种类似于中间件的功能,为函数增加额外的行为。

package main

import "fmt"

// 定义一个函数类型
type OperationFunc func(int, int) int

// 目标函数
func add(a, b int) int {
	return a + b
}

// 函数类型对象,用于扩展目标函数的功能
func withLogging(next OperationFunc) OperationFunc {
	return func(a, b int) int {
		fmt.Println("Calling withLogging")
		result := next(a, b)
		fmt.Println("Finished withLogging")
		return result
	}
}

func main() {
	// 创建一个函数类型对象,扩展了目标函数的功能
	addWithLogging := withLogging(add)

	// 调用扩展后的函数
	result := addWithLogging(3, 4)
	fmt.Println("Result:", result)
}

addWithLogging 变量实际上是一个函数类型的对象,它是一个函数,具有与 add 函数相同的签名,但是在调用 add 函数之前和之后会打印日志。

闭包函数的陷阱

for循环中使用闭包,可能会导致问题
**原因:**在循环中使用闭包时,闭包函数可能会捕获循环变量的地址,而不是其值。当闭包函数被延迟执行时,它们引用的是循环结束后最后一个迭代时循环变量的值。这意味着所有闭包函数共享同一个循环变量,其值是最后一次迭代的值。
这种情况会导致问题,因为在循环结束后执行闭包函数时,闭包函数引用的循环变量已经具有最终的值,而不是在每次迭代时的值。这可能会导致意外的行为,特别是在期望闭包函数中使用循环变量的值与迭代时的值相同时。

package main

import "fmt"

func main() {
	var funcs []func()

	for i := 0; i < 3; i++ {
		funcs = append(funcs, func() {
			fmt.Println(i)
		})
	}

	for _, f := range funcs {
		f()
	}
}

创建了一个包含三个闭包函数的切片 funcs。在 for 循环中,通过 append 将每次迭代生成的闭包函数添加到切片中。闭包函数会打印变量 i 的值。

预期的输出可能是:

0
1
2

但实际上输出是:

3
3
3

在循环体中,闭包函数 func() { fmt.Println(i) } 没有立即执行,而是被添加到 funcs 切片中。因此,当 funcs 切片中的函数被执行时,它们引用的 i 已经在循环结束时是 3 了。因此,无论何时调用这些闭包函数,它们都会打印 3。

为了解决这个问题,可以在循环中创建一个局部变量,将每次迭代的值赋给这个局部变量,然后在闭包中使用这个局部变量。例如:

package main

import "fmt"

func main() {
	var funcs []func()

	for i := 0; i < 3; i++ {
		// 在每次迭代中创建一个局部变量,将 i 的值赋给它
		// 闭包函数捕获的是局部变量的值,而不是外部循环的引用
		j := i
		funcs = append(funcs, func() {
			fmt.Println(j)
		})
	}

	for _, f := range funcs {
		f()
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值