Go函数闭包实战-复用函数

Go Web 编程实战:利用闭包和函数字面量消除重复代码

在构建 Web 应用时,我们经常会发现自己在不同的处理函数(Handler)中编写相同的逻辑。比如,验证请求路径、提取参数、处理错误等。

在之前的代码中,我们的 viewHandlereditHandlersaveHandler 都必须在开头调用 getTitle 来获取页面标题并进行错误检查。这种重复代码不仅显得臃肿,而且一旦验证逻辑发生变化,我们需要修改所有的地方。

那么,如果我们能把这些通用的验证逻辑"包装"起来,只写一次,该多好?

Go 语言的 函数字面量 (Function Literals)闭包 (Closures) 正是解决这个问题的利器。

1. 基础工作:使用正则表达式进行安全验证

在优化代码结构之前,我们先解决一个严重的安全隐患:用户可能会提供任意路径来读取或写入服务器上的文件。

为了防止这种情况,我们需要验证请求的 URL 是否合法。我们引入 regexp 包,并定义一个全局变量 validPath

Go

复制代码
import "regexp"

// 预编译正则表达式,如果编译失败会直接 Panic
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

这里使用 MustCompile 而不是 Compile,是因为正则表达式是硬编码的,如果编译失败说明代码有问题,程序不应该启动。

接下来,我们需要一个辅助函数来验证路径并提取标题:

Go

复制代码
func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
    m := validPath.FindStringSubmatch(r.URL.Path)
    // 如果匹配失败(m 为 nil),说明路径非法
    if m == nil {
        http.NotFound(w, r)
        return "", errors.New("invalid Page Title")
    }
    return m[2], nil // 返回正则匹配到的第二个子表达式(即标题部分)
}

虽然这样解决了安全问题,但我们必须在每个 Handler 里都调用一遍 getTitle,代码重复率很高。接下来,让我们用闭包来重构它。

2. 重构目标:改变 Handler 的签名

首先,我们需要调整现有的处理函数。既然我们要把"提取 Title"的逻辑抽离出去,那么这些 Handler 就不再需要自己去解析 Title,而是应该直接接收 Title 作为参数。

我们将函数的定义从标准的 (w, r) 修改为包含 title 的形式:

Go

复制代码
func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)

3. 核心武器:编写包装函数 (Wrapper Function)

现在,我们需要一个"中间人"。这个函数接收上面那种包含 title 参数的函数,然后返回一个标准的 http.HandlerFunc(即 func(http.ResponseWriter, *http.Request)),以便能被 http.HandleFunc 调用。

我们把这个包装函数命名为 makeHandler

Go

复制代码
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 1. 在这里统一进行正则验证和 Title 提取
        m := validPath.FindStringSubmatch(r.URL.Path)
        
        // 2. 如果验证失败,直接返回 404,不再执行后续逻辑
        if m == nil {
            http.NotFound(w, r)
            return
        }
        
        // 3. 验证通过,调用真正的业务逻辑 fn,并将提取出的 Title (m[2]) 传进去
        fn(w, r, m[2])
    }
}

什么是闭包?

上面代码中返回的那个匿名函数就是一个闭包 。因为它在函数体内引用了定义在它外部的变量 fn

在这个例子中,fn 变量(即我们在调用 makeHandler 时传入的 viewHandlersaveHandler)被这个闭包"捕获"并封闭在内部。无论这个闭包在哪里被调用,它都能访问到当初传入的那个 fn

4. 在 Main 函数中应用

有了 makeHandler,我们的 main 函数变得更加语义化了。我们在注册路由时,直接用 makeHandler 包裹我们的业务逻辑:

Go

复制代码
func main() {
    // 使用 makeHandler 包装具体的业务逻辑
    http.HandleFunc("/view/", makeHandler(viewHandler))
    http.HandleFunc("/edit/", makeHandler(editHandler))
    http.HandleFunc("/save/", makeHandler(saveHandler))

    log.Fatal(http.ListenAndServe(":8080", nil))
}

5. 享受成果:更纯粹的业务逻辑

最后,我们可以移除具体 Handler 中所有关于 getTitle 的调用和错误检查代码。现在的 Handler 只关注具体的业务逻辑(显示、编辑或保存),变得非常清爽:

Go

复制代码
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, http.StatusFound)
        return
    }
    renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request, title string) {
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err := p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

总结

通过使用闭包,我们成功实现了一种类似"中间件"或"装饰器"的模式:

  1. 解耦:将通用的验证逻辑(URL 解析、错误处理)与具体的业务逻辑分离。

  2. 复用 :验证代码只写了一次(在 makeHandler 中),却应用到了所有的 Handler 上。

  3. 安全:如果 URL 不合法,请求在进入业务逻辑之前就会被拦截。

这展示了 Go 语言函数式编程特性的强大之处,让代码更简洁、更易维护。

相关推荐
renke33643 小时前
Flutter 2025 跨平台工程体系:从 iOS/Android 到 Web/Desktop,构建真正“一次编写,全端运行”的产品
android·flutter·ios
ChineHe4 小时前
Gin框架基础篇002_获取/绑定请求参数
后端·golang·gin
Zender Han5 小时前
Flutter 中 AbsorbPointer 与 IgnorePointer 的区别与使用场景详解
android·flutter·ios
天下一般6 小时前
go语言设计模式<一>模板方法
开发语言·设计模式·golang
码界奇点7 小时前
基于Go语言的AI接口管理与分发系统设计与实现
开发语言·人工智能·ai·golang·毕业设计·go语言·源代码管理
bybitq7 小时前
深入浅出 Go 流程控制:从循环到延迟执行
开发语言·后端·golang
Digitally7 小时前
3种简单方法备份 iPhone 短信
ios·iphone