Golang Web入门(4):如何设计API
摘要
在之前的几篇文章中,我们从如何实现最简单的HTTP服务器,到如何对路由进行改进,到如何增加中间件。总的来讲,我们已经把Web服务器相关的内容大概梳理了一遍了。在这一篇文章中,我们将从最简单的一个main函数开始,慢慢重构,来研究如何把API设计的更加规范和具有扩展性。
1 构建一个Web应用
我们从最简单的开始,利用gin
框架实现一个小应用。
在这这篇文章中,我先不使用MySQL
和Redis
,缓存和持久化相关的内容我将在以后的文章中提到。在这个系列中,我们主要还是聊聊与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)
}
简单来讲,就是设置了请求成功和请求错误的返回结果。在请求成功的返回结果中,有不返回数据的空结果以及返回了一些查询数据的结果。在失败的结果中,有默认的结果,和带具体信息的结果。
这些需要按照实际的情况来处理,这里只是做个示范。
注意,因为在返回的结果中,成功的结果success
为true
,code
为0
,而失败的结果success
为false
,code
需要按照项目的规划来设定,所以作者在这里又做了一层抽象,设置了successResponse
和failResponse函数
。
而这两个函数都会调用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:如果有其他的问题,也可以在公众号找到作者。并且,所有文章第一时间会在公众号更新,欢迎来找作者玩~
Golang Web入门(4):如何设计API的更多相关文章
- Golang Web入门(1):自顶向下理解Http服务器
摘要 由于Golang优秀的并发处理,很多公司使用Golang编写微服务.对于Golang来说,只需要短短几行代码就可以实现一个简单的Http服务器.加上Golang的协程,这个服务器可以拥有极高的性 ...
- Golang Web入门(3):如何优雅的设计中间件
摘要 在上一篇文章中,我们已经可以实现一个性能较高,且支持RESTful风格的路由了.但是,在Web应用的开发中,我们还需要一些可以被扩展的功能. 因此,在设计框架的过程中,应该留出可以扩展的空间,比 ...
- Golang Web入门(2):如何实现一个高性能的路由
摘要 在上一篇文章中,我们聊了聊在Golang中怎么实现一个Http服务器.但是在最后我们可以发现,固然DefaultServeMux可以做路由分发的功能,但是他的功能同样是不完善的. 由Defaul ...
- WEB入门.五 页面设计简介
学习内容 Ø XHTML 的发展历程 Ø XHTML 和 HTML 的区别 Ø XHTML的DOCTYPE和基本标签 Ø CSS 常用属性 能力 ...
- golang web框架设计7:整合框架
把前面写好的路由器,控制器,日志,都整合在一起 全局变量和初始化 定义一些框架的全局变量 var ( BeeApp *App AppName string AppPath string StaticD ...
- golang web框架设计6:上下文设计
context,翻译为上下文,为什么要设计这个结构?就是把http的请求和响应,以及参数结合在一起,便于集中处理信息,以后框架的扩展等.好多框架比如gin,都是有这个上下文结构. context结构为 ...
- golang web框架设计5:配置设计
配置信息的解析,实现的是一个key=value,键值对的一个配置文件,类似于ini的配置格式,然后解析这个文件,把解析的数据保存到map中,最后调用的时候通过几个string,int之类的函数返回相应 ...
- golang web框架设计4:日志设计
beego的日志设计思路来自于seelog,根据不同的level来记录日志,beego设计的日志是一个轻量级的,采用系统log.Logger接口,默认输出到os.Stdout,用户可以实现这个接口然后 ...
- golang web框架设计3:controller设计
继续学习golang web框架设计 controller作用 MVC设计模式里面的这个C,控制器. Model是后台返回的数据: View是渲染页面,通常是HTML的模板页面: Controller ...
随机推荐
- Java&Spring过时的经典语录
字符串拼接:请用StringBuffer代替String直接相加提高性能 过去的理论 有没有人告诉过你开发中不要 String newString = "牛郎"+"织 ...
- Java数据类型与mysql对应表
- 不可被忽视的操作系统( FreeRTOS )【1】
把大多数人每个星期的双休过过成了奢侈的节假日放假,把每天23点后定义为自己的自由时间,应该如何去思考这个问题 ? 双休的两天里,不!是放假的两天里,终于有较长的时间好好的学习一下一直断断续续的Free ...
- 线程状态以及sleep yield wait join方法
前言 在日常的开发过程中,我们通过会使用Thread.sleep模拟一个耗时的任务执行过程. 在深入理解这四个方法之前,首先对线程的状态进行理解阐述. 线程概念 线程是操作系统执行任务的基本单位,处理 ...
- Activiti网关--排他网关
排他网关 1.什么是排他网关 排他网关(也叫异或(XOR)网关,或叫基于数据的排他网关),用来在流程中实现决策. 当流程执行到这个网关,所有分支都会判断条件是否为true,如果为 true 则执行该分 ...
- Sql练习201908131742
orderdt_jimmy表结构: sql查询: then amount end) t1, then amount end) t2, then amount end) t3 from orderdt_ ...
- web页面调用支付宝支付
web页面调用支付宝支付 此文章是前端单独模拟完成支付,若在线上环境则需要后台配合产生签名等参数 在蚂蚁金服开放平台申请沙箱环境 将沙箱环境中的密钥.应用网关.回调地址补全,生成密钥的方法在此 配置好 ...
- ThinkPHP6.0学习笔记-模型操作
ThinkPHP模型 模型定义 在app目录下创建Model目录,即可创建模型文件 定义一个和数据库表相匹配的模型 use think\Model; class User extends Model ...
- HDU-5963 朋友 思维
题目链接http://acm.hdu.edu.cn/showproblem.php?pid=5963 吐槽 这道题我第一眼看,嗯??博弈论?还是树上的?我好像不会啊...但是一想某人的话,感觉这个应该 ...
- WinForm中DataGridView复制选中单元格内容解决方案
WinForm中DataGridView鼠标选中单元格内容复制方案 1.CTR+C快捷键复制 前提:该控件ClipboardCopyMode属性设置值非Disable: 2.鼠标框选,自定义代码实现复 ...