gin介绍
gin是一个 Web应用框架,拥有良好的性能和简单明了的接口。同时支持中间件,类型绑定等实用功能。
为什么要用gin
在实际开发中,很少会直接实用http.Server。而自己搭建框架有一定成本,同时没有经过系统的校验,容易出现问题。而现有的框架中,gin拥有良好的性能,更重要的是接口清晰明了,接入成本极低。同时,其支持的功能也是多种多样,如中间件,类型绑定,日志规范。
gin 性能
以下是从官网拿到的性能对比指标表
- (1): Total Repetitions achieved in constant time, higher means more confident result
- (2): Single Repetition Duration (ns/op), lower is better
- (3): Heap Memory (B/op), lower is better
- (4): Average Allocations per Repetition (allocs/op), lower is better
Benchmark name | (1) | (2) | (3) | (4) |
---|---|---|---|---|
BenchmarkGin_GithubAll | 30000 | 48375 | 0 | 0 |
BenchmarkAce_GithubAll | 10000 | 134059 | 13792 | 167 |
BenchmarkBear_GithubAll | 5000 | 534445 | 86448 | 943 |
BenchmarkBeego_GithubAll | 3000 | 592444 | 74705 | 812 |
BenchmarkBone_GithubAll | 200 | 6957308 | 698784 | 8453 |
接入栗子
简单示例
接受/ping路径的Get请求,并返回message:"pong"
package main import "github.com/gin-gonic/gin" func main() { router := gin.Default() router.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) router.Run() // listen and serve on 0.0.0.0:8080 } 复制代码
支持所有的http协议
router := gin.Default() router.GET("/someGet", getting) router.POST("/somePost", posting) router.PUT("/somePut", putting) router.DELETE("/someDelete", deleting) router.PATCH("/somePatch", patching) router.HEAD("/someHead", head) router.OPTIONS("/someOptions", options) 复制代码
参数解析
router := gin.Default() // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe /**** example1: 解析Querystring ****/ router.GET("/welcome", func(c *gin.Context) { firstname := c.DefaultQuery("firstname", "Guest") // shortcut for c.Request.URL.Query().Get("lastname") lastname := c.Query("lastname") c.String(http.StatusOK, "Hello %s %s", firstname, lastname) }) /**** example2: 解析表单 ****/ router.POST("/form_post", func(c *gin.Context) { message := c.PostForm("message") nick := c.DefaultPostForm("nick", "anonymous") c.JSON(200, gin.H{ "status": "posted", "message": message, "nick": nick, }) }) 复制代码
gin特性
路由组
支持以组为单位的路由,下面栗子就是以/v1开头,以/v2开头的两组配置。路由组可以共享同样的配置,比如路由组v1可以使用中间件a。而v2可以使用另一个中间件,互不影响。
router := gin.Default() // Simple group: v1 v1 := router.Group("/v1") { v1.POST("/login", loginEndpoint) v1.POST("/submit", submitEndpoint) v1.POST("/read", readEndpoint) } // Simple group: v2 v2 := router.Group("/v2") { v2.POST("/login", loginEndpoint) v2.POST("/submit", submitEndpoint) v2.POST("/read", readEndpoint) } router.Run(":8080") 复制代码
中间件
中间件是对框架能力一个非常好的抽象。以组件的形式,为路由或路由组提供插件式功能。也可以自己实现中间件,加入到Use中来。
// Creates a router without any middleware by default r := gin.New() // Global middleware r.Use(gin.Logger()) r.Use(gin.Recovery()) // Listen and serve on 0.0.0.0:8080 r.Run(":8080") 复制代码
中间件可以非常方便的定义日志格式
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { // your custom format return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", param.ClientIP, param.TimeStamp.Format(time.RFC1123), param.Method, param.Path, param.Request.Proto, param.StatusCode, param.Latency, param.Request.UserAgent(), param.ErrorMessage, ) })) 复制代码
数据绑定
使用 c.ShouldBind方法,可以将参数自动绑定到 struct.该方法是会检查 Url 查询字符串和 POST 的数据,而且会根据 content-type类型,优先匹配JSON或者 XML,之后才是 Form.
// 定义一个 Person 结构体,用来绑定数据 type Person struct { Name string `form:"name"` Address string `form:"address"` Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` } func main() { route := gin.Default() route.GET("/testing", startPage) route.Run(":8085") } func startPage(c *gin.Context) { var person Person // 绑定到 person if c.ShouldBind(&person) == nil { log.Println(person.Name) log.Println(person.Address) log.Println(person.Birthday) } c.String(200, "Success") } 复制代码
gin原理分析
gin 可以说全是在handler上做文章。下面我们就以这三句话,一探gin。
func main() { r := gin.Default() r.GET("/getb", GetDataB) r.Run() } 复制代码
生成默认引擎
r := gin.Default() 的定义如下
func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine } 复制代码
Engine是gin中的一个很重要的概念。等下面对.r.Run分析时候,我们会发现他的本质就是http.Server里面的handler实例!
这里看到engine.Use(Logger(), Recovery()) 直观上就很像之前提前的http中间件的某种实现 Logger()是日志中间件, Recovery()是针对panic的中间件(不然每个handler都得写个panic处理逻辑)
来分析一下Recover中间件
func Recovery() HandlerFunc { return RecoveryWithWriter(DefaultErrorWriter) } func RecoveryWithWriter(out io.Writer) HandlerFunc { var logger *log.Logger if out != nil { logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags) } return func(c *Context) { defer func() { if err := recover(); err != nil { // 省略非关键代码 } }() c.Next() } } 复制代码
c.Next()能将多个中间件串联起来调用
// Next should be used only inside middleware. // It executes the pending handlers in the chain inside the calling handler. func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } } 复制代码
c.handlersc.index即当前索引位置对应的handler的调用
type HandlersChain []HandlerFunc type HandlerFunc func(*Context) 复制代码
由于处理逻辑是放在了c.Next前面,所以中间件的处理顺序是先入后出。中间件本身应该互相独立。但如果因为特殊原因,有前后依赖,就要注意这点。
注册路由规则
r.GET("/getb", GetDataB) 实现如下
// GET is a shortcut for router.Handle("GET", path, handle). func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("GET", 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) } 复制代码
可以看到,路由映射是加入了一颗树中。这里使用的是radix树,Radix树,即基数树,也称压缩前缀树,是一种提供key-value存储查找的数据结构。与Trie不同的是,它对Trie树进行了空间优化,只有一个子节点的中间节点将被压缩。同样的,Radix树的插入、查询、删除操作的时间复杂度都为O(k)。存储原理示意图如下:
启动服务
看下r.Run的实现
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 } 复制代码
可以看到,实际调用的,还是http.ListenAndServe这个方法,engine作为handler参数传入。http.ListenAndServe的原理,在前文httpServer有过阐述,这里不做过多分析。
总结
gin有如下特点:
- 接入成本非常低,作为一个组件,这是最重要的一点
- 拥有强大的中间件功能,用户可以自主定制需要的功能
- 由于使用了radix树,路由的性能很高。
- 数据绑定,让用户可以非常方便地从请求中获取想要的结构体。
gin对外接口和代码实现都非常优秀,无论是项目使用,还是源码学习,都值得推荐。