gin框架总结
一 gin框架初识
1.1 helloworld
gin框架中的路由是基于httprouter开发的。HelloWorld:
package main
import (
"github.com/gin-gonic/gin"
"fmt"
)
func main() {
r := gin.Default() //Default返回一个默认路由引擎
r.GET("/", func(c *gin.Context) {
username := c.Query("username")
fmt.Println(username)
c.JSON(200, gin.H{
"msg":"hello world",
})
})
r.Run() //默认位于0.0.0.0:8080,可传入参数":3030";也可以绑定服务器
}
二 参数获取
2.1 get请求参数
常见参数获取方式:
c.Query("username")
c.QueryDefault("username","lisi") //如果username为空,则赋值为lisi
路由地址为:/user/:name/:pass,获取参数:
name := c.Param("name")
2.2 post请求参数获取
name := c.PostForm("name")
2.3 参数绑定
参数绑定利用反射机制,自动提取querystring,form表单,json,xml等参数到结构体中,可以极大提升开发效率。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"fmt"
)
type User struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func login(c *gin.Context) {
var user User
fmt.Println(c.PostForm("username"))
fmt.Println(c.PostForm("password"))
err := c.ShouldBind(&user)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error()
})
}
c.JSON(http.StatusOK, gin.H{
"username": user.Username,
"password": user.Password,
})
}
func main() {
router := gin.Default()
router.POST("/login", login)
router.Run(":3000")
}
三 静态文件
静态化当前目录下static文件夹:
router := gin.Default()
router.Static("/static", "./static")
router.Run(":3000")
注意:同样推荐使用go build,不要使用开发工具的run功能。
四 结果返回
4.1 返回JSON
c.JSON(200,gin.H{"msg":"OK"})
c.JSON(200,结构体)
4.2 返回模板
router.LoadHTMLGlob("templates/**/*")
router.GET("/test/index", func(c *gin.Context){
c.HTML(http.StatusOK, "test/index.tmpl", gin.H{
"msg": "test",
})
})
模板文件:index.tmpl
{{define "test/index.tmpl"}}
<html>
<head>
</head>
<body>
test...
{{.}}
-----
{{.msg}}
</body>
</html>
{{end}}
注意事项:不要使用编辑器的run功能,会出现路径错误,推荐使用命令build,项目路径分配如下:
五 文件上传
5.1 单文件上传
router.POST("/upload", func (c *gin.Context) {
file, err := c.FormFile("file")
if (err != nil) {
c.JSON(http.StatusInternalServerError, gin.H{
"msg": err.Error(),
})
return
}
dst := fmt.Sprintf("/uploads/&s", file.Filename)
c.SavaeUpLoadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"msg":"ok",
})
})
5.2 多文件上传
router.POST("/upload", func(c *gin.Context) {
// 多文件
form, _ := c.MultipartForm()
files := form.File["upload[]"]
for _, file := range files {
log.Println(file.Filename)
// 上传文件到指定的路径
// c.SaveUploadedFile(file, dst)
}
c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
})
一 路由分组
访问路径是:/user/login
和 /user/signin
package main
import (
"github.com/gin-gonic/gin"
)
func login(c *gin.Context) {
c.JSON(300, gin.H{
"msg": "login",
})
}
func logout(c *gin.Context) {
c.JSON(300, gin.H{
"msg": "logout",
})
}
func main() {
router := gin.Default()
user := router.Group("/user")
{
user.GET("/login", login)
user.GET("/logout", logout)
}
router.Run(":3000")
}
二 路由设计
2.0 项目结构
笔者自己的路由设计,仅供参考:
项目结构如图:
2.1 main.go
main.go:
package main
import (
"Demo1/router"
)
func main() {
r := router.InitRouter()
_ = r.Run()
}
2.2 路由模块化核心 routes.go
routes.go:
package router
import (
"github.com/gin-gonic/gin"
)
func InitRouter() *gin.Engine {
r := gin.Default()
// 路由模块化
userRouter(r)
orderRouter(r)
return r
}
2.3 业务处理
userRouter.go示例:
package router
import (
"github.com/gin-gonic/gin"
"net/http"
)
func userRouter(r *gin.Engine) {
r.GET("/user/login", userLogin)
}
func userLogin(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 10001,
"msg": "登录成功",
"data": nil,
})
}
gin配合单元测试
https://github.com/stretchr/testify/assert 是个很好的单元测试框架。
在上一节中配置了笔者自己项目的路由模块化思路,下面是配套的单元测试demo:
userRouter_test.go
package test
import (
"Demo1/router"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestUserRouter_userLogin(t *testing.T) {
r := router.InitRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/user/login", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, `{"code":10001,"data":null,"msg":"登录成功"}`, w.Body.String())
}
一 Gin中间件
1.1 中间件的概念
gin框架允许在处理请求时,加入用户自己的钩子函数,该钩子函数即中间件。他的作用与Java中的拦截器,Node中的中间件相似。
中间件需要返回gin.HandlerFunc
函数,多个中间件通过Next函数来依次执行。
1.2 入门使用案例
现在设计一个中间件,在每次路由函数执行前打印一句话,在上一节的项目基础上新建middleware
文件夹,新建一个中间件文件MyFmt.go
:
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
)
// 定义一个中间件
func MyFMT() gin.HandlerFunc {
return func(c *gin.Context) {
host := c.Request.Host
fmt.Printf("Before: %s\n",host)
c.Next()
fmt.Println("Next: ...")
}
}
在路由函数中使用中间件:
r.GET("/user/login", middleware.MyFMT(), userLogin)
打印结果:
Before: localhost:8080
Next: ...
[GIN] 2019/07/28 - 16:28:16 | 200 | 266.33µs | ::1 | GET /user/login
1.2 中间件的详细使用方式
全局中间件:直接使用 gin.Engine
结构体的Use()
方法,中间件将会在项目的全局起作用。
func InitRouter() *gin.Engine {
r := gin.Default()
// 全局中间件
r.Use(middleware.MyFMT())
// 路由模块化
userRouter(r)
orderRouter(r)
return r
}
路由分钟中使用中间件:
router := gin.New()
user := router.Group("user", gin.Logger(),gin.Recovery())
{
user.GET("info", func(context *gin.Context) {
})
user.GET("article", func(context *gin.Context) {
})
}
单个路由使用中间件(支持多个中间件的使用):
router := gin.New()
router.GET("/test",gin.Recovery(),gin.Logger(),func(c *gin.Context){
c.JSON(200,"test")
})
1.3 内置中间件
Gin也内置了一些中间件,可以直接使用:
func BasicAuth(accounts Accounts) HandlerFunc
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc
func Bind(val interface{}) HandlerFunc //拦截请求参数并进行绑定
func ErrorLogger() HandlerFunc //错误日志处理
func ErrorLoggerT(typ ErrorType) HandlerFunc //自定义类型的错误日志处理
func Logger() HandlerFunc //日志记录
func LoggerWithConfig(conf LoggerConfig) HandlerFunc
func LoggerWithFormatter(f LogFormatter) HandlerFunc
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
func Recovery() HandlerFunc
func RecoveryWithWriter(out io.Writer) HandlerFunc
func WrapF(f http.HandlerFunc) HandlerFunc //将http.HandlerFunc包装成中间件
func WrapH(h http.Handler) HandlerFunc //将http.Handler包装成中间件
二 请求的拦截与后置
中间件的最大作用就是拦截过滤请求,比如我们有些请求需要用户登录或者需要特定权限才能访问,这时候便可以中间件中做过滤拦截。
下面三个方法中断请求后,直接返回200,但响应的body中不会有数据:
func (c *Context) Abort()
func (c *Context) AbortWithError(code int, err error) *Error
func (c *Context) AbortWithStatus(code int)
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) // 中断后可以返回json数据
如果在中间件中调用gin.Context的Next()方法,则可以请求到达并完成业务处理后,再经过中间件后置拦截处理:
func MyMiddleware(c *gin.Context){
//请求前
c.Next()
//请求后
}
一 gin.Engine
Engine是框架的入口,是gin框架的核心,通过Engine对象来定义服务路由信息、组装插件、运行服务。不过Engine的本质只是对内置HTTP服务的包装。
gin.Default()
函数会生成一个默认的 Engine 对象,包含2个默认常用插件
- Logger:用于输出请求日志
- Recovery:用于确保单个请求发生 panic 时记录异常堆栈日志,输出统一的错误响应。
func Default() *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
二 gin的路由
2.1 路由树
在 Gin 框架中,路由规则被分成了最多 9 棵前缀树,每一个 HTTP Method对应一棵 前缀树 ,树的节点按照 URL 中的 / 符号进行层级划分,URL 支持 :name
形式的名称匹配,还支持 *subpath
形式的路径通配符:
// 匹配单节点 named
pattern = /book/:id
match /book/123
nomatch /book/123/10
nomatch /book/
// 匹配子节点 catchAll mode
/book/*subpath
match /book/
match /book/123
match /book/123/10
如图所示:
每个节点都会挂接若干请求处理函数构成一个请求处理链 HandlersChain。当一个请求到来时,在这棵树上找到请求 URL 对应的节点,拿到对应的请求处理链来执行就完成了请求的处理。
type Engine struct {
...
trees methodTrees
...
}
type methodTrees []methodTree
type methodTree struct {
method string
root *node // 树根
}
type node struct {
path string // 当前节点的路径
...
handlers HandlersChain // 请求处理链
...
}
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc
Engine 对象包含一个 addRoute 方法用于添加 URL 请求处理器,它会将对应的路径和处理器挂接到相应的请求树中:
func (e *Engine) addRoute(method, path string, handlers HandlersChain)
2.2 路由组
RouterGroup 是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine 结构体继承了 RouterGroup ,所以 Engine 直接具备了 RouterGroup 所有的路由管理功能,同时 RouteGroup 对象里面还会包含一个 Engine 的指针,这样 Engine 和 RouteGroup 就成了「你中有我我中有你」的关系。
type Engine struct {
RouterGroup
...
}
type RouterGroup struct {
...
engine *Engine
...
}
RouterGroup 实现了 IRouter 接口,暴露了一系列路由方法,这些方法最终都是通过调用 Engine.addRoute 方法将请求处理器挂接到路由树中。
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
// 匹配所有 HTTP Method
Any(string, ...HandlerFunc) IRoutes
RouterGroup 内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现 URL 分组功能。
Engine 对象内嵌的 RouterGroup 对象的前缀路径是 /,它表示根路径。RouterGroup 支持分组嵌套,使用 Group 方法就可以让分组下面再挂分组,依次类推。
2.3 HTTP错误
当 URL 请求对应的路径不能在路由树里找到时,就需要处理 404 NotFound 错误。当 URL 的请求路径可以在路由树里找到,但是 Method 不匹配,就需要处理 405 MethodNotAllowed 错误。Engine 对象为这两个错误提供了处理器注册的入口。
func (engine *Engine) NoMethod(handlers ...HandlerFunc)
func (engine *Engine) NoRoute(handlers ...HandlerFunc)
异常处理器和普通处理器一样,也需要和插件函数组合在一起形成一个调用链。如果没有提供异常处理器,Gin 就会使用内置的简易错误处理器。
注意这两个错误处理器是定义在 Engine 全局对象上,而不是 RouterGroup。对于非 404 和 405 错误,需要用户自定义插件来处理。对于 panic 抛出来的异常需要也需要使用插件来处理。
2.4 HTTPS
Gin 不支持 HTTPS,官方建议是使用 Nginx 来转发 HTTPS 请求到 Gin。
三 gin.Context
gin.Context内保存了请求的上下文信息,是所有请求处理器的入口参数:
type HandlerFunc func(*Context)
type Context struct {
...
Request *http.Request // 请求对象
Writer ResponseWriter // 响应对象
Params Params // URL匹配参数
...
Keys map[string]interface{} // 自定义上下文信息
...
}
Context 对象提供了非常丰富的方法用于获取当前请求的上下文信息,如果你需要获取请求中的 URL 参数、Cookie、Header 都可以通过 Context 对象来获取。这一系列方法本质上是对 http.Request 对象的包装:
// 获取 URL 匹配参数 /book/:id
func (c *Context) Param(key string) string
// 获取 URL 查询参数 /book?id=123&page=10
func (c *Context) Query(key string) string
// 获取 POST 表单参数
func (c *Context) PostForm(key string) string
// 获取上传的文件对象
func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
// 获取请求Cookie
func (c *Context) Cookie(name string) (string, error)
...
Context 对象提供了很多内置的响应形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等。它会为每一种形式都单独定制一个渲染器。通常这些内置渲染器已经足够应付绝大多数场景,如果你觉得不够,还可以自定义渲染器。
func (c *Context) JSON(code int, obj interface{})
func (c *Context) Protobuf(code int, obj interface{})
func (c *Context) YAML(code int, obj interface{})
...
// 自定义渲染
func (c *Context) Render(code int, r render.Render)
// 渲染器通用接口
type Render interface {
Render(http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
}
所有的渲染器最终还是需要调用内置的 http.ResponseWriter(Context.Writer) 将响应对象转换成字节流写到套接字中。
type ResponseWriter interface {
// 容纳所有的响应头
Header() Header
// 写Body
Write([]byte) (int, error)
// 写Header
WriteHeader(statusCode int)
}
四 插件与请求链
gin的插件机制中,函数链的尾部是业务处理,前面部分是插件函数。在 Gin 中插件和业务处理函数形式是一样的,都是 func(*Context)。当我们定义路由时,Gin 会将插件函数和业务处理函数合并在一起形成一个链条结构。
type Context struct {
...
index uint8 // 当前的业务逻辑位于函数链的位置
handlers HandlersChain // 函数链
...
}
// 挨个调用链条中的处理函数
func (c *Context) Next() {
c.index++
for s := int8(len(c.handlers)); c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
所以在业务代码中,一般一个处理函数时,路由节点也需要挂载一个函数链条。
Gin 在接收到客户端请求时,找到相应的处理链,构造一个 Context 对象,再调用它的 Next() 方法就正式进入了请求处理的全流程。
一 请求流程梳理
首先从gin最开始的创建engine对象部分开始:
router := gin.Default()
该方法返回了Engine结构体,常见属性有:
type Engine struct {
//路由组
RouterGroup
RedirectTrailingSlash bool
RedirectFixedPath bool
HandleMethodNotAllowed bool
ForwardedByClientIP bool
AppEngine bool
UseRawPath bool
UnescapePathValues bool
MaxMultipartMemory int64
delims render.Delims
secureJsonPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
// 对象池 用来创建上下文context
pool sync.Pool
//记录路由方法的 比如GET POST 都会是数组中的一个 每个方法对应一个基数树的一个root的node
trees methodTrees
}
Default方法其实就是创建了该对象,并添加了一些默认中间件:
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
注意,这里Default方法内部调用了New方法,该方法默认添加了路由组"/"
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
context对象存储了上下文信息,包括:engine指针、request对象,responsewriter对象等,context在请求一开始就被创建,且贯穿整个执行过程,包括中间件、路由等等:
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain
index int8
engine *Engine
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
}
接下来是Use方法:
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
//调用routegroup的use方法
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
//为group的handlers添加中间件
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
最后到达最终路由,有GET,POST等多种方法,但是每个方法的处理方式都是相同的,即把group和传入的handler合并,计算出路径存入tree中等待客户端调用:
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
//调用get方法
return group.handle("GET", relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
//计算路径地址,比如group地址是 router.Group("/api")
//结果为/api/test/ 就是最终计算出来的结果 使用path.join 方法拼接 其中加了一些判断
absolutePath := group.calculateAbsolutePath(relativePath)
//把group中的handler和传入的handler合并
handlers = group.combineHandlers(handlers)
//把方法 路径 和处理方法作为node 加入到基数树种,基数树在下次单独学习分析
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
run方法则是启动服务,在http包中会有一个for逻辑不停的监听端口:
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
二 书写类似源码
一 Gin对象的构建
Gin框架是基于golang官方的http包搭建起来的,http包最重要的实现是:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
利用该方法,以及参数中的Handler接口,实现Gin的Engine,Context:
package engine
import "net/http"
type Engine struct {
}
func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
context对象其实就是对ServeHTTP方法参数的封装,因为这2个参数在web开发中一个完整请求链中都会用到:
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
}
type responseWriter struct {
http.ResponseWriter
size int
status int
}
这里多了一个属性 writermem
,如果仅仅只从功能上考虑,这里有了Request、Writer已经足够使用了,但是框架需要应对多变的返回数据情况,必须对其进行封装,比如:
type ResponseWriter interface {
http.ResponseWriter
Pusher() http.Pusher
Status() int
Size() int
WriteString(string) (int, error)
Written() bool
WriteHeaderNow()
}
这里对外暴露的是接口RespnserWriter,内部的http.ResponseWriter
实现原生的ResponseWriter接口,在reset()的时候进行拷贝即可:
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
}
这样做能够更好的符合面向接口编程的概念。
Context也可以通过对象池复用提升性能:
type Engine struct {
pool sync.Pool
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
紧接着就可以在Context的基础上实现其大量的常用方法了:
func (c *Context) Param(key string) string{
return ""
}
func (c *Context) Query(key string) string {
return ""
}
func (c *Context) DefaultQuery(key, defaultValue string) string {
return ""
}
func (c *Context) PostFormArray(key string) []string{
return nil
}
有疑问加站长微信联系(非本文作者)