Gin框架源碼解析


Gin框架源碼解析

Gin框架是golang的一個常用的web框架,最近一個項目中需要使用到它,所以對這個框架進行了學習。gin包非常短小精悍,不過主要包含的路由,中間件,日志都有了。我們可以追着代碼思考下,這個框架是如何一步一步過來的。

從http包說起

基本上現在的golang的web庫都是從http上搭建起來,golang的http包的核心如下:

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

這里的Handler是一個接口

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

所以,這里就是我們的入口,這里我們需要有一個類來實現這個接口:Engine。

type Engine struct {

}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	...
}

這里ServeHTTP的方法傳遞的兩個參數,一個是Request,一個是ResponseWriter,Engine中的ServeHTTP的方法就是要對這兩個對象進行讀取或者寫入操作。而且這兩個對象往往是需要同時存在的,為了避免很多函數都需要寫這兩個參數,我們不如封裝一個結構來把這兩個對象放在里面:Context

type Context struct {
  writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter
  ...
}


type responseWriter struct {
	http.ResponseWriter
	size   int
	status int
}

這里有幾個需要討論的點:

Writer是否可以直接使用http包的ResponseWriter接口

type ResponseWriter interface {

	Header() Header

	Write([]byte) (int, error)

	WriteHeader(statusCode int)
}

但是考慮到我們web框架的最重要的就是輸出數據給客戶端,這里的輸出邏輯我們極有可能需要自己封裝一些框架自帶的方法。所以我們不妨自定義一個結構responseWriter,來實現基本的http.ResponseWriter。並且實現一些具體的其他方法。這些具體的其他方法都有哪些呢?我們使用gin包自帶的ResponseWriter接口來說明。

type ResponseWriter interface {
	responseWriterBase

	Pusher() http.Pusher
}

type responseWriterBase interface {
	http.ResponseWriter
	http.Hijacker
	http.Flusher
	http.CloseNotifier

	Status() int

	Size() int

	WriteString(string) (int, error)

	Written() bool

	WriteHeaderNow()
}

為什么Context有writermem和Writer兩個實現了http.Response對象的結構?

首先我們自帶的ResponseWriter必須實現比http.ResponseWriter更強大的接口功能,這個是毋庸置疑的。所以,我們不妨考慮下這里如果不是兩個writermem和Writer兩個的話,只有一個存在是否可能?

如果只有Writer接口存在,這個一定不可能,這個Writer實現的是我們gin自定義的接口,外部serveHTTP傳遞的是實現了http.ResponseWriter的類,並不能保證實現了gin自帶的ResponseWriter。

如果只有writermen結構存在,這個是可能的。外部傳遞的http.ResponseWriter就被藏在了這個對象里面。但是這樣就丟失了接口的靈活性。本質還是對外暴露的是接口還是結構的邏輯,設想一下如果使用這個框架的用戶要自己實現一個ResponseWriter,就需要繼承這個結構,而不是繼承接口。而具體的調用的方法就變成了被繼承結構的方法了。例子如下:

package main

func main() {
	customResp := new(customResponseWriter)

	c := new(context)
	c.Writermem = customResp.responseWriter
	c.Writermem.call()
}

type context struct {
	Writermem responseWriter
}

type customResponseWriter struct {
	responseWriter
}

func (r *customResponseWriter)call() {

}

type responseWriter struct{}

func (r *responseWriter)call() {

}

所以這里的Context結構,對外暴露的是接口ResponseWriter,內部的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就是某個請求的上下文結構,這個結構當然是可以不斷new的,但是new這個對象的代價可以使用一個對象池進行服用,節省對象頻繁創建和銷毀的開銷。golang中的sync.Pool就是用於這個用途的。需要注意的是,這里的對象池並不是所謂的固定對象池,而是臨時對象池,里面的對象個數不能指定,對象存儲時間也不能指定,只是增加了對象復用的概率而已。

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是gin中最重要的數據結構之一了,它既然已經包了request了,那么從請求中獲取參數的各個接口它必然也需要包了。


func (c *Context) Param(key string) string
...

func (c *Context) Query(key string) string

func (c *Context) DefaultQuery(key, defaultValue string) string

...

func (c *Context) PostFormArray(key string) []string

路由

從http請求進來的邏輯理清楚了,下面就進入到了路由部分,路由其實還是分為兩個部分,一個是路由設置部分,一個是路由匹配部分。

