Go web 框架 gin 的使用
gin 是 golang 中最流行的 web 框架,拥有高性能的路由,官网中介绍的主要特点包括快速、支持中间件、crash 处理、json 验证、支持路由组等,这些特性可以类比 node 的 koa 框架。
快速开始
安装:
go get -u github.com/gin-gonic/gin
返回一个 json 的路由:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/someJson", func(c *gin.Context) { data := map[string]interface{}{ "lang": "go lang", "tag": "<br>", } c.JSON(http.StatusOK, data) }) r.Run(":8000") }
其中 gin.Default 是默认开启 logger 和 recovery 两个中间件,从源码中可以看到,相当于是调用 New 函数之后使用 Use 开启两个中间件:
改写成使用 New 方法就是:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.New() r.Use(gin.Logger(), gin.Recovery()) r.GET("/someJson", func(c *gin.Context) { data := map[string]interface{}{ "lang": "go lang", "tag": "<br>", } c.JSON(http.StatusOK, data) }) r.Run(":8000") }
路由和路由组
Gin 支持 get、post、patch、delete、put、options、head、any,其中 any 是支持 get、post、patch、delete、put、options、head 这7种方法,gin 提供了这些 http 方法的的大写形式的方法,从 gin 的源码中可以看到这些是 RouterGroup 结构体的方法:
这些方法都是 Handle 的快捷方法,这个从源码可以看到都同样使用了 group.handle 方法,Handle 中对传入的 httpMethod 方法做了字符串校验:
我们将上面的 r.GET 修改为 Handle 方法,如下:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.New() r.Use(gin.Logger(), gin.Recovery()) r.Handle(http.MethodGet, "/someJson", func(c *gin.Context) { data := map[string]interface{}{ "lang": "go lang", "tag": "<br>", } c.JSON(http.StatusOK, data) }) r.Run(":8000") }
在实际开发中,我们经常有对 api 版本、业务模块划分路由的场景,在 gin 中可以通过路由组来实现,即 Group 方法:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() v1 := r.Group("/api/v1") v1.GET("/getUser", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "name": "golang", "id": "1", }) }) r.Run(":8000") }
从源码中可以看到 Group 方法返回了一个新的 RouterGroup,同时将对应的 routePath 计算作为一个 basePath
在处理路由的时候 handle 方法会 调用 calculateAbsolutePath 方法计算出最后的路由路径
路由参数
Gin 的路由基于的是 httprouter,和 koa 一样以 :param 的方式作为一个路由参数,通过 context 的 Param 方法获取对应的值,如:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/user/:id", func(c *gin.Context) { id := c.Param("id") c.JSON(http.StatusOK, gin.H{ "user": id, }) }) r.Run(":8000") }
通过源码我们可以看到是通过 c.Params.ByName 获取的,c.Params 本质上是一个存储参数的 slice
路由参数还支持以 * 开头匹配所有,如:
package main import ( "fmt" "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/article/*id", func(c *gin.Context) { id := c.Param("id") c.JSON(http.StatusOK, gin.H{ "article": id, }) }) r.Run(":8000") }
这段代码可以匹配以下所有路由:
/aritcle/123 /article/123/info /aritcle/123/author/info
Get 和 Post 参数
在开发中最常见的就是通过 get(query string)、post 参数(http body)来向服务端传递数据,gin 通过 Context 的 Query 获取对应的 get 参数:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/user", func(c *gin.Context) { id := c.Query("id") c.JSON(http.StatusOK, gin.H{ "id": id, }) }) r.Run(":8000") }
在源码中我们可以看到调用的是 context 的 GetQuery 方法:
query 在 gin 内部是通过一个 map 来存储,map 定义为 map[string][]string,本质上是通过 context 的 c.Request.URL.Query() 方法拿到的:
在 gin 内部是通过 parseQuery 方法来解析的,从返回值也可以看出是一个以 string 为 key,string 数组为值的 map,如果想要为 query 不存在时设置一个默认值,可以使用 DefaultQuery 方法,在这个方法内部也是使用 GetQuery 方法,当不存在时使用默认值
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/user", func(c *gin.Context) { id := c.DefaultQuery("id", "456") c.JSON(http.StatusOK, gin.H{ "id": id, }) }) r.Run(":8000") }
向服务端发数据的时候常常使用 post 方法,以 form-data 的形式存放在 http body 内,在 gin 中可以通过 PostForm 方法获取对应的值:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.POST("/user", func(c *gin.Context) { id := c.PostForm("id") c.JSON(http.StatusOK, gin.H{ "code": 200, "id": id, }) }) r.Run(":8000") }
和 query 类似,可以通过 DefaultPostForm 来设置对应的默认值,内部都是通过 GetPostForm 方法来获取的:
id := c.DefaultPostForm("id", "456")
cookie 和 http header
开发过程中常常需要获取和设置 cookie,可以通过 c.Cookie 获取对应的 cookie 值,使用 c.SetCookie 来设置 cookie:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/user", func(c *gin.Context) { session, _ := c.Cookie("session") c.SetCookie("site_cookie", "cookie1", 3600, "/", "localhost", false, true) c.JSON(http.StatusOK, gin.H{ "code": 200, "session": session, }) }) r.Run(":8000") }
获取 cookie 内部也是通过 c.Request.Cookie 来获取的,c.Request.Cookie 会读取解析 http 头 cookie 字段:
Http header 可以通过 c.GetHeader(key) 的形式获取:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/user", func(c *gin.Context) { lang := c.GetHeader("lang") c.JSON(http.StatusOK, gin.H{ "code": 200, "lang": lang, }) }) r.Run(":8000") }
本质上是通过 request 的 header.get 方法获取的,即 c.Request.Header.Get,源码如下:
设置 HTTP 响应头通过 Header 方法即可:
c.Header("user", "golang")
实际上是通过 http 包的 Header struct 来设置的:
重定向
gin 框架的重定向 context 下有 Redirect 方法帮助我们重定向,也可以直接修改 context request 的 url 信息,然后继续处理 context:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/info", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/user") }) r.GET("/article", func(c *gin.Context) { c.Request.URL.Path = "/user" r.HandleContext(c) }) r.GET("/user", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "code": 200, "data": "user", }) }) r.Run(":8000") }
Redirect 本质上也是调用的 http 包的 Redirect 方法:
静态资源和模版引擎
设置静态资源和模版引擎是一个 web 服务器最基本的能力,gin 通过路由的 Static、StaticFS、StaticFile 三种方法设置:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.Static("/", "./public") r.StaticFile("/", "./public") r.StaticFS("/", http.Dir("./public")) r.Run(":8000") }
Static 本质上内部也是通过 StaticFS 方法来实现的:
golang 有一个模版引擎标准库 http/template,gin 内部默认也是使用这个标准库,这个库和我们常用的模版引擎类似,使用胡子表达式作为变量,gin 中使用 LoadHTMLGlob 方法加载模版,使用 c.HTML 表示使用模版引擎处理,源代码可以在 gin 的 render/html.go 中:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.LoadHTMLGlob("./template/*") r.GET("/index", func(c *gin.Context) { c.HTML(http.StatusOK, "index.tmpl", gin.H{ "title": "golang", }) }) r.Run(":8000") }
<html> <h1> {{ .title }} </h1> </html>
Context
gin 的 context 贯穿一次 http 请求的全流程,可以类比 koa 的 context,context 也是 gin 中最核心的一个对象,这个对象含有的字段从源码中可以看到有以下属性:
// Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { writermem responseWriter Request *http.Request Writer ResponseWriter Params Params handlers HandlersChain index int8 fullPath string engine *Engine params *Params // This mutex protect Keys map mu sync.RWMutex // Keys is a key/value pair exclusively for the context of each request. Keys map[string]interface{} // Errors is a list of errors attached to all the handlers/middlewares who used this context. Errors errorMsgs // Accepted defines a list of manually accepted formats for content negotiation. Accepted []string // queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query() queryCache url.Values // formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH, // or PUT body parameters. formCache url.Values // SameSite allows a server to define a cookie attribute making it impossible for // the browser to send this cookie along with cross-site requests. sameSite http.SameSite }
上面对 query、postForm、param 、header、cookie 等的操作都是挂在 context 上的,context 也提供了基本的元数据存取,Get 和 Set,这样我们可以把需要的(尤其是跨中间件使用的)数据挂在 context 上,本质上是存储在 c.Keys 上的:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/user", func(c *gin.Context) { c.Set("key", "value") val, _ := c.Get("key") c.JSON(http.StatusOK, gin.H{ "code": 200, "key": val, }) }) r.Run(":8000") }
除了 Get 存储外,gin 还内置了 MustGet(不存在就出发 panic)、对类型断言后的取值如 GetString、GetBool、GetInt 等
对元数据存取的方法有以下:
/************************************/ /******** METADATA MANAGEMENT********/ /************************************/ func (c *Context) Set(key string, value interface{}) {} // Get returns the value for the given key, ie: (value, true). // If the value does not exists it returns (nil, false) func (c *Context) Get(key string) (value interface{}, exists bool) {} // MustGet returns the value for the given key if it exists, otherwise it panics. func (c *Context) MustGet(key string) interface{} {} // GetString returns the value associated with the key as a string. func (c *Context) GetString(key string) (s string) {} // GetBool returns the value associated with the key as a boolean. func (c *Context) GetBool(key string) (b bool) {} // GetInt returns the value associated with the key as an integer. func (c *Context) GetInt(key string) (i int) {} // GetInt64 returns the value associated with the key as an integer. func (c *Context) GetInt64(key string) (i64 int64) {} // GetUint returns the value associated with the key as an unsigned integer. func (c *Context) GetUint(key string) (ui uint) { } // GetUint64 returns the value associated with the key as an unsigned integer. func (c *Context) GetUint64(key string) (ui64 uint64) {} // GetFloat64 returns the value associated with the key as a float64. func (c *Context) GetFloat64(key string) (f64 float64) {} // GetTime returns the value associated with the key as time. func (c *Context) GetTime(key string) (t time.Time) {} // GetDuration returns the value associated with the key as a duration. func (c *Context) GetDuration(key string) (d time.Duration) {} // GetStringSlice returns the value associated with the key as a slice of strings. func (c *Context) GetStringSlice(key string) (ss []string) {} // GetStringMap returns the value associated with the key as a map of interfaces. func (c *Context) GetStringMap(key string) (sm map[string]interface{}) {} // GetStringMapString returns the value associated with the key as a map of strings. func (c *Context) GetStringMapString(key string) (sms map[string]string) {} // GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {}
gin 中把对 query、postForm、param 等请求数据称为 Input Data,从 gin 的源码中可以看到这部分的方法主要有对 query、postForm、param 等信息的获取和参数绑定的方法,源码如下:
/************************************/ /************ INPUT DATA ************/ /************************************/ // Param returns the value of the URL param. // It is a shortcut for c.Params.ByName(key) // router.GET("/user/:id", func(c *gin.Context) { // // a GET request to /user/john // id := c.Param("id") // id == "john" // }) func (c *Context) Param(key string) string { return c.Params.ByName(key) } // Query returns the keyed url query value if it exists, // otherwise it returns an empty string `("")`. // It is shortcut for `c.Request.URL.Query().Get(key)` // GET /path?id=1234&name=Manu&value= // c.Query("id") == "1234" // c.Query("name") == "Manu" // c.Query("value") == "" // c.Query("wtf") == "" func (c *Context) Query(key string) string { value, _ := c.GetQuery(key) return value } // DefaultQuery returns the keyed url query value if it exists, // otherwise it returns the specified defaultValue string. // See: Query() and GetQuery() for further information. // GET /?name=Manu&lastname= // c.DefaultQuery("name", "unknown") == "Manu" // c.DefaultQuery("id", "none") == "none" // c.DefaultQuery("lastname", "none") == "" func (c *Context) DefaultQuery(key, defaultValue string) string { if value, ok := c.GetQuery(key); ok { return value } return defaultValue } // GetQuery is like Query(), it returns the keyed url query value // if it exists `(value, true)` (even when the value is an empty string), // otherwise it returns `("", false)`. // It is shortcut for `c.Request.URL.Query().Get(key)` // GET /?name=Manu&lastname= // ("Manu", true) == c.GetQuery("name") // ("", false) == c.GetQuery("id") // ("", true) == c.GetQuery("lastname") func (c *Context) GetQuery(key string) (string, bool) { if values, ok := c.GetQueryArray(key); ok { return values[0], ok } return "", false } // QueryArray returns a slice of strings for a given query key. // The length of the slice depends on the number of params with the given key. func (c *Context) QueryArray(key string) []string { values, _ := c.GetQueryArray(key) return values } func (c *Context) initQueryCache() { if c.queryCache == nil { if c.Request != nil { c.queryCache = c.Request.URL.Query() } else { c.queryCache = url.Values{} } } } // GetQueryArray returns a slice of strings for a given query key, plus // a boolean value whether at least one value exists for the given key. func (c *Context) GetQueryArray(key string) ([]string, bool) { c.initQueryCache() if values, ok := c.queryCache[key]; ok && len(values) > 0 { return values, true } return []string{}, false } // QueryMap returns a map for a given query key. func (c *Context) QueryMap(key string) map[string]string { dicts, _ := c.GetQueryMap(key) return dicts } // GetQueryMap returns a map for a given query key, plus a boolean value // whether at least one value exists for the given key. func (c *Context) GetQueryMap(key string) (map[string]string, bool) { c.initQueryCache() return c.get(c.queryCache, key) } // PostForm returns the specified key from a POST urlencoded form or multipart form // when it exists, otherwise it returns an empty string `("")`. func (c *Context) PostForm(key string) string { value, _ := c.GetPostForm(key) return value } // DefaultPostForm returns the specified key from a POST urlencoded form or multipart form // when it exists, otherwise it returns the specified defaultValue string. // See: PostForm() and GetPostForm() for further information. func (c *Context) DefaultPostForm(key, defaultValue string) string { if value, ok := c.GetPostForm(key); ok { return value } return defaultValue } // GetPostForm is like PostForm(key). It returns the specified key from a POST urlencoded // form or multipart form when it exists `(value, true)` (even when the value is an empty string), // otherwise it returns ("", false). // For example, during a PATCH request to update the user's email: // email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com" // email= --> ("", true) := GetPostForm("email") // set email to "" // --> ("", false) := GetPostForm("email") // do nothing with email func (c *Context) GetPostForm(key string) (string, bool) { if values, ok := c.GetPostFormArray(key); ok { return values[0], ok } return "", false } // PostFormArray returns a slice of strings for a given form key. // The length of the slice depends on the number of params with the given key. func (c *Context) PostFormArray(key string) []string { values, _ := c.GetPostFormArray(key) return values } func (c *Context) initFormCache() { if c.formCache == nil { c.formCache = make(url.Values) req := c.Request if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { if err != http.ErrNotMultipart { debugPrint("error on parse multipart form array: %v", err) } } c.formCache = req.PostForm } } // GetPostFormArray returns a slice of strings for a given form key, plus // a boolean value whether at least one value exists for the given key. func (c *Context) GetPostFormArray(key string) ([]string, bool) { c.initFormCache() if values := c.formCache[key]; len(values) > 0 { return values, true } return []string{}, false } // PostFormMap returns a map for a given form key. func (c *Context) PostFormMap(key string) map[string]string { dicts, _ := c.GetPostFormMap(key) return dicts } // GetPostFormMap returns a map for a given form key, plus a boolean value // whether at least one value exists for the given key. func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { c.initFormCache() return c.get(c.formCache, key) } // get is an internal method and returns a map which satisfy conditions. func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) { dicts := make(map[string]string) exist := false for k, v := range m { if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key { if j := strings.IndexByte(k[i+1:], ']'); j >= 1 { exist = true dicts[k[i+1:][:j]] = v[0] } } } return dicts, exist } // FormFile returns the first file for the provided form key. func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { if c.Request.MultipartForm == nil { if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { return nil, err } } f, fh, err := c.Request.FormFile(name) if err != nil { return nil, err } f.Close() return fh, err } // MultipartForm is the parsed multipart form, including file uploads. func (c *Context) MultipartForm() (*multipart.Form, error) { err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory) return c.Request.MultipartForm, err } // SaveUploadedFile uploads the form file to specific dst. func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error { src, err := file.Open() if err != nil { return err } defer src.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, src) return err } // Bind checks the Content-Type to select a binding engine automatically, // Depending the "Content-Type" header different bindings are used: // "application/json" --> JSON binding // "application/xml" --> XML binding // otherwise --> returns an error. // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It decodes the json payload into the struct specified as a pointer. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. func (c *Context) Bind(obj interface{}) error { b := binding.Default(c.Request.Method, c.ContentType()) return c.MustBindWith(obj, b) } // BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON). func (c *Context) BindJSON(obj interface{}) error { return c.MustBindWith(obj, binding.JSON) } // BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML). func (c *Context) BindXML(obj interface{}) error { return c.MustBindWith(obj, binding.XML) } // BindQuery is a shortcut for c.MustBindWith(obj, binding.Query). func (c *Context) BindQuery(obj interface{}) error { return c.MustBindWith(obj, binding.Query) } // BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML). func (c *Context) BindYAML(obj interface{}) error { return c.MustBindWith(obj, binding.YAML) } // BindHeader is a shortcut for c.MustBindWith(obj, binding.Header). func (c *Context) BindHeader(obj interface{}) error { return c.MustBindWith(obj, binding.Header) } // BindUri binds the passed struct pointer using binding.Uri. // It will abort the request with HTTP 400 if any error occurs. func (c *Context) BindUri(obj interface{}) error { if err := c.ShouldBindUri(obj); err != nil { c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck return err } return nil } // MustBindWith binds the passed struct pointer using the specified binding engine. // It will abort the request with HTTP 400 if any error occurs. // See the binding package. func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error { if err := c.ShouldBindWith(obj, b); err != nil { c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck return err } return nil } // ShouldBind checks the Content-Type to select a binding engine automatically, // Depending the "Content-Type" header different bindings are used: // "application/json" --> JSON binding // "application/xml" --> XML binding // otherwise --> returns an error // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It decodes the json payload into the struct specified as a pointer. // Like c.Bind() but this method does not set the response status code to 400 and abort if the json is not valid. 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) } // ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML). func (c *Context) ShouldBindYAML(obj interface{}) error { return c.ShouldBindWith(obj, binding.YAML) } // ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header). func (c *Context) ShouldBindHeader(obj interface{}) error { return c.ShouldBindWith(obj, binding.Header) } // ShouldBindUri binds the passed struct pointer using the specified binding engine. func (c *Context) ShouldBindUri(obj interface{}) error { m := make(map[string][]string) for _, v := range c.Params { m[v.Key] = []string{v.Value} } return binding.Uri.BindUri(m, obj) } // 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) } // ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request // body into the context, and reuse when it is called again. // // NOTE: This method reads the body before binding. So you should use // ShouldBindWith for better performance if you need to call only once. func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) { var body []byte if cb, ok := c.Get(BodyBytesKey); ok { if cbb, ok := cb.([]byte); ok { body = cbb } } if body == nil { body, err = ioutil.ReadAll(c.Request.Body) if err != nil { return err } c.Set(BodyBytesKey, body) } return bb.BindBody(body, obj) } // ClientIP implements a best effort algorithm to return the real client IP. // It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not. // If it's it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]). // If the headers are nots syntactically valid OR the remote IP does not correspong to a trusted proxy, // the remote IP (coming form Request.RemoteAddr) is returned. func (c *Context) ClientIP() string { if c.engine.AppEngine { if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" { return addr } } remoteIP, trusted := c.RemoteIP() if remoteIP == nil { return "" } if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil { for _, headerName := range c.engine.RemoteIPHeaders { ip, valid := validateHeader(c.requestHeader(headerName)) if valid { return ip } } } return remoteIP.String() } // RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port). // It also checks if the remoteIP is a trusted proxy or not. // In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks // defined in Engine.TrustedProxies func (c *Context) RemoteIP() (net.IP, bool) { ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)) if err != nil { return nil, false } remoteIP := net.ParseIP(ip) if remoteIP == nil { return nil, false } if c.engine.trustedCIDRs != nil { for _, cidr := range c.engine.trustedCIDRs { if cidr.Contains(remoteIP) { return remoteIP, true } } } return remoteIP, false } func validateHeader(header string) (clientIP string, valid bool) { if header == "" { return "", false } items := strings.Split(header, ",") for i, ipStr := range items { ipStr = strings.TrimSpace(ipStr) ip := net.ParseIP(ipStr) if ip == nil { return "", false } // We need to return the first IP in the list, but, // we should not early return since we need to validate that // the rest of the header is syntactically valid if i == 0 { clientIP = ipStr valid = true } } return } // ContentType returns the Content-Type header of the request. func (c *Context) ContentType() string { return filterFlags(c.requestHeader("Content-Type")) } // IsWebsocket returns true if the request headers indicate that a websocket // handshake is being initiated by the client. func (c *Context) IsWebsocket() bool { if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") && strings.EqualFold(c.requestHeader("Upgrade"), "websocket") { return true } return false } func (c *Context) requestHeader(key string) string { return c.Request.Header.Get(key) }
gin 中像上面的对 cookie 、header、body 的存取和 c.JSON、c.HTML 等决定输出渲染类型统归类为 RESPONSE RENDERING(响应渲染),各类方法的源代码如下:
/************************************/ /******** RESPONSE RENDERING ********/ /************************************/ // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. func bodyAllowedForStatus(status int) bool { switch { case status >= 100 && status <= 199: return false case status == http.StatusNoContent: return false case status == http.StatusNotModified: return false } return true } // Status sets the HTTP response code. func (c *Context) Status(code int) { c.Writer.WriteHeader(code) } // Header is a intelligent shortcut for c.Writer.Header().Set(key, value). // It writes a header in the response. // If value == "", this method removes the header `c.Writer.Header().Del(key)` func (c *Context) Header(key, value string) { if value == "" { c.Writer.Header().Del(key) return } c.Writer.Header().Set(key, value) } // GetHeader returns value from request headers. func (c *Context) GetHeader(key string) string { return c.requestHeader(key) } // GetRawData return stream data. func (c *Context) GetRawData() ([]byte, error) { return ioutil.ReadAll(c.Request.Body) } // SetSameSite with cookie func (c *Context) SetSameSite(samesite http.SameSite) { c.sameSite = samesite } // SetCookie adds a Set-Cookie header to the ResponseWriter's headers. // The provided cookie must have a valid Name. Invalid cookies may be // silently dropped. func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) { if path == "" { path = "/" } http.SetCookie(c.Writer, &http.Cookie{ Name: name, Value: url.QueryEscape(value), MaxAge: maxAge, Path: path, Domain: domain, SameSite: c.sameSite, Secure: secure, HttpOnly: httpOnly, }) } // Cookie returns the named cookie provided in the request or // ErrNoCookie if not found. And return the named cookie is unescaped. // If multiple cookies match the given name, only one cookie will // be returned. func (c *Context) Cookie(name string) (string, error) { cookie, err := c.Request.Cookie(name) if err != nil { return "", err } val, _ := url.QueryUnescape(cookie.Value) return val, nil } // Render writes the response headers and calls render.Render to render data. func (c *Context) Render(code int, r render.Render) { c.Status(code) if !bodyAllowedForStatus(code) { r.WriteContentType(c.Writer) c.Writer.WriteHeaderNow() return } if err := r.Render(c.Writer); err != nil { panic(err) } } // HTML renders the HTTP template specified by its file name. // It also updates the HTTP code and sets the Content-Type as "text/html". // See http://golang.org/doc/articles/wiki/ func (c *Context) HTML(code int, name string, obj interface{}) { instance := c.engine.HTMLRender.Instance(name, obj) c.Render(code, instance) } // IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body. // It also sets the Content-Type as "application/json". // WARNING: we recommend to use this only for development purposes since printing pretty JSON is // more CPU and bandwidth consuming. Use Context.JSON() instead. func (c *Context) IndentedJSON(code int, obj interface{}) { c.Render(code, render.IndentedJSON{Data: obj}) } // SecureJSON serializes the given struct as Secure JSON into the response body. // Default prepends "while(1)," to response body if the given struct is array values. // It also sets the Content-Type as "application/json". func (c *Context) SecureJSON(code int, obj interface{}) { c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj}) } // JSONP serializes the given struct as JSON into the response body. // It adds padding to response body to request data from a server residing in a different domain than the client. // It also sets the Content-Type as "application/javascript". func (c *Context) JSONP(code int, obj interface{}) { callback := c.DefaultQuery("callback", "") if callback == "" { c.Render(code, render.JSON{Data: obj}) return } c.Render(code, render.JsonpJSON{Callback: callback, Data: obj}) } // JSON serializes the given struct as JSON into the response body. // It also sets the Content-Type as "application/json". func (c *Context) JSON(code int, obj interface{}) { c.Render(code, render.JSON{Data: obj}) } // AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string. // It also sets the Content-Type as "application/json". func (c *Context) AsciiJSON(code int, obj interface{}) { c.Render(code, render.AsciiJSON{Data: obj}) } // PureJSON serializes the given struct as JSON into the response body. // PureJSON, unlike JSON, does not replace special html characters with their unicode entities. func (c *Context) PureJSON(code int, obj interface{}) { c.Render(code, render.PureJSON{Data: obj}) } // XML serializes the given struct as XML into the response body. // It also sets the Content-Type as "application/xml". func (c *Context) XML(code int, obj interface{}) { c.Render(code, render.XML{Data: obj}) } // YAML serializes the given struct as YAML into the response body. func (c *Context) YAML(code int, obj interface{}) { c.Render(code, render.YAML{Data: obj}) } // ProtoBuf serializes the given struct as ProtoBuf into the response body. func (c *Context) ProtoBuf(code int, obj interface{}) { c.Render(code, render.ProtoBuf{Data: obj}) } // String writes the given string into the response body. func (c *Context) String(code int, format string, values ...interface{}) { c.Render(code, render.String{Format: format, Data: values}) } // Redirect returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { c.Render(-1, render.Redirect{ Code: code, Location: location, Request: c.Request, }) } // Data writes some data into the body stream and updates the HTTP code. func (c *Context) Data(code int, contentType string, data []byte) { c.Render(code, render.Data{ ContentType: contentType, Data: data, }) } // DataFromReader writes the specified reader into the body stream and updates the HTTP code. func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) { c.Render(code, render.Reader{ Headers: extraHeaders, ContentType: contentType, ContentLength: contentLength, Reader: reader, }) } // File writes the specified file into the body stream in an efficient way. func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } // FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way. func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { defer func(old string) { c.Request.URL.Path = old }(c.Request.URL.Path) c.Request.URL.Path = filepath http.FileServer(fs).ServeHTTP(c.Writer, c.Request) } // FileAttachment writes the specified file into the body stream in an efficient way // On the client side, the file will typically be downloaded with the given filename func (c *Context) FileAttachment(filepath, filename string) { c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename="%s"", filename)) http.ServeFile(c.Writer, c.Request, filepath) } // SSEvent writes a Server-Sent Event into the body stream. func (c *Context) SSEvent(name string, message interface{}) { c.Render(-1, sse.Event{ Event: name, Data: message, }) } // Stream sends a streaming response and returns a boolean // indicates "Is client disconnected in middle of stream" func (c *Context) Stream(step func(w io.Writer) bool) bool { w := c.Writer clientGone := w.CloseNotify() for { select { case <-clientGone: return true default: keepOpen := step(w) w.Flush() if !keepOpen { return false } } } }
Http Request
在 golang 中使用 http 包可以很方便的实现一个简单的服务器,如下:
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/user", func(writer http.ResponseWriter, request *http.Request) { //writer.Write() _, err :=writer.Write([]byte("hello world")) if err!= nil { fmt.Println(err) } }) err := http.ListenAndServe(":8000",nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }
和 gin 一样,路由的 handler 中都涉及到 http.ResponseWriter 和 http.Request 这两个对象,在 gin 中 http.ResponseWriter 被包裹在了一个 *ResponseWriter 对象中,http.Request 则是直接挂在 context 上的:
中间件
gin 的中间件可以类比 koa 的中间件,是一种洋葱模型,这个模型的中心是最终处理请求的 handler,称之为 main handler,其他为称为 middleware handler,每一个 middleware handle 可以分为两部分,随着 request 的流动,左边是入,右边为出,而分割点就是 next,本质就是通过这个next来执行函数链 ,各个中间件符合先进后出原则:
如下自定义两个全局使用的中间件,可以反映出这个模型:
package main import ( "fmt" "github.com/gin-gonic/gin" "net/http" ) func m1() gin.HandlerFunc{ return func(c *gin.Context) { fmt.Println("m1 start") c.Next() fmt.Println("m1 end") } } func m2() gin.HandlerFunc { return func(c *gin.Context) { fmt.Println("m2 start") c.Next() fmt.Println("m2 end") } } func main() { r := gin.New() r.Use(m1()) r.Use(m2()) r.GET("/user", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "code": 200, "data": "user", }) }) r.Run(":8000") }
当请求打进来的时候,输出将是 m1 start、m2 start、m2 end、m1 start:
在上文最常用的 gin.Default 中默认使用了 Logger 和 Recovery 作为全局中间件,分别作为日志和 panic 处理,这种直接使用 engine 的 Use 方法来挂载中间件的方法,会使得中间件在全局起作用
和 koa 类似,我们可以把中间件挂载在路由和路由组上,这样这些中间件就只会在匹配的路由中生效,如下,m1 只会在 user 这个路由组下生效,m2 则只会在 /article 这个路由下生效:
package main import ( "fmt" "github.com/gin-gonic/gin" "net/http" ) func m1() gin.HandlerFunc{ return func(c *gin.Context) { fmt.Println("m1 start") c.Next() fmt.Println("m1 end") } } func m2() gin.HandlerFunc { return func(c *gin.Context) { fmt.Println("m2 start") c.Next() fmt.Println("m2 end") } } func main() { r := gin.New() userRouter := r.Group("/user", m1()) userRouter.GET("/info", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "code": 200, "data": "info", }) }) r.GET("/article", m2(), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "code": 200, "data": "article", }) }) r.Run(":8000") }
在实际开发中经常需要自定义中间件,一个中间件本质上就是一个 handler 函数即是一个以 *gin.Context 为参数的函数,在实际开发中中间件可以是一个返回 handler 的函数,这样使用的时候调用这个函数即可,如果本身就是一个 handler,则不需要调用,直接作为参数即可:
package main import ( "fmt" "github.com/gin-gonic/gin" "net/http" ) func middle1() gin.HandlerFunc{ return func(c *gin.Context) { fmt.Println("middleware 1") c.Next() } } func middle2(c *gin.Context) { fmt.Println("middleware 2") c.Next() } func main() { r := gin.Default() r.Use(middle1()) r.Use(middle2) r.GET("/user", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "data": "user", }) }) r.Run(":8000") }
中间件中除了 Next 外还可以使用 Abort、AbortWithStatus、AbortWithStatusJSON、AbortWithError 方法拦截请求,这种常常可以用于鉴权、鉴参数等前置流程,如下,当请求的 header 没有 token 时会拦截请求,直接返回没权限:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func auth() gin.HandlerFunc{ return func(c *gin.Context) { token := c.GetHeader("token") if token == "" { c.AbortWithStatusJSON(401, gin.H{ "message": "没有权限", }) } c.Next() } } func main() { r := gin.Default() r.Use(auth()) r.GET("/user", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "data": "user", }) }) r.Run(":8000") }
在 gin 的 官方文档下,可以看到官方维护和外部维护的中间件列表:https://github.com/gin-gonic/contrib,需要使用的时候可以先搜索这里
脚手架工程
- vsouza/go-gin-boilerplate
- https://github.com/Gourouting/singo
- https://github.com/doublesouth/gin-scaffold
可参考的开源代码仓库
参考
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论