@
Gin 官网地址 https://gin-gonic.com/ 源码release最新版本v1.9.1
Gin 官网文档地址 https://gin-gonic.com/docs/
Gin 源码地址 https://github.com/gin-gonic/gin
Gin是目前使用最广泛、最快的全功能web框架之一,采用Go语言(Golang)编写HTTP 服务,与它类似如martini-like API ,但Gin性能更好,基于httprouter其速度快了40倍。
Gin是一种用于构建Web应用程序的Go语言框架,具有高性能、易于使用、轻量级和灵活的特点。 Gin提供了许多功能,例如路由、中间件、错误处理等。同时,Gin还可以与许多其他Go语言库和框架无缝集成。 Gin的设计目的是提供一种快速、可靠和高效的方式来构建Web应用程序,以满足现代Web应用程序的需求;它的文档和社区支持也非常好,因此它成为了Go语言中最受欢迎的Web框架之一。
前置环境:需要go 1.13及以上版本
# 下载并安装 go get -u github.com/gin-gonic/gin
创建quick_start.go文件,导入gin库
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { // 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine r := gin.Default() // 配置路由映射 r.GET("/hello", func(c *gin.Context) { c.String(http.StatusOK, "hello,welcome to go world!") }) // 监听端口,默认为0.0.0.0:8080 r.Run() }
启动运行,访问http://localhost:8080/hello可以看到成功返回信息
go run quick_start.go
package main import ( "github.com/gin-gonic/gin" "net/http" ) func demoGet(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo get", }) } func demoPost(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo post", }) } func demoPut(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo put", }) } func demoDelete(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo delete", }) } func demoPatch(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo patch", }) } func demoHead(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo head", }) } func demoOptions(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo options", }) } func main() { // 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine r := gin.Default() // 配置路由映射 r.GET("/DemoGet", demoGet) r.POST("/DemoPost", demoPost) r.PUT("/DemoPut", demoPut) r.DELETE("/DemoDelete", demoDelete) r.PATCH("/DemoPatch", demoPatch) r.HEAD("/DemoHead", demoHead) r.OPTIONS("/DemoOptions", demoOptions) r.Run() // 监听端口,默认为0.0.0.0:8080 }
运行程序,通过HTTP请求工具如PostMan输入请求地址http://localhost:8080/DemoPost,返回预期结果
package main import ( "github.com/gin-gonic/gin" "net/http" ) func getVal(c *gin.Context) { name := c.Query("name") addr := c.DefaultQuery("addr", "home") c.JSON(http.StatusOK, gin.H{ "name": name, "addr": addr, }) } func postVal(c *gin.Context) { name := c.PostForm("name") addr := c.DefaultPostForm("addr", "home") c.JSON(http.StatusOK, gin.H{ "name": name, "addr": addr, }) } func restVal(c *gin.Context) { name := c.Param("name") addr := c.Param("addr") c.JSON(http.StatusOK, gin.H{ "name": name, "addr": addr, }) } func postMapVal(c *gin.Context) { ids := c.QueryMap("ids") names := c.PostFormMap("names") c.JSON(http.StatusOK, gin.H{ "ids": ids, "names": names, }) } func main() { // 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine r := gin.Default() // 配置路由映射 r.GET("/GetVal", getVal) r.POST("/PostVal", postVal) r.POST("/PostMapVal", postMapVal) r.POST("/RestVal/:name/:addr", restVal) r.Run() // 监听端口,默认为0.0.0.0:8080 }
运行程序,通过HTTP请求工具如PostMan输入请求地址http://localhost:8080/GetVal?name=itxiaoshen,返回预期结果
用Post提交方式,输入请求地址http://localhost:8080/PostVal并选择Body的中form-data或者x-www-form-urlencoded填写参数,返回预期结果
输入请求地址http://localhost:8080/PostMapVal?ids[a]=hello&ids[b]=world,在Body体填写相应的数组参数值,通过数组返回并输出预期结果
输入请求地址http://localhost:8080/RestVal/lixuefeng/qinghua,在url路径参数中输入相应的参数值,返回预期结果
package main import ( "github.com/gin-gonic/gin" "net/http" ) type Person struct { Name string `form:"name"` Address string `form:"addr"` Age int } type PersonUri struct { Name string `uri:"name" binding:"required"` Address string `uri:"addr" binding:"required"` Age int `uri:"age" binding:"required"` } func bindVal(c *gin.Context) { var person Person if c.ShouldBind(&person) == nil { c.JSON(http.StatusOK, gin.H{ "name": person.Name, "addr": person.Address, "age": person.Age, }) } } func personMethod(c *gin.Context) { var person PersonUri if err := c.ShouldBindUri(&person); err != nil { c.JSON(http.StatusBadRequest, gin.H{"msg": err}) return } c.JSON(http.StatusOK, gin.H{ "name": person.Name, "addr": person.Address, "age": person.Age, }) } func main() { // 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine r := gin.Default() // 配置路由映射 r.GET("/BindVal", bindVal) r.GET("/person/:name/:addr/:age", personMethod) r.Run() // 监听端口,默认为0.0.0.0:8080 }
运行程序,输入请求地址http://localhost:8080/BindVal?name=wangchuanjun&addr=zhengjiang&age=20,由于传入age字段与Person的Age字段名没匹配上,因此Age字段获取不到值采用的默认值0输出
通过uri路径参数输入请求地址http://localhost:8080/person/yangju/chengsan/26,返回预期结果
package main import ( "fmt" "github.com/gin-gonic/gin" "net/http" "time" ) func main() { r := gin.New() // LoggerWithFormatter中间件将把日志写入gin.DefaultWriter,默认为gin.DefaultWriter = os.Stdout r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { // 自定义输出日志 return fmt.Sprintf("custom log format:%s -------- [%s] \"%s %s %d %s \"%s\" %s\"%s\n", param.ClientIP, param.Method, param.Path, param.Request.Proto, param.StatusCode, param.Latency, param.Request.UserAgent(), param.ErrorMessage, param.TimeStamp.Format(time.RFC1123), ) })) r.Use(gin.Recovery()) r.GET("/hello", func(c *gin.Context) { c.String(http.StatusOK, "hello,welcome to go world!") }) r.Run(":8080") }
启动运行,访问http://localhost:8080/hello,查看运行的控制台日志可以看到自定义日志输出
package main import ( "github.com/gin-gonic/gin" "log" "net/http" "time" ) // 返回gin.HandlerFunc函数 func Logger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // 设置 userId 变量 c.Set("userId", "10001") // 请求前 c.Next() // 请求后 latency := time.Since(t) log.Print(latency) // access the status we are sending status := c.Writer.Status() log.Println(status) } } func main() { r := gin.New() // 使用自定义中间件 r.Use(Logger()) r.GET("/hello", func(c *gin.Context) { userId := c.MustGet("userId").(string) c.String(http.StatusOK, "userId="+userId) }) // 监听 0.0.0.0:8080 r.Run(":8080") }
启动运行,访问http://localhost:8080/hello,查看运行的控制台日志可以看到输出响应的日志,也响应userId结果
路由组可以多级嵌套,实现细粒度控制
package main import ( "github.com/gin-gonic/gin" "net/http" ) func demoGet(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo get", }) } func demoPost(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "demo post", }) } func main() { // 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine r := gin.Default() v1 := r.Group("/v1") { v1.GET("/DemoGet", demoGet) v1.POST("/DemoPost", demoPost) } v2 := r.Group("/v2") { v2.GET("/DemoGet", demoGet) v2.POST("/DemoPost", demoPost) } r.Run() // 监听端口,默认为0.0.0.0:8080 }
运行程序,访问http://localhost:8080/v2/DemoGet,返回预期结果
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { router := gin.Default() router.LoadHTMLGlob("templates/*") // 可以通过下面LoadHTMLFiles加载指定的文件 //router.LoadHTMLFiles("templates/index.html", "templates/index1.html") router.GET("/index", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{ "content": "my website begin", }) }) router.Run(":8080") }
运行程序,访问http://localhost:8080/index,返回预期结果
package main import ( "fmt" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/cookie", func(c *gin.Context) { cookie, err := c.Cookie("gin_cookie") if err != nil { cookie = "NotSet" c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true) } fmt.Printf("Cookie value: %s \n", cookie) }) router.Run() }
运行程序,访问http://localhost:8080/index,可以查看cookie信息
package main import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/testdata/protoexample" "net/http" ) func main() { r := gin.Default() r.GET("/someXML", func(c *gin.Context) { c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) }) r.GET("/someYAML", func(c *gin.Context) { c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) }) r.GET("/someProtoBuf", func(c *gin.Context) { reps := []int64{int64(1), int64(2)} label := "test" data := &protoexample.Test{ Label: &label, Reps: reps, } c.ProtoBuf(http.StatusOK, data) }) r.Run(":8080") }
运行程序,访问http://localhost:8080/someYAML,可以查看渲染内容
package main import ( "github.com/gin-gonic/gin" "net/http" ) // 模拟一些私有数据 var secrets = gin.H{ "jasper": gin.H{"email": "jasper@163.com", "phone": "18821212121"}, "lili": gin.H{"email": "lili@163.com", "phone": "18821212122"}, "sam": gin.H{"email": "sam@163.com", "phone": "18821212123"}, } func main() { r := gin.Default() // 使用gin.BasicAuth()中间件进行分组,实际中这里应该是从数据库中获取,gin.Accounts是map[string]string authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ "jasper": "123456", "austin": "123456", "lili": "123456", "sam": "123456", })) authorized.GET("/secrets", func(c *gin.Context) { // 获取user,它是由BasicAuth中间件设置的 user := c.MustGet(gin.AuthUserKey).(string) if secret, ok := secrets[user]; ok { c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret}) } else { c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("}) } }) // 监听 0.0.0.0:8080 r.Run(":8080") }
运行程序,浏览器上访问http://localhost:8080/admin/secrets,在弹出认证窗口上输入用户名密码如jasper/123456
点击登录按钮后则返回预期响应信息。
# getbootstrap官网地址:https://getbootstrap.com # 下载最新版本5.3.0bootstrap wget https://github.com/twbs/bootstrap/releases/download/v5.3.0/bootstrap-5.3.0-dist.zip
解压目录下css和js目录拷贝到工程目录下,这里将css和js目录放在根目录下的assets目录下,然后创建main.go
package main import ( "github.com/gin-gonic/gin" "net/http" ) func index(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) } func main() { // 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine r := gin.Default() // 配置静态资源 r.Static("/assets", "assets") r.Static("/favicon.ico", "assets/favicon.ico") // 配置路由映射 r.GET("/index", index) //加载html模版文件 r.LoadHTMLGlob("templates/*") r.Run() // 监听端口,默认为0.0.0.0:8080 }
这里从getbootstrap官网上找一个组件放到html文件中
在templates目录下创建index.html模版文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="../assets/css/bootstrap.min.css"> <script src="../assets/js/bootstrap.min.js"></script> <title>静态资源及BootStrap示例</title> </head> <body> <h1> <button type="button" class="btn btn-primary">Primary</button> <button type="button" class="btn btn-secondary">Secondary</button> <button type="button" class="btn btn-success">Success</button> <button type="button" class="btn btn-danger">Danger</button> <button type="button" class="btn btn-warning">Warning</button> <button type="button" class="btn btn-info">Info</button> <button type="button" class="btn btn-light">Light</button> <button type="button" class="btn btn-dark">Dark</button> <button type="button" class="btn btn-link">Link</button> </h1> </body> </html>
上面在html配置link和script时前面加了../,这样才可以在IDE环境中定位到,总体目录结构如下
运行程序,访问http://localhost:8080/index ,已经成功加载到静态资源
关于Session的可以使用第三方库的方式,在https://pkg.go.dev/上搜索go session,选择第一个github.com/gin-contrib/sessions,其支持多后端会话管理的Gin中间件包括cookie-based、Redis、memcached、MongoDB、GORM、memstore、PostgreSQL。
# 下载并安装 go get github.com/gin-contrib/sessions # 在代码中导入 import "github.com/gin-contrib/sessions"
package main import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() store := cookie.NewStore([]byte("secret")) r.Use(sessions.Sessions("mysession", store)) r.GET("/session", func(c *gin.Context) { session := sessions.Default(c) if session.Get("mykey") != "myvalue" { session.Set("mykey", "myvalue") session.Save() } c.JSON(200, gin.H{"mykey": session.Get("mykey")}) }) r.Run(":8080") }
运行程序,访问http://localhost:8080/session ,成功返回预期结果
上面示例使用的是cookie-based后端存储方式,下面则演示redis作为后端存储示例
package main import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() store, _ := redis.NewStore(10, "tcp", "localhost:6379", "123456", []byte("secret")) r.Use(sessions.Sessions("myredissession", store)) r.GET("/incr", func(c *gin.Context) { session := sessions.Default(c) var count int v := session.Get("mycount") if v == nil { count = 0 } else { count = v.(int) count++ } session.Set("mycount", count) session.Save() c.JSON(200, gin.H{"mycount": count}) }) r.Run(":8080") }
项目目录下命令行执行go mod tidy,然后多次访问http://localhost:8080/incr,可以看到mycount一直在自增
package main import ( "github.com/gin-gonic/gin" "io" "os" ) func main() { // 禁用控制台颜色,在将日志写入文件时不需要控制台颜色。 gin.DisableConsoleColor() // 记录到文件 f, _ := os.Create("my_app.log") gin.DefaultWriter = io.MultiWriter(f) // 如果需要同时将日志写入文件和控制台,请使用以下代码 // gin.DefaultWriter = io.MultiWriter(f, os.Stdout) router := gin.Default() router.GET("/hello", func(c *gin.Context) { c.String(200, "welcome to go world!") }) router.Run(":8080") }
启动程序,多次访问http://localhost:8080/hello,可以看到在当前目录下生成了my_app.log文件,用户可以结合日志需要比如使用logrus和file-rotatelogs库实现。
Gin整体流程还是比较简单的,从上面使用代码示例也可以看到基础执行流程大致如下:
主要设计核心方法流程
使用gin框架开发时一般情况下使用默认的engine即可,因为相对于直接使用gin.New()创建Engine对象,它只是多注册了两个中间件。
func Default() *Engine { debugPrintWARNINGDefault() // 创建引擎 engine := New() // 默认使用Logger和Recovery engine.Use(Logger(), Recovery()) return engine }
// Use将全局中间件附加到路由器上。也就是说,通过Use()附加的中间件将是包含在每个请求(甚至404、405、静态文件)的处理程序链中,例如记录器或错误管理中间件的正确位置。 func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { engine.RouterGroup.Use(middleware...) engine.rebuild404Handlers() engine.rebuild405Handlers() return engine } // 将中间件添加到路由组中 func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() }
// 链接group.handle快捷方式 func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodGet, relativePath, handlers) } func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() } func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { assert1(path[0] == '/', "path must begin with '/'") assert1(method != "", "HTTP method can not be empty") assert1(len(handlers) > 0, "there must be at least one handler") debugPrintRoute(method, path, handlers) root := engine.trees.get(method) if root == nil { root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) // Update maxParams if paramsCount := countParams(path); paramsCount > engine.maxParams { engine.maxParams = paramsCount } if sectionsCount := countSections(path); sectionsCount > engine.maxSections { engine.maxSections = sectionsCount } }
// Run将路由器附加到http上,服务器并开始监听和服务HTTP请求;也即是http.ListenAndServe快捷方式,对于除非发生错误,否则此方法将无限期地阻塞调用例程 func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }() if engine.isUnsafeTrustedProxies() { debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.") } address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine.Handler()) return }
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) }
在New()创建*Engine的方法中可以看到初始化重要信息,
其核心组成的重要结构如下
Engine是一个总的引擎,保存了各个组件的信息 ,其他组件信息如下:
type RouterGroup struct { // 路由组处理函数链,其下路由的函数链将结合路由组和自身的函数组成最终的函数链 Handlers HandlersChain // 路由组的基地址,一般是其下路由的公共地址 basePath string // 路由组所属的Engine,这里构成了双向引用 engine *Engine // 该路由组是否位于根节点,基于RouterGroup.Group创建路由组时此属性为false root bool }
在RouterGroup数据结构有一个非常重要的成员字段HandlersChain(处理器链 ),用于收集该路由组下注册的middleware函数。在运行时,会按顺序执行HandlersChain中的注册的函数。
// HandlersChain 定义为一个HandlerFunc切片. type HandlersChain []HandlerFunc // HandlerFunc 定义gin中间件使用的处理程序作为返回值 type HandlerFunc func(*Context)
路由树数组trees:trees是一棵树,保存了url与handle的映射关系 ,粗暴一点可以简单理解为key就是url字符串,value对应的[]HandleFunc;标准库本身的路由是不区分请求方法的,也就是说注册一个路由后,GET、POST都能匹配到该路由,而还需要在同一个路由在不同的请求方法下,由不同的逻辑进行处理。其实就是通过路由树实现的,gin的针对每个请求方法都有一棵路由树。Gin利用基于Radix Tree基数树思想通过优秀的数据结构和算法设计达到高性能目标。其核心实现是在gin的tree.go源码文件中。
context对象池:engine中的pool用于复用Context,gin.Context是gin框架暴露给开发的另一个核心对象,可以通过该对象获取请求信息,业务处理的结果也是通过该对象写回客户端的。为了实现context对象的复用,gin基于sync.Pool实现了对象池。由于请求多会产生很多数量的context,利用pool来重复利用对象,从而减少内存的分配也提高了效率。
Context:包含了Request,Writer等信息,用于request中传递值。
本人博客网站IT小神 www.itxiaoshen.com