路由其實並不僅僅是url,還包括HTTP的請求方法,而實現一個REST風格的http請求,需要支持REST支持的方法,比如GET,PUT,POST,DELETE,OPTION等。

路由一定是有很多個路由路徑,可以使用數組存儲,但更巧妙的是,使用Redix樹結構進行存儲。這樣尋找的方法更為高效。

首先我們會在Engine這個結構中增加樹結構,並且提供增加路由的功能

type Engine struct {
  ...
	pool             sync.Pool
	trees            methodTrees
}

type methodTrees []methodTree

type methodTree struct {
	method string
	root   *node
}

type node struct {
	path      string
	indices   string
	children  []*node
	handlers  HandlersChain
	priority  uint32
	nType     nodeType
	maxParams uint8
	wildChild bool
}

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)
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
	root.addRoute(path, handlers)
}

其中我們可以看到engine.trees實際上是有多個樹組成,這里的每個樹都是根據HTTP method進行區分的。每增加一個路由,就往engine中對應的method的樹中增加一個path和handler的關系。

這個樹是一個Redix樹,父節點存儲子節點的公共部分,子節點存在各自的特有路徑。
如圖:

那么具體往這個trees中增加路由怎么增加呢?

這里選擇使用一個結構RouterGroup

type RouterGroup struct {
	Handlers HandlersChain
	basePath string
	engine   *Engine
	root     bool
}

type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc

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 (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("POST", relativePath, handlers)
}

// 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)
}

// DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("DELETE", relativePath, handlers)
}

// PATCH is a shortcut for router.Handle("PATCH", path, handle).
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("PATCH", relativePath, handlers)
}

// PUT is a shortcut for router.Handle("PUT", path, handle).
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("PUT", relativePath, handlers)
}

// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("OPTIONS", relativePath, handlers)
}

// HEAD is a shortcut for router.Handle("HEAD", path, handle).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("HEAD", relativePath, handlers)
}

// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
	group.handle("GET", relativePath, handlers)
	group.handle("POST", relativePath, handlers)
	group.handle("PUT", relativePath, handlers)
	group.handle("PATCH", relativePath, handlers)
	group.handle("HEAD", relativePath, handlers)
	group.handle("OPTIONS", relativePath, handlers)
	group.handle("DELETE", relativePath, handlers)
	group.handle("CONNECT", relativePath, handlers)
	group.handle("TRACE", relativePath, handlers)
	return group.returnObj()
}

那么Engine就繼承RouterGroup:

type Engine struct {
  RouterGroup
  ...
	pool             sync.Pool
	trees            methodTrees
}

看到這里就有一點REST的味道了吧。

有人會問,為什么不把這些方法的具體實現放在Engine中呢?這里我考慮到是由於“路由”和“引擎”畢竟是兩個邏輯,使用繼承的方式有利於代碼邏輯分離。並且gin還定義了接口IRoutes來表示RouterGroup實現的方法。

type IRoutes interface {
	Use(...HandlerFunc) IRoutes

	Handle(string, string, ...HandlerFunc) IRoutes
	Any(string, ...HandlerFunc) IRoutes
	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

	StaticFile(string, string) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}

將RouterGroup和Engine區分開,還有一個好處。我們有時候需要將一批路由加個統一前綴,這里需要用到方法:

使用例子如下:

v1 := router.Group("/v1")

v1.GET("/login", func(c *gin.Context) {
  c.String(http.StatusOK, "v1 login")
})

這里再看一下RouterGroup的Group函數。

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}

它把RouterGroup暴露出來,而不是把Engine暴露出來,這樣整個邏輯就很清晰,我可以對這個RouterGroup進行各種自定義方法。在最后調用v1.GET的時候再將帶有絕對路徑的handler掛在engine上的tree上。

在請求進來的時候,路由匹配,在engine的handleHTTPRequest

func (engine *Engine) handleHTTPRequest(c *Context) {
	httpMethod := c.Request.Method
	path := c.Request.URL.Path
	unescape := false
	if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
		path = c.Request.URL.RawPath
		unescape = engine.UnescapePathValues
	}

	// Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		handlers, params, tsr := root.getValue(path, c.Params, unescape)
		if handlers != nil {
			c.handlers = handlers
			c.Params = params
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
		if httpMethod != "CONNECT" && path != "/" {
			if tsr && engine.RedirectTrailingSlash {
				redirectTrailingSlash(c)
				return
			}
			if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
				return
			}
		}
		break
	}
  ...
}

