Go 中的函数式编程

2025-06-08

Go 中的函数式编程

由米歇尔·穆德斯撰写✏️

为什么要用 Go 进行函数式编程?简而言之,函数式编程让你的代码更易读、更易于测试,而且由于没有状态和可变数据,复杂性更低。如果遇到 bug,只要不违反函数式编程规则,你就可以快速调试你的应用。当函数被隔离时,你不必处理影响输出的隐藏状态变化。

软件工程师兼作家Eric Elliot对函数编程做出如下定义。

函数式编程是通过组合纯函数来构建软件的过程,避免共享状态、可变数据和副作用。函数式编程是声明式而非命令式的,应用程序状态通过纯函数进行传递。与面向对象编程不同,面向对象编程中的应用程序状态通常与对象中的方法共享并共存。

我更进一步说:函数式编程,就像面向对象编程和过程式编程一样,代表着一种范式转变。它在编写代码时强制了一种独特的思维方式,并引入了一整套需要遵循的新规则。

需要理解的4个重要概念

要完全掌握函数式编程,您必须首先了解以下相关概念。

  1. 纯函数和幂等性
  2. 副作用
  3. 函数组合
  4. 共享状态和不可变数据

让我们快速回顾一下。

LogRocket 免费试用横幅

1. 纯函数与幂等性

如果输入相同,纯函数始终会返回相同的输出。此属性也称为幂等性。幂等性意味着函数应该始终返回相同的输出,与调用次数无关。

2.副作用

其次,纯函数不能有任何副作用。换句话说,你的函数不能与外部环境交互。

例如,函数式编程将 API 调用视为副作用。为什么?因为 API 调用被视为不受你直接控制的外部环境。API 可能存在一些不一致的情况,例如超时或失败,甚至可能返回意外值。它不符合纯函数的定义,因为我们要求每次调用 API 时都能获得一致的结果。

其他常见的副作用包括:

  • 数据突变
  • DOM 操作
  • 请求冲突的数据,例如DateTime当前time.Now()

3. 函数组合

函数组合的基本思想很简单:将两个纯函数组合起来创建一个新函数。这意味着相同输入产生相同输出的概念在这里仍然适用。因此,从简单的纯函数开始创建更高级的功能非常重要。

4. 共享状态和不可变数据

函数式编程的目标是创建不保存状态的函数。共享状态尤其容易在纯函数中引入副作用或可变性问题,使其变得不纯。

然而,并非所有状态都是坏的。有时,为了解决某个软件问题,状态是必要的。函数式编程的目标是使状态可见且明确,以消除任何副作用。程序使用不可变数据结构,通过纯函数获取新数据。这样,就不需要可能引起副作用的可变数据了。

现在我们已经了解了基础知识,让我们定义一些在 Go 中编写函数代码时要遵循的规则。

函数式编程规则

正如我提到的,函数式编程是一种范式。因此,很难为这种编程风格定义确切的规则。而且,你并不总是能够完全遵循这些规则;有时,你确实需要依赖一个保存状态的函数。

但是,为了尽可能地遵循函数式编程范式,我建议遵循以下准则。

  • 没有可变数据以避免副作用
  • 无状态(或隐式状态,例如循环计数器)
  • 一旦变量被赋值,就不要修改它
  • 避免副作用,例如 API 调用

我们在函数式编程中经常遇到的一个好处是强大的模块化。函数式编程鼓励自下而上的编程风格,而不是自上而下的软件工程。首先,定义一些模块,将您预计未来会用到的类似纯函数组合在一起。接下来,开始编写这些小型、无状态、独立的函数来创建您的第一个模块。

我们本质上是在创建黑盒。稍后,我们将按照自下而上的方法将这些黑盒连接起来。这将帮助您构建强大的测试基础,尤其是用于验证纯函数正确性的单元测试。

一旦你对模块基础的稳固性有了信心,就该开始将这些模块整合在一起了。开发过程中的这一步还涉及编写集成测试,以确保两个组件的正确集成。

Go 中的 5 个函数式编程示例

为了更全面地了解 Go 函数式编程的工作原理,让我们来探讨五个基本示例。

1. 更新字符串

这是纯函数最简单的例子。通常,当你想更新一个字符串时,你会这样做:


Enter fullscreen mode Exit fullscreen mode

名称 := “名字”
名称 := 名称 + “姓氏”


Enter fullscreen mode Exit fullscreen mode

上面的代码片段不符合函数式编程的规则,因为变量在函数内部无法修改。因此,我们应该重写这段代码,让每个值都有自己的变量。

下面的代码片段更具可读性。


Enter fullscreen mode Exit fullscreen mode
firstname := "first"
lastname := "last"
fullname := firstname + " " + lastname
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

当查看非函数式代码片段时,我们必须仔细查看程序,确定其最新状态,name从而找到变量的最终值name。这需要花费更多的时间和精力来理解函数的功能。

接下来,让我们看看如何避免更新数组。

2. 避免更新数组

如前所述,函数式编程的目标是通过纯函数,使用不可变数据导出新的不可变数据状态。这也可以应用于数组,每次更新一个数组时,我们都会创建一个新的数组。

在非函数式编程中,像这样更新数组:


Enter fullscreen mode Exit fullscreen mode

名称:= [3]string{“汤姆”,“本”}

// Add Lucas to the array
names[2] = "Lucas"
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

