仿照 Laravel 框架对 Go 路由处理器代码进行拆分

问题引入

到目前为止,虽然我们演示的代码逻辑都比较简单,所有的路由、处理器都是放在应用入口文件里的,如果构建的是更加复杂的、处理多个资源的应用,就会导致入口文件非常臃肿,即使是最简单的博客应用,也要处理文章、用户、图片等多个资源,所以我们需要对目前这种情况做优化。

Go 语言 Web 应用开发中,没有特定的控制器概念,但是我们可以参照其他语言 MVC 框架设计模式对代码结构进行拆分,以 Laravel 框架为例,官方建议随着业务逻辑变得复杂,我们需要把路由闭包定义的业务逻辑放到资源对应的控制器去实现,在 Go Web 开发中,我们完全也可以参照这种理念对代码结构进行调整。

我们假设要开发一个简单的博客应用,需要处理文章、用户两种资源,现在我们的目标是把两种资源对应的处理器方法拆分到不同文件去存放(不一定要定义不同的资源处理器类),并且为了代码组织结构更加清晰,我们顺手把服务器、路由器、路由定义、处理器方法都拆分开,这样会使得代码非常容易维护,也不会造成所有业务逻辑杂糅在一起,使得单个文件非常臃肿。

想法是好的,但具体怎么实现呢?其实也不难,无非把原来混在一起的逻辑按照规划的目标做拆分就好了。

项目初始化

我们依然基于 gorilla/mux 实现路由器,做路由匹配和请求分发,而且没有特别声明,后续 Web 开发教程都会使用它作为默认的路由器。接下来,我们创建一个空目录 github.com/xueyuanjun/goblog 作为新的项目根目录,在 goblog 目录下新建一个 handlers 目录存放所有的处理器及处理器方法(可以类比其他语言 MVC 框架中的控制器目录),然后创建一个 routes 目录用来存放路由定义和路由器实现,最后在 goblog 目录下创建 main.go 作为入口文件。

在开始编码之前,在 goblog 目录下运行如下代码初始化 Go Module,并将模块路径替换成本地路径以便 goblog 下的包在提交到 Github 之前可以正常被引用:

go mod init github.com/xueyuanjun/goblog
go mod edit -replace github.com/xueyuanjun/goblog=/path/to/goblog

注:请将上述第二条命令中的 /path/to/goblog 替换成 goblog 目录在系统中的绝对路径。

编写路由器实现

首先来实现路由器相关逻辑,在 routes 目录下创建 web.go,定义所有 Web 请求路由:

package routes

import "net/http"

// 定义一个 WebRoute 结构体用于存放单个路由
type WebRoute struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

// 声明 WebRoutes 切片存放所有 Web 路由
type WebRoutes []WebRoute

// 定义所有 Web 路由
var webRoutes = WebRoutes{
    
}

在这里,我们定义了一个 WebRoute 结构体来表示单个路由,其中包含了路由名称、请求方法、匹配字符串模式、以及对应的处理器方法,路由器可以根据这些配置请求请求分发。

然后定义了一个 WebRoutes 切片来存放所有 WebRoute 类型的路由,现在这个切片为空,表示还没有定义任何路由。

接下来,在 routes 目录下创建一个 router.go 用来定义路由器,编写路由器实现之前,先安装 gorilla/mux 依赖:

go get github.com/gorilla/mux

然后编写 router.go 实现代码如下:

package routes

import "github.com/gorilla/mux"

// 返回一个 mux.Router 类型指针,从而可以当作处理器使用
func NewRouter() *mux.Router {

    // 创建 mux.Router 路由器示例
    router := mux.NewRouter().StrictSlash(true)

    // 遍历 web.go 中定义的所有 webRoutes
    for _, route := range webRoutes {
        // 将每个 web 路由应用到路由器
        router.Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(route.HandlerFunc)
    }

    return router
}

我们在 NewRouter 方法中创建 mux.Router 示例并将 web.go 中定义的所有 Web 路由都应用到这个路由器,以便可以处理用户请求的路由匹配和分发,如果后续接入其他路由,比如 API 路由,也可以通过类似实现提供支持。

启动 Web 服务器

接下来,我们打开 goblog/main.go,基于上一步返回的路由器启动 Web 服务器:

package main

import (
     . "github.com/xueyuanjun/goblog/routes"
    "log"
    "net/http"
)

func main()  {
    startWebServer("8080")
}

func startWebServer(port string)  {
    r := NewRouter()
    http.Handle("/", r)

    log.Println("Starting HTTP service at " + port)
    err := http.ListenAndServe(":"+port, nil) // Goroutine will block here

    if err != nil {
        log.Println("An error occured starting HTTP listener at port " + port)
        log.Println("Error: " + err.Error())
    }
}

我们将 Web 服务器启动逻辑封装到 startWebServer 方法中实现,该方法需要传入端口参数。在具体实现时,我们调用了 routes/router.go 中定义的 NewRouter 方法,将其返回值作为处理器传入 http.Handle 方法,最后调用 http.ListenAndServe 启动 Web 服务器并监听传入的端口号。

最后在 main 方法中调用 startWebServer 方法即可。

定义路由和处理器方法

至此上层代码都已经编写完了,现在只要有路由和对应的处理器方法就可以启动 Web 服务器处理用户请求了。

我们在 handlers 目录下分别创建三个文件:common.gopost.gouser.go,分别用于处理通用请求、文章资源和用户资源,首先在 common.go 中编写首页请求处理器方法:

package handlers

import (
    "io"
    "net/http"
)

func Home(w http.ResponseWriter, r *http.Request)  {
    io.WriteString(w, "Welcome to my blog site")
}

然后在 post.go 定义文章列表页对应处理器方法:

package handlers

import (
    "io"
    "net/http"
)

func GetPosts(w http.ResponseWriter, r *http.Request)  {
    io.WriteString(w, "All posts")
}

以及在 user.go 中定义获取指定用户对应处理器方法:

package handlers

import (
    "github.com/gorilla/mux"
    "io"
    "net/http"
)

func GetUser(w http.ResponseWriter, r *http.Request)  {
    // Get user from DB by id...
    params := mux.Vars(r)
    id := params["id"]
    io.WriteString(w, "Return user info with id = " + id)
}

由于只是简单示例,所以业务逻辑都非常简单。

接下来,就可以在 routes/web.go 中添加路由了:

// 定义所有 Web 路由
var webRoutes = WebRoutes{
    WebRoute{
        "Home",
        "GET",
        "/",
        handlers.Home,
    },
    WebRoute{
        "Posts",
        "GET",
        "/posts",
        handlers.GetPosts,
    },
    WebRoute{
        "User",
        "GET",
        "/user/{id}",
        handlers.GetUser,
    },
}

至此,所有代码都编写完了,现在整个项目的目录结构如下所示:

-w550

测试路由访问

这样一来,你就可以在 goblog 根目录下运行 go run main.go 启动 Web 服务器然后访问这些路由了:

-w542

当然,你也可以为每次处理器文件编写测试代码,然后执行 HTTP 测试,关于 HTTP 测试我们在上篇教程中已经介绍过,这里就不单独演示了。

上一篇: 基于 gorilla/mux 实现路由匹配和请求分发:健康检查与接口测试

下一篇: Go 语言通过 Request 对象读取 HTTP 请求报文