去Engine中的tree中調用getValue獲取出對應的handlers進行處理。

中間件

下面就要聊到路由對應的handlers是什么了?這里我們看到tree中路由對應的是HandlersChain,實際就是[]HandlerFunc,所以一個路由,實際上會對應多個handlers。

首先我們已經把request和responseWriter封裝在context里面了,多個handler只要處理好這個context就可以了,所以是可以一個路由擁有多個handler的。

其次這里的handler是怎么來的呢?

每個路由的handler有幾個來源,第一個來源是在engine.GET的時候調用增加的。第二個來源是RouterGroup.GET的時候增加的,其實這兩種方式都是調用

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 (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
}

從兩個copy的順序可以看出,group的handler高於自定義的handler。這里自定義的handler可以是多個,比如:

router.GET("/before", MiddleWare(), func(c *gin.Context) {
  request := c.MustGet("request").(string)
  c.JSON(http.StatusOK, gin.H{
    "middile_request": request,
  })
})

func MiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("before middleware")
		c.Set("request", "clinet_request")
		c.Next()
		fmt.Println("before middleware")
	}
}

這里的/before實際上是帶了兩個handler。

第三種方法是使用Use增加中間件的方式:

router.Use(MiddleWare())

這里的會把這個中間件(實際上也是一個handler)存放到routerRroup上。所以中間件是屬於groupHandlers的。

在請求進來的時候是如何調用的呢?

答案還是在handleHTTPRequest中

func (engine *Engine) handleHTTPRequest(c *Context) {
	...
		handlers, params, tsr := root.getValue(path, c.Params, unescape)
		if handlers != nil {
			c.handlers = handlers
			c.Params = params
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
	..
}

func (c *Context) Next() {
	c.index++
	for s := int8(len(c.handlers)); c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

每個請求進來,匹配好路由之后,會獲取這個路由最終combine的handlers,把它放在全局的context中,然后通過調用context.Next()來進行遞歸調用這個handlers。當然在中間件里面需要記得調用context.Next() 把控制權還給Context。

靜態文件

golang的http包中對靜態文件的讀取是有封裝的:

func ServeFile(w ResponseWriter, r *Request, name string)

routerGroup也是有把這個封裝成為方法的

func (group *RouterGroup) Static(relativePath, root string) IRoutes {
	return group.StaticFS(relativePath, Dir(root, false))
}

func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {
  ...
	handler := group.createStaticHandler(relativePath, fs)
  ...
}

func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
	...
		fileServer.ServeHTTP(c.Writer, c.Request)
	...
}

所以調用應該像這樣:

router.Static("/assets", "./assets")
router.StaticFS("/more_static", http.Dir("my_file_system"))
router.StaticFile("/favicon.ico", "./resources/favicon.ico")

其中的StaticFS的第二個參數可以是實現了http.FileSystem的任何結構。

綁定

參數一個一個獲取是很麻煩的,我們一般還會把參數賦值到某個struct中,這個時候解析參數,賦值的過程很繁瑣。我們是不是提供一個自動綁定的方法來操作呢?

package main

import (
	"log"
	"time"

	"github.com/gin-gonic/gin"
)

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
	// If `GET`, only `Form` binding engine (`query`) used.
	// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
	// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
	if c.ShouldBind(&person) == nil {
		log.Println(person.Name)
		log.Println(person.Address)
		log.Println(person.Birthday)
	}

	c.String(200, "Success")
}
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"

這個是不是很方便?它是怎么實現的呢?

首先參數解析是和http請求的content-type頭有關,當content-type頭為application/json的時候,我們會在body中傳遞json,並且應該解析請求body中的json,而content-type頭為application/xml的時候,我們會解析body中的xml。

我們之前說了,這些解析的行為應該都是Context包了的。所以這些方法都定義在Context中

func (c *Context) ShouldBind(obj interface{}) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.ShouldBindWith(obj, b)
}

// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj interface{}) error {
	return c.ShouldBindWith(obj, binding.JSON)
}

// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
func (c *Context) ShouldBindXML(obj interface{}) error {
	return c.ShouldBindWith(obj, binding.XML)
}

// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error {
	return c.ShouldBindWith(obj, binding.Query)
}

// ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
	return b.Bind(c.Request, obj)
}

這里binding這塊應該怎么設計呢?其實知道了具體的解析方式,就知道如何綁定,比如知道了這個是json解析,我就可以很方便將參數直接json.Decode,如果知道這個是query解析,我可以直接從URL.Query中獲取請求串,如果知道這個是表單form,我就可以直接request.ParseForm來解析。

所以,這個還是一個接口,多個結構實現的設計。

定義一個接口:

type Binding interface {
	Name() string
	Bind(*http.Request, interface{}) error
}

定一個多個結構:

type formBinding struct{}

func (formBinding) Bind(req *http.Request, obj interface{}) error {
	if err := req.ParseForm(); err != nil {
		return err
	}
	req.ParseMultipartForm(defaultMemory)
	if err := mapForm(obj, req.Form); err != nil {
		return err
	}
	return validate(obj)
}

type jsonBinding struct{}

func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
	return decodeJSON(req.Body, obj)
}
var (
	JSON          = jsonBinding{}
	XML           = xmlBinding{}
	Form          = formBinding{}
	Query         = queryBinding{}
	FormPost      = formPostBinding{}
	FormMultipart = formMultipartBinding{}
	ProtoBuf      = protobufBinding{}
	MsgPack       = msgpackBinding{}
)
...

在使用綁定解析的時候,我們可以使用ShouldBindWith來指定我們要使用的是哪些解析方式。

參數驗證

我們希望在綁定參數的時候,也能給我做一下驗證,有點像laravel里面的Validater一樣,我在綁定的對象設置一下這個字段是否可以為空,是否必須是int等。官網的例子:

package main

import (
	"net/http"
	"reflect"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"gopkg.in/go-playground/validator.v8"
)

// Booking contains binded and validated data.
type Booking struct {
	CheckIn  time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
	CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}

func bookableDate(
	v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
	field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
	if date, ok := field.Interface().(time.Time); ok {
		today := time.Now()
		if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
			return false
		}
	}
	return true
}

func main() {
	route := gin.Default()

	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("bookabledate", bookableDate)
	}

	route.GET("/bookable", getBookable)
	route.Run(":8085")
}

func getBookable(c *gin.Context) {
	var b Booking
	if err := c.ShouldBindWith(&b, binding.Query); err == nil {
		c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
	} else {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}
}

這種需要怎么做呢?

首先當然在上面說的Bind的函數里面需要加上驗證的邏輯,比如像jsonBinding:

func decodeJSON(r io.Reader, obj interface{}) error {
	decoder := json.NewDecoder(r)
	if EnableDecoderUseNumber {
		decoder.UseNumber()
	}
	if err := decoder.Decode(obj); err != nil {
		return err
	}
	return validate(obj)
}

這里的validate:

func validate(obj interface{}) error {
	if Validator == nil {
		return nil
	}
	return Validator.ValidateStruct(obj)
}

var Validator StructValidator = &defaultValidator{}

調用了一個全局的defaultValidator:

type defaultValidator struct {
	once     sync.Once
	validate *validator.Validate
}

這里的defaultValidator的ValidateStruct()最終調用的就是validator.v8包的Stuct方法

func (v *defaultValidator) ValidateStruct(obj interface{}) error {
...
		if err := v.validate.Struct(obj); err != nil {
			return err
		}
...
}

同樣的,gin為了不讓Validator綁死在validator.v8上,這個default的Validator不是寫死是validator.v8的結構,而是自己定義了一個接口:

type StructValidator interface {

	ValidateStruct(interface{}) error

	Engine() interface{}
}

如果你想用其他的validator,或者自定義一個validator,那么只要實現了這個接口,就可以把它賦值到Validator就可以了。

這種用接口隔離第三方庫的方式確實很巧妙。

Logger中間件

既然有中間件機制,我們可以定義幾個默認的中間件,日志Logger()是一個必要的中間件。

這個Logger中間件的作用是記錄下每個請求的請求地址,請求時長等:

[GIN] 2018/09/18 - 11:37:32 | 200 |     413.536µs |             ::1 | GET      /index

具體實現追下去看就明白了,請求前設置開始時間,請求后設置結束時間,然后打印信息。

Recovery中間件