让我们根据函数式编程范式来尝试一下。


Enter fullscreen mode Exit fullscreen mode
names := []string{"Tom", "Ben"}
allNames := append(names, "Lucas")
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

该示例使用原始names切片与append()函数结合向新数组添加额外的值。

3. 避免更新地图

这是函数式编程的一个比较极端的例子。假设我们有一个 map,它的键是字符串类型,值是整数类型。这个 map 保存着我们家里还剩下的水果数量。然而,我们刚买了苹果,想把它添加到列表中。


Enter fullscreen mode Exit fullscreen mode

水果:= map [string] int {“香蕉”:11}

// Buy five apples
fruits["apples"] = 5
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

我们可以在函数式编程范式下完成相同的功能。


Enter fullscreen mode Exit fullscreen mode
fruits := map[string]int{"bananas": 11}
newFruits := map[string]int{"apples": 5}

allFruits := make(map[string]int, len(fruits) + len(newFruits))


for k, v := range fruits {
    allFruits[k] = v
}


for k, v := range newFruits {
    allFruits[k] = v
}
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

由于我们不想修改原始映射,代码会循环遍历两个映射,并将值添加到新映射中。这样,数据就保持不变。

然而,从代码的长度就能看出,这段代码的性能比简单的可变更新 map 差很多,因为我们循环遍历了两个 map。这正是需要牺牲代码质量来换取代码性能的关键所在。

4. 高阶函数和柯里化

大多数程序员在他们的代码中并不经常使用高阶函数,但在函数式编程中建立柯里化很有用。

假设我们有一个简单的函数,用于将两个整数相加。虽然这已经是一个纯函数了,但我们想进一步阐述这个例子,以展示如何通过柯里化创建更高级的功能。

在这种情况下,我们只能接受一个参数。接下来,该函数将另一个函数作为闭包返回。由于该函数返回一个闭包,它会记住外部作用域,其中包含初始输入参数。


Enter fullscreen mode Exit fullscreen mode

func add(x int) func(y int) int {
return func(y int) int {
return x + y
}
}


Enter fullscreen mode Exit fullscreen mode

现在让我们尝试一下柯里化并创建更高级的纯函数。


Enter fullscreen mode Exit fullscreen mode

func main() {
// 创建更多变体
add10 := add(10)
add20 := add(20)

// Currying
fmt.Println(add10(1)) // 11
fmt.Println(add20(1)) // 21
Enter fullscreen mode Exit fullscreen mode

}


Enter fullscreen mode Exit fullscreen mode

这种方法在函数式编程中很常见,尽管在范式之外并不常见到它。

5.递归

递归是一种常用的软件模式,用于规避循环的使用。由于循环始终保持内部状态以了解其当前处于哪一轮,因此我们不能在函数式编程范式下使用它们。

例如,下面的代码片段尝试计算一个数字的阶乘。阶乘是一个整数与其下方所有整数的乘积。因此,4 的阶乘等于 24(= 4 * 3 * 2 * 1)。

通常,你会使用循环来实现这一点。


Enter fullscreen mode Exit fullscreen mode

func factorial(fac int) int {
result := 1
for ; fac > 0; fac-- {
result *= fac
}
返回 result
}


Enter fullscreen mode Exit fullscreen mode

为了在函数式编程范式中实现这一点,我们需要使用递归。换句话说,我们会一遍又一遍地调用同一个函数,直到达到阶乘的最小整数。


Enter fullscreen mode Exit fullscreen mode

func calculateFactorial(fac int) int {
如果 fac == 0 {
返回 1
}
返回 fac * calculateFactorial(fac - 1)
}


Enter fullscreen mode Exit fullscreen mode

结论

让我们总结一下关于函数式编程的知识:

  • Golang虽然支持函数式编程,但它的设计初衷并非如此,缺少Map、Filter、Reduce等函数就证明了这一点。
  • 函数式编程提高了代码的可读性,因为函数是纯粹的,因此易于理解
  • 纯函数更容易测试,因为没有可以改变输出的内部状态

要了解有关纯函数的用例及其重要性的更多信息,请查看这篇有关 Redux Reducer 对纯函数的需求的FreeCodeCamp 文章。

为了更好地了解函数式、过程式和面向对象编程之间的区别,或者如果你想了解哪种范式最适合你,我建议你阅读 Lili Ouaknin Felsen 的这篇富有洞察力的 Medium 文章


插件:LogRocket,一个用于 Web 应用的 DVR

 
LogRocket 仪表板免费试用横幅
 
LogRocket是一款前端日志工具,可让您重播问题,就像它们发生在您自己的浏览器中一样。您无需猜测错误发生的原因,也无需要求用户提供屏幕截图和日志转储,LogRocket 允许您重播会话以快速了解问题所在。它可与任何应用程序完美兼容,无论使用哪种框架,并且提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的更多上下文。
 
除了记录 Redux 操作和状态之外,LogRocket 还记录控制台日志、JavaScript 错误、堆栈跟踪、带有标头 + 正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,以记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能重现像素完美的视频。
 
免费试用


Go 中的函数式编程一文首先出现在LogRocket 博客上。

鏂囩珷鏉ユ簮锛�https://dev.to/bnevilleoneill/functional-programming-in-go-46gl
PREV
面向新手的 GraphQL + React
NEXT
从 create-react-app 到 PWA