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 WriterResponseWriter ... } type responseWriter struct { http.ResponseWriter sizeint 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 { ... poolsync.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 { ... poolsync.Pool treesmethodTrees } type methodTrees []methodTree type methodTree struct { method string root*node } type node struct { pathstring indicesstring children[]*node handlersHandlersChain priorityuint32 nTypenodeType 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 rootbool } 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 ... poolsync.Pool treesmethodTrees }
看到這裡就有一點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 { Namestring`form:"name"` Addressstring`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 { CheckIntime.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 { oncesync.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 forc.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 forc.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 { Namestring`form:"name"` Addressstring`form:"address"` Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` } type Booking struct { CheckIntime.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()}) } }