仿照 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.go
、post.go
、user.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,
},
}
至此,所有代码都编写完了,现在整个项目的目录结构如下所示:
测试路由访问
这样一来,你就可以在 goblog
根目录下运行 go run main.go
启动 Web 服务器然后访问这些路由了:
当然,你也可以为每次处理器文件编写测试代码,然后执行 HTTP 测试,关于 HTTP 测试我们在上篇教程中已经介绍过,这里就不单独演示了。
5 Comments
在 routes/web.go 文件中 还需要加入 import “github.com/xueyuanjun/goblog/handlers”
如果不加引入的话 报 undefined: handlers 错误
路由分组和路由中间件还是挺麻烦的
不太明白返回一个 mux.Router 类型指针,从而可以当作处理器使用 这句话,为什么返回是指针类型,就可以当做handleFunc了呢
1.
handler
方法的第二个参数是一个Handler
接口,所以此处传入的处理器实例要实现Handler
接口的ServeHTTP
方法才行;而mux.NewRouter
是实现了Handler
接口的,所以router.go
文件中的NewRouter()
方法才返回了mux.NewRouter
的路由实例;2.其次,mux包中的
NewRouter()
方法定义的返回值就是指针类型;mux.NewRouter
实现Handler
接口的ServeHTTP
方法时的返回值也是*Router
类型,记得接口篇(二)
中讲过类型*Router
存在所有Handler
接口中声明的方法,严格来说,只有*Router
类型实现了Handler
接口;3.这里没有当做handlerFunc,它就是handler实例;
so,以上是我个人理解,贴出来交流,不对的地方需要学院君来讲解了..
http.Handle("/", r)这段代码可注释掉,替换成err := http.ListenAndServe(":"+port, r)。如果查看源码就回发现ListenAndServe这个方法对handler进行了判断不存在时,hander=DefaultServeMux; 而http.Handler的方法也是对DefaultServeMux的设置