在之前的几篇文章中,我们从如何实现最简单的HTTP服务器,到如何对路由进行改进,到如何增加中间件。总的来讲,我们已经把Web服务器相关的内容大概梳理了一遍了。在这一篇文章中,我们将从最简单的一个main函数开始,慢慢重构,来研究如何把API设计的更加规范和具有扩展性。
1 构建一个Web应用
gin
MySQLRedis
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Result struct {
Success bool
Msg string
}
func Login (ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
//这里判断用户名密码的正确性
r := Result{false, "请求失败"}
if username != "" && password != "" {
r = Result{true, "请求成功"}
}
ctx.JSON(http.StatusOK, r)
}
func main() {
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
router.POST("/login", Login)
router.Run(":8000")
}
这是一个简单到不能再简单的登录接口了。请求之后的返回的结果如下:
{
"Success": true,
"Msg": "请求成功"
}
HandlerPOSTbody
然后应该在数据库中进行比对,在这里省略了这一步骤。
我们创建了一个结构体,作为返回的JSON结构。
gin
// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj})
}
Content-Typeapplication/json
注意,如果这里你的结构体字段第一个字母是小写,返回的json数据将为空。原因是这样的,这里调用了别的包的序列化方法,如果是小写的字段,在别的包无法访问,也就会造成返回数据为空的情况。
main
好,下面我们开始重构。
2 Handler
mainHandler
main
package main
import (
"github.com/gin-gonic/gin"
"hongjijun.com/helloworldGo/api/v1"
)
func main() {
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
router.POST("/login", v1.Login)
router.Run(":8080")
}
是不是感觉已经好很多了。
mainHandler
Handler
package v1
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Result struct {
Success bool
Msg string
}
func Login(ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
//这里判断用户名密码的正确性
r := Result{false, "请求失败"}
if username != "" && password != "" {
r = Result{true, "请求成功"}
}
ctx.JSON(http.StatusOK, r)
}
在这里我们发现这个包的代码还是不够整洁。
为什么呢,因为我们把返回结果也放到了这个包中。而返回结果,他应该是通用的。
既然是通用的,那我们就应该把它抽象出来。
3 Response
我们来看看此时包的结构:
common
来看看我们抽象出的response:
package response
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Result struct {
Success bool
Code int
Msg string
Data interface{}
}
func response(success bool, code int, msg string, data interface{}, ctx *gin.Context) {
r := Result{success, code, msg, data}
ctx.JSON(http.StatusOK, r)
}
func successResponse(data interface{}, ctx *gin.Context) {
response(true, 0, "请求成功", data, ctx)
}
func failResponse(code int, msg string, ctx *gin.Context) {
response(false, code, msg, nil, ctx)
}
func SuccessResultWithEmptyData(ctx *gin.Context) {
successResponse(nil, ctx)
}
func SuccessResult(data interface{}, ctx *gin.Context) {
successResponse(data, ctx)
}
func FailResultWithDefaultMsg(code int, ctx *gin.Context) {
failResponse(code, "请求失败", ctx)
}
func FailResult(code int, msg string, ctx *gin.Context) {
failResponse(code, msg, ctx)
}
简单来讲,就是设置了请求成功和请求错误的返回结果。在请求成功的返回结果中,有不返回数据的空结果以及返回了一些查询数据的结果。在失败的结果中,有默认的结果,和带具体信息的结果。
这些需要按照实际的情况来处理,这里只是做个示范。
successtruecode0successfalsecodesuccessResponsefailResponse函数
ginJSONresponse
注意,在这个response包中,只有返回结果的几个函数:SuccessResultWithEmptyData、SuccessResult、FailResultWithDefaultMsg、FailResult是给外部函数调用的,其他的函数是内部调用的。所以注意函数名第一个字母的大小写,来设置公有还是私有。
如图:
其余的任何函数,在外部都是无法调用的。
此时,我们再来看看Handler:
package v1
import (
"github.com/gin-gonic/gin"
"hongjijun.com/helloworldGo/common"
)
func Login(ctx *gin.Context) {
username := ctx.PostForm("username")
password := ctx.PostForm("password")
//这里判断用户名密码的正确性
if username != "" && password != ""{
response.SuccessResultWithEmptyData(ctx)
}
}
此时,无论在哪个Handler中,我们只需要调用response.Xxx,就能返回数据了。
到了这里,Handler部分基本上讲完了。但是作者在这里还没有实现对错误结果的抽象,你可以自己试试看。
4 服务启动
现在我们的main函数虽然比起之前简洁了不少:
func main() {
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
router.POST("/login", v1.Login)
router.Run(":8080")
}
但是,看起来整洁只是因为这里只有一个路由。
main
run.go
run.gomain
package application
import (
"github.com/gin-gonic/gin"
v1 "hongjijun.com/helloworldGo/api/v1"
)
func Run() {
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
router.POST("/login", v1.Login)
router.Run(":8080")
}
main
package main
import (
"hongjijun.com/helloworldGo/application"
)
func main() {
application.Run()
}
真的是越来越像Spring boot了(笑)
这样子的话,我们的应用入口就显得很简洁了。但是在Run函数中,依旧没有解决我们说的当路由增加之后的复杂性,我们继续往下重构。
5 Router
Run()
所以,我们应该把路由部分的服务抽象出来。
我们之间来看看效果:
package application
import (
"hongjijun.com/helloworldGo/application/initial"
)
func Run() {
router := initial.Router()
// 这里还可以创建其他的服务
// ...
router.Run(":8080")
}
Run()
initial.Router()
applicationinitialinitialrun.go
router.go
package initial
import (
"github.com/gin-gonic/gin"
"hongjijun.com/helloworldGo/router"
)
func Router() *gin.Engine{
//新建一个路由
router := gin.New()
//注册中间件
router.Use(gin.Logger(), gin.Recovery())
//设置一个分组,这里的分组是空的,是为了之后进行更细致的分组
api := router.Group("")
//加入用户管理类的路由
apirouter.InitMangerUserRouter(api)
// ...插入其他的路由
//返回
return router
}
很容易理解,在这个Router()方法中,定义了中间件,路由分组这些东西。
这里先解释一下:
我们先设置了一个空的路由分组,这个分组是作为根分组存在的。然后,我们把各个模块作为这个分组的子分组。举个例子:我们的项目中,有用户相关的模块,有订单相关的模块,那么这里的一个模块,就是一个分组,一个分组下面,有多个接口。
所以,我们就可以组成这些路由:
- /manageUser/register
- /manageUser/login
- /order/add
- /order/delete
所以,我们增加这样的目录:
router
apirouter.InitMangerUserRouter(api)/manageUser/*router
package apirouter
import (
"github.com/gin-gonic/gin"
v1 "hongjijun.com/helloworldGo/api/v1"
)
func InitMangerUserRouter(group *gin.RouterGroup) {
manageUserRouter := group.Group("manageUser")
manageUserRouter.POST("login", v1.Login)
// ...其他路由
}
manageUsermanageUser
loginmanageUser
6 整体文件结构
- api目录:所有的Handler
- application目录:应用所需的各种服务,如路由,持久化,缓存等等,然后由run.go统一启动
- common目录:公共资源,如抽象的返回结果等
- router目录:注册各种路由分组
- main.go:启动应用