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": "[email protected]", "phone": "18821212121"},
"lili": gin.H{"email": "[email protected]", "phone": "18821212122"},
"sam": gin.H{"email": "[email protected]", "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">Primarybutton>
<button type="button" class="btn btn-secondary">Secondarybutton>
<button type="button" class="btn btn-success">Successbutton>
<button type="button" class="btn btn-danger">Dangerbutton>
<button type="button" class="btn btn-warning">Warningbutton>
<button type="button" class="btn btn-info">Infobutton>
<button type="button" class="btn btn-light">Lightbutton>
<button type="button" class="btn btn-dark">Darkbutton>
<button type="button" class="btn btn-link">Linkbutton>
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