Recovery也是一個必要的中間件,試想一下,如果某個業務邏輯出現panic請求,難道整個http server就掛了?這是不允許的。所以這個Recovery做的事情是捕獲請求中的panic信息,吧信息打印到日志中。

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 {
				if logger != nil {
					stack := stack(3)
					httprequest, _ := httputil.DumpRequest(c.Request, false)
					logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

logger和Recovery這兩個中間件在生成默認的Engine的時候已經加上了。

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

總結

gin是個很精致的框架,它的路由,參數綁定,中間件等邏輯使用非常方便,擴展性也是設計的非常好,沒有多余的耦合。

附錄

帶個我從各個地方搜索出來的demo例子

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"log"
	"fmt"
	"time"
	"gopkg.in/go-playground/validator.v8"
	"reflect"
	"github.com/gin-gonic/gin/binding"
)

func main() {
	router := gin.Default()

	router.Use()
	router.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "It works")
	})

	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,
		})
	})

	router.POST("/upload", func(c *gin.Context) {
		// single file
		file, _ := c.FormFile("file")
		log.Println(file.Filename)

		c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
	})

	router.LoadHTMLGlob("templates/*")
	router.GET("/upload", func(c *gin.Context) {
		c.HTML(http.StatusOK, "upload.html", gin.H{})
	})
	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "Main website",
		})
	})

	router.GET("/redict/google", func(c *gin.Context) {
		c.Redirect(http.StatusMovedPermanently, "https://google.com")
	})

	v1 := router.Group("/v1")

	v1.GET("/login", func(c *gin.Context) {
		c.String(http.StatusOK, "v1 login")
	})

	v2 := router.Group("/v2")

	v2.GET("/login", func(c *gin.Context) {
		c.String(http.StatusOK, "v2 login")
	})

	router.Use(MiddleWare())

	router.GET("/before", MiddleWare(), func(c *gin.Context) {
		request := c.MustGet("request").(string)
		c.JSON(http.StatusOK, gin.H{
			"middile_request": request,
		})
	})

	router.GET("/sync", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		log.Println("Done! in path" + c.Request.URL.Path)
	})

	router.GET("/async", func(c *gin.Context) {
		cCp := c.Copy()
		go func() {
			time.Sleep(5 * time.Second)
			log.Println("Done! in path" + cCp.Request.URL.Path)
		}()
	})

	router.GET("/user/:name", func(c *gin.Context) {
		name := c.Param("name")
		c.String(http.StatusOK, "Hello %s", name)
	})

	router.GET("/welcome", func(c *gin.Context) {
		firstname := c.DefaultQuery("firstname", "Guest")
		lastname := c.Query("lastname") // shortcut for     c.Request.URL.Query().Get("lastname")

		c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
	})

	router.GET("/User/:name/*action",func (c *gin.Context){
		name:= c.Param("name")
		action := c.Param("action")
		message := name + "is" + action
		c.String(http.StatusOK,message)
	})

	router.GET("/welcome2", func(c *gin.Context) {
		firstname := c.DefaultQuery("firstname", "Guest")
		lastname := c.Query("lastname") // shortcut for     c.Request.URL.Query().Get("lastname")

		c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
	})

	router.Static("/assets", "./assets")
	router.StaticFS("/more_static", http.Dir("my_file_system"))
	router.StaticFile("/favicon.ico", "./resources/favicon.ico")

	router.GET("/testing", startPage)

	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("bookabledate", bookableDate)
	}

	router.GET("/bookable", getBookable)

	router.Run(":8001")
}

func MiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("before middleware")
		c.Set("request", "clinet_request")
		c.Next()
		fmt.Println("before middleware")
	}
}

func startPage(c *gin.Context) {
	var person Person
	if c.ShouldBind(&person) == nil {
		log.Println(person.Name)
		log.Println(person.Address)
		log.Println(person.Birthday)
	}

	c.String(200, "Success")
}

type Person struct {
	Name     string    `form:"name"`
	Address  string    `form:"address"`
	Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

type Booking struct {
	CheckIn  time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
	CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}

func bookableDate(
	v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
	field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
	if date, ok := field.Interface().(time.Time); ok {
		today := time.Now()
		if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
			return false
		}
	}
	return true
}

func getBookable(c *gin.Context) {
	var b Booking
	if err := c.ShouldBindWith(&b, binding.Query); err == nil {
		c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
	} else {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}
}



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM