Go Web 编程实战:利用闭包和函数字面量消除重复代码
在构建 Web 应用时,我们经常会发现自己在不同的处理函数(Handler)中编写相同的逻辑。比如,验证请求路径、提取参数、处理错误等。
在之前的代码中,我们的 viewHandler、editHandler 和 saveHandler 都必须在开头调用 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 时传入的 viewHandler 或 saveHandler)被这个闭包"捕获"并封闭在内部。无论这个闭包在哪里被调用,它都能访问到当初传入的那个 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)
}
总结
通过使用闭包,我们成功实现了一种类似"中间件"或"装饰器"的模式:
-
解耦:将通用的验证逻辑(URL 解析、错误处理)与具体的业务逻辑分离。
-
复用 :验证代码只写了一次(在
makeHandler中),却应用到了所有的 Handler 上。 -
安全:如果 URL 不合法,请求在进入业务逻辑之前就会被拦截。
这展示了 Go 语言函数式编程特性的强大之处,让代码更简洁、更易维护。