函数式编程在Golang中的应用
posts/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B%E5%9C%A8golang%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8在之前的文章中已经谈论过函数式编程了,这里就不在过多赘述。写这篇文章主要是对自己过去使用 Golang 对遇到的代码问题进行总结。由于 Golang 自身是一门极简的抽象语言,加载上自己在写代码的同时总是在追求最佳实践,查了许多资料总结了下 Golang 中的函数式编程,接下来我们将讨论 Golang 中函数式编程的一些常见形式。通过了解这些常见形式可以让我们今后更高效阅读他人的源码或编写自己的代码。
常见形式
纯函数 (Pure Functions)
在 Go 语言中,许多内置函数默认就是纯函数,例如 append()
或 strings.Replace()
等。这类函数总是在相同的输入下,总是返回相同的输出,并且没有副作用的函数。这意味着在函数的执行过程中,不会对程序的状态产生任何影响,也不会与外部环境发生交互。
遵循函数式编程的惯例,我们应该避免更新原始变量,而是创建新的变量。例如,对于字符串操作和数组操作,我们应该尽量避免直接修改原始值,而是通过创建新的值来完成操作:
// 错误的
name := "Geison"
name := name + " Flores"
// 正确的
const firstName = "Geison"
const lastName = “Flores”
const name = firstName + “ “ + lastName
又或者是对于数组:
// 错误的
years := [4]int{2001, 2002}
years[2] = 2003
years[3] = 2004
// 正确的
years := [2]int{2001, 2002}
allYears := append(years, 2003, [2]int{2004, 2005})
对于 Map 等数据结构的操作也应该遵循相同的原则。
除了使用内置的纯函数外,我们还可以编写自己的纯函数来实现复杂的逻辑。通过结合 Golang 的测试功能,一旦函数通过测试,就能保证其在多个场景下的可靠性和稳定性,从而提高了编码效率和代码的可读性。
闭包 (Closure)
区别闭包函数与普通函数的本质是分配堆上还是分配栈上的问题。闭包函数由于分配到堆上的特性导致函数调用后内部的变量不会发生内存逃逸从而不会被释放掉,从而实现了持久化的效果。
比如下面的计数器例子:
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
c1 := counter()
fmt.Println(c1()) // 输出:1
fmt.Println(c1()) // 输出:2
fmt.Println(c1()) // 输出:3
c2 := counter()
fmt.Println(c2()) // 输出:1
fmt.Println(c2()) // 输出:2
}
在上述示例中,counter
函数返回了一个闭包函数,每次调用该闭包函数都会增加一个计数值并返回。这个计数值被保存在count
变量中,由于闭包函数捕获了count
变量,所以在每次调用闭包函数时,count
的值都会被保留并更新。
使用闭包函数可以实现许多功能,比如统计函数调用次数、延迟执行等。
装饰器 (Decorator)
如果你之前使用过 Python 的话,那么对装饰器是再熟悉不过了。通过在函数签名上使用@xxx
语法,可以为函数赋予新的功能,而装饰器的实现本质上也是通过闭包来实现的。
让我们看一个利用装饰器来检测函数执行时间的例子:
func timeDecorator(f func()) func() {
funcName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
return func() {
fmt.Printf("开始执行函数 %s...\n", funcName)
start := time.Now()
f()
fmt.Printf("函数 %s 执行完毕,耗时: %s\n", funcName, time.Since(start))
}
}
func myFunc() {
time.Sleep(2 * time.Second)
fmt.Println("函数执行中...")
}
func main() {
decoratedFunction := timeDecorator(myFunc)
decoratedFunction()
}
利用装饰器,我们可以很容易地实现函数执行时间的测量,而且不需要在原始函数中插入额外的代码。这使得我们的代码更加整洁、易于调试,并且可以轻松地为函数添加新的功能而不需要修改原始函数的代码。
可选函数 (Optional Functional)
本结引用Go 编程模式:Functional Options | 酷 壳 - CoolShell中 Server 配置的问题。
下面我们讨论如何配置下面的这个Server
结构体(对象):
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
TLS *tls.Config
}
当然你可以多写一个构造函数包含各种参数都 New 一遍,但是对于 Golang 这种不支持函数重载的语言(据说会影响性能)调用的时候应该会麻烦。
又或者可以使用类似 Java 的 Builder 模式(链式调用)使用的时候长这样:
sb := ServerBuilder{}
server, err := sb.Create("127.0.0.1", 8080).
WithProtocol("udp").
WithMaxConn(1024).
WithTimeOut(30*time.Second).
Build()
但是如果构建过程出错需要返回错误信息,又要往 Server 这个对象里面添加错误属性(引入了无关信息),显然很麻烦(当然不做错误处理的话我觉得还好)。
所以有了如下 Optional Functional 的操作:
type Option func(*Server)
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConns(maxconns int) Option {
return func(s *Server) {
s.MaxConns = maxconns
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
MaxConns: 1000,
TLS: nil,
}
for _, option := range options {
option(&srv)
}
//...
return &srv, nil
}
这次我们再调用:
s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))
这样就解决了之前处理错误的缺点。主要还是对于代码呈现以及后期维护提供了很大的方便。
函数数组 (Array of functions)
函数数组虽然不属于函数式编程思想的核心,但其也利用到了头等函数这一性质,将同样签名的函数组合为数组循环调用。不过就有点偏向于事件驱动了,不过这个使用方法还是值得学习的。下面是一个使用函数数组模拟 GUI 中触发事件的代码:
type EventType int
const (
Click EventType = iota
Hover
DoubleClick
)
func handleClick(eventData any) {
fmt.Println("Handling click event:", eventData)
}
func handleHover(eventData any) {
fmt.Println("Handling hover event:", eventData)
}
func handleDoubleClick(eventData any) {
fmt.Println("Handling double click event:", eventData)
}
func main() {
eventHandlers := map[EventType]func(any){
Click: handleClick,
Hover: handleHover,
DoubleClick: handleDoubleClick,
}
eventData := "Some event data"
eventTypes := []EventType{Click, Hover, DoubleClick}
for _, eventType := range eventTypes {
handler, ok := eventHandlers[eventType]
if ok {
handler(eventData)
} else {
fmt.Println("No handler found for event type:", eventType)
}
}
}
函数数组在某些场景下可以很方便地使用,比如 Telegram Bot 事件的处理。但函数数组中的函数必须具有相同的签名(即接受相同类型的参数并返回相同类型的结果)。
高阶函数 (Higher-order Functions)
高阶函数是指能够接受函数作为参数或返回函数作为结果的函数。在函数式编程中,高阶函数是一种非常重要的概念,它可以帮助我们实现代码的抽象、复用和灵活性。上文中提到的闭包,装饰器和可选函数都涉及到了高阶函数,不过本节只介绍 Map,Filter 和 Reduce 的使用。
映射 (Map)
在函数式编程中,Map
函数接受一个函数和一个列表作为参数,然后将该函数应用于列表中的每个元素,并返回一个新的列表,其中每个元素都是原始列表中对应元素经过函数处理后的结果。这类似于数学中的映射,即将一个集合中的每个元素映射到另一个集合中的对应元素。
func Map(f func(int) int, arr []int) []int {
result := make([]int, len(arr))
for i, v := range arr {
result[i] = f(v)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
// 对原始数组中的每个元素进行平方操作
squared := Map(func(x int) int { return x * x }, numbers)
fmt.Println("Squared:", squared)
}
过滤 (Filter)
在函数式编程中,Filter
函数接受一个函数和一个列表作为参数,然后根据函数的条件判断,过滤出列表中满足条件的元素,并返回一个新的列表。
func Filter(f func(int) bool, arr []int) []int {
result := make([]int, 0)
for _, v := range arr {
if f(v) {
result = append(result, v)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
// 过滤出原始数组中的偶数元素
evens := Filter(func(x int) bool { return x%2 == 0 }, numbers)
fmt.Println("Evens:", evens)
}
规约 (Reduce)
在函数式编程中,Reduce
函数接受一个函数、一个初始值和一个列表作为参数,然后将函数应用于列表中的所有元素,并使用初始值进行累积,最终返回一个结果。
func Reduce(f func(int, int) int, initial int, arr []int) int {
result := initial
for _, v := range arr {
result = f(result, v)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
// 对原始数组中的所有元素进行累加
sum := Reduce(func(acc, x int) int { return acc + x }, 0, numbers)
fmt.Println("Sum:", sum)
}
柯里化和偏函数 (Currying and Partial Functions)
柯里化(Currying)是将多参数函数转换为一系列单参数函数的技术,使得函数可以以链式调用的方式被调用,增加了函数的灵活性和复用性。偏函数(Partial Functions)是从一个多参数函数中创建一个新函数,固定部分参数值而不传递所有参数,简化了函数的使用和组合。
下面是一个典型的例子:
func plus(x, y int) int {
return x + y
}
func partialPlus(x int) func(int) int {
return func(y int) int {
return plus(x, y)
}
}
func main() {
plus_one := partialPlus(1)
fmt.Println(plus_one(5)) //prints 6
}
上面的例子看上去有些无聊,下面来个更务实的版本:
假设有一个电商平台,用户可以根据不同的商品类别来筛选商品列表。我们可以编写一个函数来实现商品筛选的功能,该函数接受商品类别作为参数,并返回符合条件的商品列表。
type Product struct {
ID int
Name string
Category string
Price float64
}
// 根据商品类别筛选商品列表
func filterByCategory(products []Product, category string) []Product {
var filtered []Product
for _, p := range products {
if p.Category == category {
filtered = append(filtered, p)
}
}
return filtered
}
// 返回一个偏函数,用于筛选指定类别的商品列表
func partialFilterByCategory(category string) func([]Product) []Product {
return func(products []Product) []Product {
return filterByCategory(products, category)
}
}
func main() {
products := []Product{
{ID: 1, Name: "iPhone 11", Category: "手机", Price: 5999.99},
{ID: 2, Name: "iPhone 12", Category: "手机", Price: 6999.99},
{ID: 3, Name: "MacBook Pro", Category: "电脑", Price: 9999.99},
{ID: 4, Name: "AirPods Pro", Category: "耳机", Price: 1299.99},
{ID: 5, Name: "iPad Air", Category: "平板电脑", Price: 4999.99},
}
// 使用偏函数筛选电子产品
filterPhone := partialFilterByCategory("手机")
phones := filterPhone(products)
fmt.Println("手机列表:", phones) // [{1 iPhone 11 手机 5999.99} {2 iPhone 12 手机 6999.99}]
}
在上面的例子中,我们使用偏函数 partialFilterByCategory
来创建一个用于筛选电子产品的函数 filterPhone
。然后,我们将原始商品列表传递给这个偏函数,即可得到符合条件的手机列表。这种方式使得我们可以方便地复用筛选函数,并且能够在需要时灵活地变换参数。
总结
本篇探讨了在 Golang 中应用函数式编程的一些常见形式。通过了解这些常见形式,我们可以更好地理解和应用函数式编程的思想,从而提高代码的质量和效率。当然也不能拿着锤子看哪里都想钉子,结合情况适度使用。