zoukankan      html  css  js  c++  java
  • Golang Web入门(4):如何设计API

    摘要

    在之前的几篇文章中,我们从如何实现最简单的HTTP服务器,到如何对路由进行改进,到如何增加中间件。总的来讲,我们已经把Web服务器相关的内容大概梳理了一遍了。在这一篇文章中,我们将从最简单的一个main函数开始,慢慢重构,来研究如何把API设计的更加规范和具有扩展性。

    1 构建一个Web应用

    我们从最简单的开始,利用gin框架实现一个小应用。

    在这这篇文章中,我先不使用MySQLRedis,缓存和持久化相关的内容我将在以后的文章中提到。在这个系列中,我们主要还是聊聊与Web有关的内容。

    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": "请求成功"
    }
    

    在这个Handler中的逻辑是这样的:获取POST请求中的body参数,得到了用户传到后台的用户名和密码。

    然后应该在数据库中进行比对,在这里省略了这一步骤

    我们创建了一个结构体,作为返回的JSON结构

    最后调用了gin的JSON方法返回数据。这里的第一个参数是HTTP状态码,第二个参数是需要返回的数据。我们来看看这个JSON方法:

    // 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})
    }
    

    意思是,会把返回的数据序列化为JSON类型,并且把Content-Type设置为application/json

    注意,如果这里你的结构体字段第一个字母是小写,返回的json数据将为空。原因是这样的,这里调用了别的包的序列化方法,如果是小写的字段,在别的包无法访问,也就会造成返回数据为空的情况。
    

    但是你有没有发现,把全部业务逻辑都丢到main函数的做法简直太不优雅了!所有的业务逻辑都耦合在一起,没有做到“一个函数实现一个功能”的目标。

    好,下面我们开始重构

    2 Handler

    既然所有的函数都在main函数中,我们不如先把Handler转移出来,单独作为一个包。

    这时候我们来看看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")
    }
    

    是不是感觉已经好很多了。

    main函数中,主要就是注册路由,而其余的Handler,则保存在其他的包中。

    我们继续看看我们的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)
    }
    

    简单来讲,就是设置了请求成功请求错误的返回结果。在请求成功的返回结果中,有不返回数据的空结果以及返回了一些查询数据的结果。在失败的结果中,有默认的结果,和带具体信息的结果。

    这些需要按照实际的情况来处理,这里只是做个示范。

    注意,因为在返回的结果中,成功的结果successtruecode0,而失败的结果successfalsecode需要按照项目的规划来设定,所以作者在这里又做了一层抽象,设置了successResponsefailResponse函数

    而这两个函数都会调用gin上下文中的JSON方法,所以将这里的返回再次抽象,抽象出了response函数。

    注意,在这个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的文件(借鉴了Spring boot的结构)。

    这个run.go的代码,就是原来main函数里面的代码:

    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()这个函数中,是为了启动服务。这里说的服务,不仅仅是指现在在操作的路由,还有其他的服务,比如数据库连接池,Redis等等。

    所以,我们应该把路由部分的服务抽象出来。

    我们之间来看看效果:

    package application
    
    import (
    	"hongjijun.com/helloworldGo/application/initial"
    )
    
    func Run() {
    	router := initial.Router()
    	
    	// 这里还可以创建其他的服务
    	// ...
    	
    	router.Run(":8080")
    }
    
    

    注意看,我们的路由处理,已经被挪到了其他位置了。在这个Run()函数中,我们只需要获取路由,然后执行,别的操作,不应该由这个函数来完成

    然后我们再来看看initial.Router()这个函数。

    注意看,我在application这个目录下,新建了一个叫initial的目录,这个initial目录和我们的run.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)
    	// ...其他路由
    }
    

    在这个注册路由分组的函数中,我们先把分组设置为manageUser,表示下面的路由都会拼接在manageUser后面。

    然后,我们在这里注册了login,并且,在这里还可以继续写属于manageUser这个模块的其他路由。

    6 整体文件结构

    • api目录:所有的Handler
    • application目录:应用所需的各种服务,如路由,持久化,缓存等等,然后由run.go统一启动
    • common目录:公共资源,如抽象的返回结果等
    • router目录:注册各种路由分组
    • main.go:启动应用

    7 写在最后

    首先,谢谢你能看到这里~

    在这一篇的文章中,我主要是总结了前面三篇文章的内容,构建了一个Web应用的Demo。这里面很多都是我自己对于Web应用结构的理解,不一定对,也不一定合适,主要是做一个示范,希望能够对你的学习起到一些启发启发作用。也希望你可以指出我的错误,我们一起进步~

    到了这里,《Golang Web入门》系列就结束了,谢谢你们的支持。之前你们的关注和点赞,都是对我特别大的鼓励。也非常感谢你们在发现了错误之后的留言,让我知道了自己理解有误的地方。(鞠躬~

    PS:如果有其他的问题,也可以在公众号找到作者。并且,所有文章第一时间会在公众号更新,欢迎来找作者玩~

  • 相关阅读:
    php socket 发送HTTP请求 POST json
    登录令牌 Token 介绍
    如何打开rdb文件
    Web登录其实没那么简单
    爆款小程序是如何诞生的?
    如何在小程序上增加音视频?
    拒绝“割韭菜”— 谈谈区块链正经的商用场景!
    想知道微信怎么做指纹支付开发?看这里!
    游戏安全有多重要?——GAME-TECH游戏开发者技术沙龙
    嘿,OCR文字识别了解下!
  • 原文地址:https://www.cnblogs.com/hongjijun/p/12779745.html
Copyright © 2011-2022 走看看