Gin Web Framework

Kesa...大约 31 分钟golanggin

参考 Gin 官方文档,记录学习 Gin 的笔记

1. Introduction

Gin is a web framework written in GO. It features a martini-like API with performance that is up to 40 times faster thanks to httprouteropen in new window.

2. Installation

You can use the below Go command to install Gin.

$ go get -u github.com/gin-gonic/gin

3. Quick Start

// gin-note/quick-start/main.go
package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})
	router.Run(":9090")
}

Run main.go:

$ go run ./main.go 

Visit http://127.0.0.1:9090/ping:

$ curl -X GET http://127.0.0.1:9090/ping
{"message":"pong"}

4. API Examples

You can find a number of ready-to-run examples at Gin examples repositoryopen in new window(之后会单独学习这些示例)

4.1 Using GET,POST,PUT,PATCH,DELETE and OPTIONS

// gin-note/http-methods/main.go
package main

import (
	"net/http"

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

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

	router.GET("/get", getting)
	router.POST("/post", posting)
	router.PUT("/put", putting)
	router.DELETE("/delete", deleting)
	router.PATCH("/patch", patching)
	router.HEAD("/head", head)
	router.OPTIONS("/option", options)

	router.Run(":9090")
}

func getting(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "GET",
	})
}

func posting(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "POST",
	})
}

func putting(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "PUT",
	})
}

func deleting(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "DELETE",
	})
}

func patching(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "PATCH",
	})
}

func head(c *gin.Context) {
	c.Header("key", "val")
}

func options(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "OPTION",
	})
}
  • gin.Default: Creates a gin router with default middleware, logger and recovery (crash-free) middleware
  • router.Run: By default it serves on :8080 unless a PORT environment variable was defined. You can use router.Run(":9090") for a hard coded port

tips:

HTTP HEAD 方法 请求资源的头部信息, 并且这些头部与 HTTP GETopen in new window 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源

4.2 Parameters in path

// gin-note/params-in-path/main.go
package main

import (
	"net/http"

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

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

	router.GET("/user/:name", hello)
	router.GET("/user/:name/*action", action)
	router.POST("/user/:name/*action", fullPath)
	router.GET("/user/groups", groups)

	router.Run(":9090")
}

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

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

func fullPath(c *gin.Context) {
	path := c.FullPath()
	b := path == "/user/:name/*action"
	c.String(http.StatusOK, "path: %s,%t", path, b)
}

func groups(c *gin.Context) {
	c.String(http.StatusOK, "The available groups are [...]")
}
  • /user/:name: this handler will match /user/john but will not match /user/ or /user
  • user/:name/*action: will match user/john/ and /user/john/send
  • c.FullPath: for each matched request Context will hold the route definition
  • /user/groups: this handler will add a new router for /user/groups. Exact routes are resolved before param routes, regardless of the order they were defined. Routes starting with /user/groups are never interpreted as /user/:name/... routes

Run main.go and test:

$ curl http://127.0.0.1:9090/user/kesa
Hello kesa
$ curl http://127.0.0.1:9090/user/kesa/
kesa is /
$ curl http://127.0.0.1:9090/user/kesa/send
kesa is /send
$ curl -X POST http://127.0.0.1:9090/user/kesa/
path: /user/:name/*action,true
$ curl http://127.0.0.1:9090/user/groups
The available groups are [...]
  • user/kesa/将会匹配user/:name/*action,而不是user/:name
  • user/groups优先进行精准匹配

tips:

路由树

在 Gin 框架中,路由规则被分成了最多 9 棵前缀树,每一个 HTTP Method对应一棵 前缀树 ,树的节点按照 URL 中的 / 符号进行层级划分,URL 支持 :name 形式的名称匹配,还支持 *subpath 形式的路径通配符

// 匹配单节点 named
pattern = /book/:id
match /book/123
nomatch /book/123/10
nomatch /book/

// 匹配子节点 catchAll mode
/book/*subpath
match /book/
match /book/123
match /book/123/10

4.3 QueryString parameters

// gin-note/query-string-params/main.go
package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/welcome", welcome)
	router.Run(":9090")
}

func welcome(c *gin.Context) {
	firstname := c.Query("firstname")
	lastname := c.Query("lastname")
	c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
}

Run and test:

$ curl http://127.0.0.1:9090/welcome?firstname=kesa&lastname=jin
Hello kesa jin

4.4 Multipart/Urlencoded Form

//
package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.POST("/form_post", formPost)
	router.Run(":9090")
}

func formPost(c *gin.Context) {
	message := c.PostForm("message")
	nick := c.DefaultPostForm("nick", "anonymous")
	c.JSON(http.StatusOK, gin.H{
		"status":  "posted",
		"message": message,
		"nick":    nick,
	})
}

Run and test:

$ curl -X POST -d "message=hello" http://127.0.0.1:9090/form_post  
{"message":"hello","nick":"anonymous","status":"posted"}

tips:

Difference between form-data and x-www-form-urlencoded

These are different Form content types defined by W3C. If you want to send simple text/ ASCII data, then x-www-form-urlencoded will work. This is the default.

But if you have to send non-ASCII text or large binary data, the form-data is for that.

4.5 Query + post form

// gin-note/query-and-form/main.go
package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.POST("/post", details)
	router.Run(":9090")
}

func details(c *gin.Context) {
	id := c.Query("id")
	page := c.DefaultQuery("page", "0")
	message := c.PostForm("message")
	name := c.PostForm("name")
	c.JSON(http.StatusOK, gin.H{
		"id":      id,
		"page":    page,
		"name":    name,
		"message": message,
	})
}

Run and test:

$ curl -X POST -d "message=hello&name=kesa" http://127.0.0.1:9090/post\?id\=1\&page\=1
{"id":"1","message":"hello","name":"kesa","page":"1"}

4.6 Map as querystring or postform parameters

// gin-note/map-as-param/main.go
package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.POST("/post", post)
	router.Run(":9090")
}

func post(c *gin.Context) {
	ids := c.QueryMap("ids")
	names := c.PostFormMap("names")
	c.JSON(http.StatusOK, gin.H{
		"ids":   ids,
		"names": names,
	})
}

Run and test :

$ curl -X POST -d "names[first]=kesa&names[second]=jin" -g "http://127.0.0.1:9090/post?ids[a]=1&ids[b]=2"
{"ids":{"a":"1","b":"2"},"names":{"first":"kesa","second":"jin"}}

tips:

curl 发送请求时要使用-g 以禁用网址序列和范围使用{}和[],否则参数中带有{},[]将会报错

4.7 Upload files

Single file

file.Filename SHOULD NOT be trusted. See Content-Disposition on MDNopen in new window and #1693open in new window

The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.

// gin-note/upload-single-file/main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"path/filepath"

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

func main() {
	router := gin.Default()
	router.MaxMultipartMemory = 8 << 20 // 8 MiB
	router.POST("/upload", upload)
	router.Run(":9090")
}

func upload(c *gin.Context) {
	file, _ := c.FormFile("file")
	log.Println(file.Filename)
	dst := filepath.Base(file.Filename)
	log.Println("dst:", dst)
	c.SaveUploadedFile(file, dst)
	c.String(http.StatusOK, fmt.Sprintf("%s uploaded", file.Filename))
}

  • router.MaxMultipartMemory = 8 << 20: Set a lower memory limit for multipart forms (default is 32 MiB)

Run and test:

$ curl -X POST http://127.0.0.1:9090/upload -F "[email protected]" \    
-H "Content-Type: multipart/form-data"
test.txt uploaded

注意

这里特别强调不要信任任何用户定义的文件名,非常危险#1693open in new window

比如用户设定用户名为../test.txt,可以改变存储的位置,或者文件名为 html 标签,这样都是非常危险的

简单的处理方案是使用filepath.Base过滤文件名(../text.txt->text.txt),但这样仍有安全隐患,应该自定义文件名,比如

now := time.Now().Format("20060102150405")
dst := fmt.Sprintf("%s_%s", username, now)

Multiple files

// gin-note/upload-multiple-files/main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"path/filepath"
	"strings"
	"time"

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

func main() {
	router := gin.Default()
	router.MaxMultipartMemory = 8 << 20
	router.POST("/upload", upload)
	router.Run(":9090")
}

func upload(c *gin.Context) {
	form, _ := c.MultipartForm()
	files := form.File["upload[]"]
	username := c.Query("username")
	for _, file := range files {
		log.Println(file.Filename)
		extension := filepath.Ext(file.Filename)
		filename := strings.TrimSuffix(file.Filename, extension)
		now := time.Now().Format("20060102150405")
		dst := fmt.Sprintf("%s_%s_%s%s", now, username, filename, extension)
		c.SaveUploadedFile(file, dst)
	}
	c.String(http.StatusOK, fmt.Sprintf("%d files uploaded", len(files)))
}

Run and test:

$ curl -X POST "http://127.0.0.1:9090/upload?username=kesa" \   
-F "upload[][email protected]" \
-F "upload[][email protected]" \
-F "upload[][email protected]" \
-H "Content-Type: multipart/form-data"
3 files uploaded

4.8 Grouping routes

// gin-note/grouping-routes/main.go
package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	v1 := router.Group("/v1")
	{
		v1.GET("/get", v1Get)
	}
	v2 := router.Group("/v2")
	{
		v2.GET("/get", v2Get)
	}
	router.Run(":9090")
}

func v1Get(c *gin.Context) {
	c.String(http.StatusOK, "V1 GET ")
}

func v2Get(c *gin.Context) {
	c.String(http.StatusOK, "V2 GET ")
}

Run and test:

$ curl "http://127.0.0.1:9090/v1/get"
V1 GET 
$ curl "http://127.0.0.1:9090/v2/get"
V2 GET          

4.9 Using middleware

Blank Gin without middleware by default

Use

router := gin.New()

instead of

// Default with the Logger and Recovery middleware already attached
router := gin.Default()

Using middleware

// gin-note/using-middleware/main.go
package main

import (
	"fmt"
	"net/http"

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

func main() {
	router := gin.New()
	router.Use(gin.Logger())
	router.Use(gin.Recovery())
	router.Use(MyGlobalMiddleware())

	router.GET("/req", func(c *gin.Context) {
		middleware := c.MustGet("middleware")
		c.String(http.StatusOK, fmt.Sprintf("Middleware type: %s", middleware))
	})

	router.GET("/req1", MyRouteMiddleware(), func(c *gin.Context) {
		middleware := c.MustGet("middleware")
		c.String(http.StatusOK, fmt.Sprintf("Middleware type: %s", middleware))
	})

	grp := router.Group("/grp")
	grp.Use(MyGroupMiddleware())
	{
		grp.GET("/req", func(c *gin.Context) {
			middleware := c.MustGet("middleware")
			c.String(http.StatusOK, fmt.Sprintf("Middleware type: %s", middleware))
		})
	}

	router.Run(":9090")
}

func MyGlobalMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set("middleware", "global")
		fmt.Println("[Global] Before request")
		c.Next()
		fmt.Println("[Global] After request")
	}
}

func MyRouteMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set("middleware", "route")
		fmt.Println("[Route] Before request")
		c.Next()
		fmt.Println("[Route] After request")
	}
}

func MyGroupMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set("middleware", "group")
		fmt.Println("[Group] Before request")
		c.Next()
		fmt.Println("[Group] After request")
	}
}
  • router.Use(gin.Logger()): Global middleware, Logger middleware will write the logs to gin.DefaultWrite even if you set with GIN_MODE=release, by default gin.DefaultWriter = os.Stdout
  • router.Use(gin.Recovery()): Recovery middleware recovers from any panics and writes a 500(http code) if there was one
  • router.GET("/req1", MyRouteMiddleware(), func(c *gin.Context){...}): Per route middleware, you can add as many as you desire
  • grp.Use(MyGroupMiddleware()): Per group middleware, use custom middleware just in this group

Run and test:

$ curl -X GET "http://127.0.0.1:9090/req" 
Middleware type: global
$ curl -X GET "http://127.0.0.1:9090/req1"
Middleware type: route
$ curl -X GET "http://127.0.0.1:9090/grp/req"
Middleware type: group

tips:

调用/grp/req的时候,日志如下:

2021/11/18 09:58:30 [Global] Before request
2021/11/18 09:58:30 [Group] Before request
2021/11/18 09:58:30 [Group] After request
2021/11/18 09:58:30 [Global] After request

可以看到不同层级的 middleware 会按照后进先出(栈)的顺序调用

Custom Recovery behavior

// gin-note/custom-recovery/main.go
package main

import (
	"fmt"
	"net/http"

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

func main() {
	router := gin.New()
	router.Use(gin.Logger())
	router.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
		if err, ok := recovered.(string); ok {
			c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
		}
		c.AbortWithStatus(http.StatusInternalServerError)
	}))

	router.GET("/panic", func(c *gin.Context) {
		panic("foo")
	})

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

	router.Run(":9090")
}

Run and test:

$ curl -X GET "http://127.0.0.1:9090/"       
ohai%                                                                 $ curl -X GET "http://127.0.0.1:9090/panic"  
error: foo

Custom Log Format

// gin-note/custom-log-format/main.go
package main

import (
	"fmt"
	"net/http"
	"time"

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

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

	router.Use(gin.LoggerWithFormatter(func(params gin.LogFormatterParams) string {
		return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\"%s \n",
			params.ClientIP,
			params.TimeStamp.Format(time.RFC1123),
			params.Method,
			params.Path,
			params.Request.Proto,
			params.StatusCode,
			params.Latency,
			params.Request.UserAgent(),
			params.ErrorMessage)
	}))
	router.Use(gin.Recovery())

	router.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
	})

	router.Run(":9090")
}

Run and test:

$ curl -X GET "http://127.0.0.1:9090/ping" 
pong

log:

127.0.0.1 - [Thu, 18 Nov 2021 13:24:09 CST] "GET /ping HTTP/1.1 200 65.781µs "curl/7.79.1" 

How to write log file

package main

import (
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"

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

type Login struct {
	User     string `form:"user" json:"user" xml:"user" binding:"required"`
	Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

func main() {
	f, err := createLogFile("./log/gin.log")
	if err != nil {
		log.Fatal("create log file failed:", err)
	}
	defer f.Close()
	gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
	router := gin.Default()
	router.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func createLogFile(path string) (*os.File, error) {
	dir := filepath.Dir(path)
	log.Println("dir is: ", dir)
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		err = os.MkdirAll(path, 0755)
		if err != nil {
			log.Fatal("create dir failed:", err)
		}
	}
	return os.Create(path)
}
  • gin.DefaultWriter = io.MultiWriter(f, os.Stdout): write the logs to file and console at the same time

Run and test:

$ curl -X GET "http://0.0.0.0:9090/ping"
pong

Controlling Log output coloring

By default, logs output on console should be colorized depending on the detected TTY.

Never colorize logs:

// gin-note/colorize-log/main.go
package main

import (
	"net/http"

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

func main() {
	gin.DisableConsoleColor()
	router := gin.Default()
	router.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
	})

	router.Run(":9090")
}

Always colorize logs:

// gin-note/colorize-log/main.go
package main

import (
	"net/http"

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

func main() {
	gin.ForceConsoleColor()
	router := gin.Default()
	router.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
	})

	router.Run(":9090")
}

4.10 Model binding and validation

To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).

Gin use go-playground/validator/v10open in new window for validation. Check the full docs on tags usage hereopen in new window.

Also, Gin provides two sets of methods for binding:

  • Type - Must bind
    • Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML, BindHeader
    • Behavior - These methods use MustBindWith under the hood. If there is a binding error, the request is aborted with c.AbortWithError(400,err).SetType(ErrorTypeBind). This sets the response status code to 400 and the Content-Type header is set to text/plain;charset=utf-8. Note that if you try to set the response code after this, it will result in a warning [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422. If you wish to have greater control over the behavior, consider using the ShouldBind equivalent method.
  • Type - Should bind
    • Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML, ShouldBindHeader
    • Behavior - These methods use ShouldBindWith under the hood. If there is a binding error, the error is returne and it is the developer's responsibilty to handle the request and error appropriately.

When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use MustBindWith or ShouldBindWith.

You can also specify the specific fields are required. If a field is decorated with binding:"required" and has a empty value when binding, an error will be returned.

// gin-note/bind-and-validation/main.go
package main

import (
	"log"
	"net/http"

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

type Login struct {
	User     string `form:"user" json:"user" xml:"user" binding:"required"`
	Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

func main() {
	router := gin.Default()
	router.POST("/loginJSON", loginJSON)
	router.POST("/loginXML", loginXML)
	router.POST("/loginForm", loginForm)
	log.Println("Listen and serve on 0.0.0.0:8080")
	router.Run(":9090")
}

func loginJSON(c *gin.Context) {
	var json Login
	if err := c.ShouldBindJSON(&json); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err.Error(),
		})
		return
	}
	if json.User != "menu" || json.Password != "123" {
		c.JSON(http.StatusUnauthorized, gin.H{
			"status": "unauthorized",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"status": "you are logged in",
	})
}

func loginXML(c *gin.Context) {
	var xml Login
	if err := c.ShouldBindXML(&xml); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err.Error(),
		})
		return
	}
	if xml.User != "menu" || xml.Password != "123" {
		c.JSON(http.StatusUnauthorized, gin.H{
			"status": "unauthorized",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"status": "you are logged in",
	})
}

func loginForm(c *gin.Context) {
	var form Login
	if err := c.ShouldBind(&form); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err.Error(),
		})
		return
	}
	if form.User != "menu" || form.Password != "123" {
		c.JSON(http.StatusUnauthorized, gin.H{
			"status": "unauthorized",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"status": "you are logged in",
	})
}

Run and test:

$ curl -X POST "http://0.0.0.0:9090/loginJSON" \
-d '{"user":"menu","password":"123"}'
{"status":"you are logged in"}
$ curl -X POST "http://0.0.0.0:9090/loginXML" \ 
-d '<?xml version="1.0" encode="UTF-8"?><root><user>menu</user><password>123</password></root>'
{"status":"you are logged in"}
$  curl -X POST "http://0.0.0.0:9090/loginForm" \
-d 'user=menu&password=123'
{"status":"you are logged in"}

tips:

对于 form 数据使用了 ShouldBind

JSON 和 XML 类型的数据有对应的方法 ShouldBindJSONShouldBindXML,那是对于 FORM 数据使用的是 ShouldBind

ShouldBind的描述是:

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.

可以看到 ShouldBind 可以自动根据数据类型选择不同额绑定方式,这个自动绑定是如何实现的呢~

这里看下源码, ShouldBind:

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

binding.Default:

func Default(method, contentType string) Binding {
	if method == http.MethodGet {
		return Form
	}

	switch contentType {
	case MIMEJSON:
		return JSON
	case MIMEXML, MIMEXML2:
		return XML
	case MIMEPROTOBUF:
		return ProtoBuf
	case MIMEMSGPACK, MIMEMSGPACK2:
		return MsgPack
	case MIMEYAML:
		return YAML
	case MIMEMultipartPOSTForm:
		return FormMultipart
	default: // case MIMEPOSTForm:
		return Form
	}
}
const (
	MIMEJSON              = "application/json"
	MIMEHTML              = "text/html"
	MIMEXML               = "application/xml"
	MIMEXML2              = "text/xml"
	MIMEPlain             = "text/plain"
	MIMEPOSTForm          = "application/x-www-form-urlencoded"
	MIMEMultipartPOSTForm = "multipart/form-data"
	MIMEPROTOBUF          = "application/x-protobuf"
	MIMEMSGPACK           = "application/x-msgpack"
	MIMEMSGPACK2          = "application/msgpack"
	MIMEYAML              = "application/x-yaml"
)

这里会根据请求头中的 Content-Type 来决定绑定数据的方式,前文也提到了ShouldBind类的方法底层均会调用 ShouldBindWith, ShouldBindJSONShouldBindXML 相较于 ShouldBind 来说只是将绑定的类型固定下来而已

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

ShouldBindXXX 只是 ShouldBindWith的简短写法,这里没有提供 ShouldBindForm

Skip validate

When running the above example using the above the curl command, it returns error. Because the example use binding:"required" for Password. If using binding:"-" for Password, then it will not return error when running the above example again.

Custom Validators

It is also possible to register custom validators.

// gin-note/custom-validator/main.go
package main

import (
	"log"
	"net/http"
	"time"

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

	"github.com/go-playground/validator/v10"

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

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

var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
	date, ok := fl.Field().Interface().(time.Time)
	if ok {
		today := time.Now()
		if today.After(date) {
			return false
		}
	}
	return true
}

func main() {
	router := gin.Default()
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("bookableDate", bookableDate)
	}
	router.GET("/bookable", getBookable)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

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

  • gtfield=CheckIn: 当前 field (Checkout) 需要比 CheckIn(此处大小写敏感) 大
  • time_format: 指定时间格式,若不符合将会报错

Run and test:

$ curl "http:/127.0.0.1:9090/bookable?checkin=2030-06-20&checkout=2030-06-30"
{"message":"Booking dates are valid"}
$ curl "http:/127.0.0.1:9090/bookable?checkin=2000-06-20&checkout=2030-06-30"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookableDate' tag"}
$ curl "http:/127.0.0.1:9090/bookable?checkin=2030-06-20&checkout=2011-06-30"
{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
$ curl -G "http:/127.0.0.1:9090/bookable" --data-urlencode "checkin=2030-06-20&checkout=2030-06-30 11:20:11"
{"error":"parsing time \"2030-06-20\u0026checkout=2030-06-30 11:20:11\": extra text: \"\u0026checkout=2030-06-30 11:20:11\""}

注意: -G:使用 GET 发送数据,加上 --data-urlencode, 将参数进行编码后以 Query String 的形式发送,2030-06-30 11:20:11中含有空格,若不进行编码将无法识别

tips:

使用 ShouldBindWith(&b,binding.Query) ,结构体的字段标签需要使用form才可以绑定,可以看下源码:

ShouldBindWith:

func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
   return b.Bind(c.Request, obj)
}

Binding一个接口,其Bind方法定义了绑定数据行为:

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

binding.Query是实现了Binding接口:

func (queryBinding) Bind(req *http.Request, obj interface{}) error {
	values := req.URL.Query()
	if err := mapForm(obj, values); err != nil {
		return err
	}
	return validate(obj)
}

接着追踪代码:

func mapForm(ptr interface{}, form map[string][]string) error {
	return mapFormByTag(ptr, form, "form")
}

func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
	// Check if ptr is a map
	ptrVal := reflect.ValueOf(ptr)
	var pointed interface{}
	if ptrVal.Kind() == reflect.Ptr {
		ptrVal = ptrVal.Elem()
		pointed = ptrVal.Interface()
	}
	if ptrVal.Kind() == reflect.Map &&
		ptrVal.Type().Key().Kind() == reflect.String {
		if pointed != nil {
			ptr = pointed
		}
		return setFormMap(ptr, form)
	}

	return mappingByPtr(ptr, formSource(form), tag)
}

func mappingByPtr(ptr interface{}, setter setter, tag string) error {
	_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
	return err
}

可以看到 Query 实际上使用了 form 标签进行数据的绑定,接着看mapping函数:

func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
	if field.Tag.Get(tag) == "-" { // just ignoring this field
		return false, nil
	}

	var vKind = value.Kind()

	if vKind == reflect.Ptr {
		var isNew bool
		vPtr := value
		if value.IsNil() {
			isNew = true
			vPtr = reflect.New(value.Type().Elem())
		}
		isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
		if err != nil {
			return false, err
		}
		if isNew && isSetted {
			value.Set(vPtr)
		}
		return isSetted, nil
	}

	if vKind != reflect.Struct || !field.Anonymous {
		ok, err := tryToSetValue(value, field, setter, tag)
		if err != nil {
			return false, err
		}
		if ok {
			return true, nil
		}
	}

	if vKind == reflect.Struct {
		tValue := value.Type()

		var isSetted bool
		for i := 0; i < value.NumField(); i++ {
			sf := tValue.Field(i)
			if sf.PkgPath != "" && !sf.Anonymous { // unexported
				continue
			}
			ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
			if err != nil {
				return false, err
			}
			isSetted = isSetted || ok
		}
		return isSetted, nil
	}
	return false, nil
}

利用了反射机制,将 Query String 的参数绑定到结构体中,会忽略掉标签为form:"-"的字段

Struct Level Validation

package main

import (
	"log"
	"net/http"

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

	"github.com/go-playground/validator/v10"

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

type User struct {
	Firstname string `json:"firstname"`
	Lastname  string `json:"lastname"`
	Email     string `binding:"required,email"`
}

func UserStructLevelValidation(sl validator.StructLevel) {
	user := sl.Current().Interface().(User)
	if len(user.Firstname) == 0 && len(user.Lastname) == 0 {
		sl.ReportError(user.Firstname, "Firstname", "Firstname", "FirstnameOrLastname", "")
		sl.ReportError(user.Lastname, "Lastname", "Lastname", "FirstnameOrLastname", "")
	}

}

func main() {
	router := gin.Default()
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterStructValidation(UserStructLevelValidation, User{})
	}
	router.POST("/user", validateUser)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func validateUser(c *gin.Context) {
	var user User
	if err := c.ShouldBindJSON(&user); err == nil {
		c.JSON(http.StatusOK, gin.H{
			"message": "User validation successful",
		})
	} else {
		c.JSON(http.StatusBadRequest, gin.H{
			"message": "User validation failed",
			"error":   err.Error(),
		})
	}
}

Run and test:

$ curl -X POST "http://127.0.0.1:9090/user" -d '{"firstname":"A","lastname":"B","email":"[email protected]"}'
{"message":"User validation successful"}
$ curl -X POST "http://127.0.0.1:9090/user" -d '{"email":"[email protected]"}' 
{"error":"Key: 'User.Firstname' Error:Field validation for 'Firstname' failed on the 'FirstnameOrLastname' tag\nKey: 'User.Lastname' Error:Field validation for 'Lastname' failed on the 'FirstnameOrLastname' tag","message":"User validation failed"}

Only Bind Query String

ShouldBindQuery function only binds the query params and not the post data.

package main

import (
	"log"
	"net/http"

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

type Person struct {
	Name    string `form:"name"`
	Address string `form:"address"`
}

func main() {
	router := gin.Default()
	router.Any("/test", startPage)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func startPage(c *gin.Context) {
	var p Person
	if c.ShouldBindQuery(&p) == nil {
		log.Println("===== Only Bind Query String =====")
		log.Println(p.Name)
		log.Println(p.Address)
	}
	c.String(http.StatusOK, "Success")

}

Run and test:

$ curl -X GET "http://0.0.0.0:9090/test?name=kesa&address=xyz"
Success
$ curl -X POST "http://0.0.0.0:9090/test?name=kesa&address=xyz" -d "name=ingnore&address=ignore"
Success

log:

[GIN-debug] Listening and serving HTTP on :9090
2021/11/22 02:29:29 ===== Only Bind Query String =====
2021/11/22 02:29:29 kesa
2021/11/22 02:29:29 xyz
[GIN] 2021/11/22 - 02:29:29 | 200 |     115.852µs |       127.0.0.1 | GET      "/test?name=kesa&address=xyz"
2021/11/22 02:30:19 ===== Only Bind Query String =====
2021/11/22 02:30:19 kesa
2021/11/22 02:30:19 xyz
[GIN] 2021/11/22 - 02:30:19 | 200 |      40.661µs |       127.0.0.1 | POST     "/test?name=kesa&address=xyz"

可以看到仅绑定了 query string 数据

tips:

ShouldBindQueryShouldBindWith(obj,binding.Query)的简写,binding.Query实现了Binding接口,其Bind方法:

func (queryBinding) Bind(req *http.Request, obj interface{}) error {
	values := req.URL.Query()
	if err := mapForm(obj, values); err != nil {
		return err
	}
	return validate(obj)
}

可以看到,数据来源使用了req.URL.Query获取,即仅绑定了 Query String 的数据

Bind Query String or Post Data

package main

import (
	"log"
	"net/http"
	"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:"8"`
	CreateTime time.Time `form:"createdTime" time_format:"unixNano"`
	UnixTime   time.Time `form:"unixTime" time_format:"unix"`
}

func main() {
	router := gin.Default()
	router.POST("/test", startPage)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func startPage(c *gin.Context) {
	var p Person
	if err := c.ShouldBind(&p); err == nil {
		log.Println(p.Name)
		log.Println(p.Address)
		log.Println(p.Birthday)
		log.Println(p.CreateTime)
		log.Println(p.UnixTime)
	} else {
		c.String(http.StatusOK, err.Error())
		return
	}
	c.String(http.StatusOK, "Success")

}

Run and test:

# 只有 query string
$ curl -X POST "http://0.0.0.0:9090/test?name=kesa&address=xyz&birthday=2000-02-12&createdTime=1562400033000000123&unixTime=1562400033" 
Success
# 同时有 query string 和 form data
curl -X POST "http://0.0.0.0:9090/test?name=kesa&address=xyz&birthday=2000-02-12&createdTime=1562400033000000123&unixTime=1562400033" -d "name=change_name&address=change_addr"
Success

log:

# 只有 query string
2021/11/22 09:51:26 kesa
2021/11/22 09:51:26 xyz
2021/11/22 09:51:26 2000-02-12 00:00:00 +0800 CST
2021/11/22 09:51:26 2019-07-06 16:00:33.000000123 +0800 CST
2021/11/22 09:51:26 2019-07-06 16:00:33 +0800 CST
# 同时有 query string 和 form data
2021/11/22 09:53:50 change_name
2021/11/22 09:53:50 change_addr
2021/11/22 09:53:50 2000-02-12 00:00:00 +0800 CST
2021/11/22 09:53:50 2019-07-06 16:00:33.000000123 +0800 CST
2021/11/22 09:53:50 2019-07-06 16:00:33 +0800 CST

如果设置Content-Type: application/json,再次发送请求:

curl -X POST "http://0.0.0.0:9090/test?name=kesa&address=xyz&birthday=2000-02-12&createdTime=1562400033000000123&unixTime=1562400033" -d '{"name":"dreamjz"}' -H 'Content-Type: application/json'
Success

log:

2021/11/22 09:57:32 dreamjz
2021/11/22 09:57:32 
2021/11/22 09:57:32 0001-01-01 00:00:00 +0000 UTC
2021/11/22 09:57:32 0001-01-01 00:00:00 +0000 UTC
2021/11/22 09:57:32 0001-01-01 00:00:00 +0000 UTC

可以看到这里只会解析 json 的数据

tips

为什么?

这里看下源码,之前已经看到 ShouldBind 会根据 Content-Type 来选择解析方式

当只有 query string 或 form data 时,会使用 formBinding 来进行数据绑定

Bind 方法如下:

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

ParseForm:

func (r *Request) ParseForm() error {
	var err error
	if r.PostForm == nil {
		if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
			r.PostForm, err = parsePostForm(r)
		}
		if r.PostForm == nil {
			r.PostForm = make(url.Values)
		}
	}
	if r.Form == nil {
		if len(r.PostForm) > 0 {
			r.Form = make(url.Values)
			copyValues(r.Form, r.PostForm)
		}
		var newValues url.Values
		if r.URL != nil {
			var e error
			newValues, e = url.ParseQuery(r.URL.RawQuery)
			if err == nil {
				err = e
			}
		}
		if newValues == nil {
			newValues = make(url.Values)
		}
		if r.Form == nil {
			r.Form = newValues
		} else {
			copyValues(r.Form, newValues)
		}
	}
	return err

从这里可以看到,ParseForm不仅会解析 form data ,同时也会解析 query string

Content-Typeapplication/json时,会使用jsonBinding 来解析

Bind方法如下:

func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
	if req == nil || req.Body == nil {
		return fmt.Errorf("invalid request")
	}
	return decodeJSON(req.Body, obj)
}

可以看到,当 Content-Type为 json 时,此时只会解析 request body 的内容

Bind Uri

package main

import (
	"log"
	"net/http"

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

type Person struct {
	ID   string `uri:"id" binding:"required,uuid"`
	Name string `uri:"name" binding:"required"`
}

func main() {
	router := gin.Default()
	router.GET("/:name/:id", bindUri)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func bindUri(c *gin.Context) {
	var person Person
	if err := c.ShouldBindUri(&person); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"error": err.Error(),
		})
		return
	}
	c.JSON(http.StatusOK, person)
}

Run and test:

$ curl http://127.0.0.1:9090/kesa/987fbc97-4bed-5078-9f07-9141ba07c9f3
{"ID":"987fbc97-4bed-5078-9f07-9141ba07c9f3","Name":"kesa"}
$ curl http://127.0.0.1:9090/kesa/non-uuid
{"error":"Key: 'Person.ID' Error:Field validation for 'ID' failed on the 'uuid' tag"}

Bind Header

package main

import (
	"log"
	"net/http"

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

type testHeader struct {
	Rate   int    `header:"Rate"`
	Domain string `header:"Domain"`
}

func main() {
	router := gin.Default()
	router.GET("/bindHeader", bindHeader)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func bindHeader(c *gin.Context) {
	var h testHeader
	if err := c.ShouldBindHeader(&h); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"error": err.Error(),
		})
		return
	}
	c.JSON(http.StatusOK, h)
}

Run and test

$ curl -H "Rate:300" -H "Domain:music" "http://0.0.0.0:9090/bindHeader"
{"Rate":300,"Domain":"music"}

Bind HTML checkboxes

package main

import (
	"log"
	"net/http"

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

type myForm struct {
	Colors []string `form:"colors[]"`
}

func main() {
	router := gin.Default()
	router.POST("/bindCheckboxes", bindCheckboxes)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func bindCheckboxes(c *gin.Context) {
	var f myForm
	if err := c.ShouldBind(&f); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"error": err.Error(),
		})
		return
	}
	c.JSON(http.StatusOK, f)
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MyForm</title>
</head>
<body>
<form action="http://localhost:9090/bindCheckboxes" method="POST">
    <p>Check some colors</p>
    <label for="red">Red</label>
    <input type="checkbox" name="colors[]" value="red" id="red">
    <label for="green">Green</label>
    <input type="checkbox" name="colors[]" value="green" id="green">
    <label for="blue">Blue</label>
    <input type="checkbox" name="colors[]" value="blue" id="blue">
    <input type="submit">
</form>
</body>
</html>

实际上表单发送的是 form data ,按照之前的 bind form data 的方式即可

Multipart/Urlencoded binding

package main

import (
	"log"
	"mime/multipart"
	"net/http"

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

type ProfileForm struct {
	Name   string                `form:"name" binding:"required"`
	Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
}

func main() {
	router := gin.Default()
	router.POST("/profile", bindProfile)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func bindProfile(c *gin.Context) {
	var form ProfileForm
	if err := c.ShouldBind(&form); err != nil {
		c.String(http.StatusBadRequest, "bad request:", err.Error())
		return
	}
	err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
	if err != nil {
		c.String(http.StatusInternalServerError, "save file error:", err.Error())
		return
	}
	c.String(http.StatusOK, "success")
}

Run and test:

$ curl -X POST -F "name=kesa" -F "[email protected]" "http://0.0.0.0:9090/profile"
success

4.11 XML, JSON, YAML, and ProtoBuf rendering

package main

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin/testdata/protoexample"

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

func main() {
	router := gin.Default()
	router.GET("/someJSON", someJSON)
	router.GET("/moreJSON", moreJSON)
	router.GET("/someXML", someXML)
	router.GET("/someYAML", someYAML)
	router.GET("/someProtoBuf", someProtoBuf)
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func someJSON(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "hey",
		"status":  http.StatusOK,
	})
}

func moreJSON(c *gin.Context) {
	var msg struct {
		Name    string `json:"user"`
		Message string
		Number  int
	}
	msg.Name = "Kesa"
	msg.Message = "hey"
	msg.Number = 123
	c.JSON(http.StatusOK, msg)
}

func someXML(c *gin.Context) {
	c.XML(http.StatusOK, gin.H{
		"message": "hey",
		"status":  http.StatusOK,
	})
}

func someYAML(c *gin.Context) {
	c.YAML(http.StatusOK, gin.H{
		"message": "hey",
		"status":  http.StatusOK,
	})
}

func someProtoBuf(c *gin.Context) {
	reps := []int64{int64(1), int64(2)}
	label := "test"
	data := &protoexample.Test{
		Label: &label,
		Reps:  reps,
	}
	c.ProtoBuf(http.StatusOK, data)
}

  • gin.H: a shortcut for map[string]interface{}
  • protoexample.Test: The specific definition of protobuf is written in the testdata/protobufeample file
  • c.ProtoBuf: Note that data becomes binary data in the response, will output protoexample.Test protobuf serialized data

Run and test:

$ curl "http://0.0.0.0:9090/someJSON"
{"message":"hey","status":200}
$ curl "http://0.0.0.0:9090/moreJSON"
{"user":"Kesa","Message":"hey","Number":123}
$  curl "http://0.0.0.0:9090/someXML"
<map><message>hey</message><status>200</status></map>%
$ curl "http://0.0.0.0:9090/someYAML"
message: hey
status: 200
$ curl "http://0.0.0.0:9090/someProtoBuf"
test▒▒

tips:

What are protocol buffers

Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

SecureJSON

Using SecureJSON to prevent json hijacking. Default prepends "while(1)," to repsonse body if the given struct is array values.

package main

import (
	"log"
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/secureJSON", func(c *gin.Context) {
		names := []string{"lena", "kesa", "foo"}
		c.SecureJSON(http.StatusOK, names)
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

Run and test

$ curl "http://0.0.0.0:9090/secureJSON"                               
while(1);["lena","kesa","foo"]

tips

json hijacking

json劫持(jsonhijacking)漏洞其实是一个跨域数据窃取漏洞,它通过诱导用户点击恶意文件,重写Array()的构造函数的方法,将敏感的json数据发送攻击者,从而造成敏感信息泄露

现在这些漏洞大多已经被修复了,参见Is JSON Hijacking still an issue in modern browsers?open in new window

JSONP

Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists.

package main

import (
	"log"
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/JSONP", func(c *gin.Context) {
		c.JSONP(http.StatusOK, gin.H{
			"foo": "bar",
		})
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

Run and test

$ curl "http://0.0.0.0:9090/JSONP?callback=x"
x({"foo":"bar"});

tips:

JSONP, or JSON-P (JSON with Padding), is an historical JavaScriptopen in new window technique for requesting data by loading a <script> element,[1]open in new window which is an element intended to load ordinary JavaScriptz

AsciiJSON

Using AsciiJSON to Generate ASCII-Only JSON with escaped non-ASCII characters.

package main

import (
	"log"
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/ASCII-JSON", func(c *gin.Context) {
		c.JSONP(http.StatusOK, gin.H{
			"lang": "GO语言",
			"tag":  "<br>",
		})
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

Run and test

$ curl "http://0.0.0.0:9090/ASCII-JSON"
{"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}

Pure JSON

Normally, JSON replaces special HTML characters with their unicode entities, e.g. < becomes \u003c. If you want to encode such characters literally, you can use PureJSON instead. This feature is unavailable in Go 1.6 and lower.

package main

import (
	"log"
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/json", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"html": "<h1>Hello</h1>",
		})
	})
	router.GET("/pureJson", func(c *gin.Context) {
		c.PureJSON(http.StatusOK, gin.H{
			"html": "<h1>Hello</h1>",
		})
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

Run and test

$ curl "http://0.0.0.0:9090/json"      
{"html":"\u003ch1\u003eHello\u003c/h1\u003e"}
$ curl "http://0.0.0.0:9090/pureJson"
{"html":"<h1>Hello</h1>"}

4.12 Serving static files

package main

import (
	"log"
	"net/http"

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

func main() {
	router := gin.Default()
	router.Static("/assets", "./serving-static-files/assets")
	router.StaticFS("/more_static", http.Dir("./serving-static-files/more_static"))
	router.StaticFS("/more_static2", gin.Dir("./serving-static-files/more_static", false))
	router.StaticFile("/favicon.ico", "./serving-static-files/resources/favicon.ico")
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

  • router.Static("/assets", "./serving-static-files/assets"): 可以访问 ./serving-static-files/assets目录下的文件(/assets/xxx/filename),但是不可以直接访问/assets/
  • outer.StaticFS("/more_static", http.Dir("./serving-static-files/more_static")) : 和static类似,但是可访问/more_static/,列出文件列表
  • router.StaticFS("/more_static2", gin.Dir("./serving-static-files/more_static", false)): 指定gin.Dir(root,listDirectory),可以控制是否可以列出目录
  • router.StaticFile:可以访问单个文件

Run and test

$ curl "http://0.0.0.0:9090/more_static/"
<pre>
<a href="Pac%20Man.ico">Pac Man.ico</a>
</pre>
$ curl "http://0.0.0.0:9090/more_static2/"
<pre>
</pre>

4.13 Serving data from file

package main

import (
	"log"
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/favicon", func(c *gin.Context) {
		c.File("./serving-data-from-file/resources/favicon.ico")
	})
	fs := http.FileSystem(http.Dir("././serving-data-from-file/assets"))
	router.GET("/pacman", func(c *gin.Context) {
		c.FileFromFS("Pac Man.ico", fs)
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

4.14 Serving data from reader

package main

import (
	"log"
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/someDataFromReader", func(c *gin.Context) {
		response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")
		if err != nil || response.StatusCode != http.StatusOK {
			c.Status(http.StatusServiceUnavailable)
		}
		reader := response.Body
		defer reader.Close()
		contentLength := response.ContentLength
		contentType := response.Header.Get("Content-Type")
		extraHeaders := map[string]string{
			"Content-Disposition": `attachment;filename="gopher.png"`,
		}
		c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

4.15 HTML rendering

Using LoadHTMLGlob() or LoadHTMLFiles()

package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.LoadHTMLGlob("./html-rendering/templates/*")
	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"title": "Main Website",
		})
	})
	router.Run(":9090")
}

Run and test

$ curl "http://0.0.0.0:9090/index"
<html>
        <h1>
                Main Website
        </h1>
</html>

Using templates with same name in different directories

package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.LoadHTMLGlob("./templates-samename-diff-dir/templates/**/*")
    	//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")

	router.GET("/posts/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
			"title": "Posts",
		})
	})
	router.GET("/users/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
			"title": "Users",
		})
	})
	router.Run(":9090")
}
  • templates/**/*: 加载template及其第一级子目录下的所有模板,若使用templates/*只能匹配templates下的文件,无法加载子目录
  • LoadHTMLFiles: 可以指定模板文件

Run and test

$ curl "http://0.0.0.0:9090/posts/index"
<html><h1>
        Posts
</h1>
<p>Using posts/index.tmpl</p>
</html>
$ curl "http://0.0.0.0:9090/users/index"  
<html><h1>
        Users
</h1>
<p>Using users/index.tmpl</p>
</html>

Custom Template renderer

You can also use your own html template render

import "html/template"

func main() {
	router := gin.Default()
	html := template.Must(template.ParseFiles("file1", "file2"))
	router.SetHTMLTemplate(html)
	router.Run(":8080")
}

Custom Delimiters

Custom Template Funcs

import (
    "fmt"
    "html/template"
    "net/http"
    "time"

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

func formatAsDate(t time.Time) string {
    year, month, day := t.Date()
    return fmt.Sprintf("%d%02d/%02d", year, month, day)
}

func main() {
    router := gin.Default()
    router.Delims("{[{", "}]}")
    router.SetFuncMap(template.FuncMap{
        "formatAsDate": formatAsDate,
    })
    router.LoadHTMLFiles("./testdata/template/raw.tmpl")

    router.GET("/raw", func(c *gin.Context) {
        c.HTML(http.StatusOK, "raw.tmpl", gin.H{
            "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
        })
    })

    router.Run(":8080")
}

raw.tmpl

Date: {[{.now | formatAsDate}]}

Result:

Date: 2017/07/01

Multitemplate

Gin allow by default use only one html.Template. Check a multitemplate renderopen in new window for using features like go 1.6 block template.

4.16 Redirects

Issuing a HTTP redirect is easy. Both internal and external locations are supported.

package main

import (
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/bing", func(c *gin.Context) {
		c.Redirect(http.StatusMovedPermanently, "https://www.bing.com")
	})
	router.GET("/welcome", func(c *gin.Context) {
		c.String(http.StatusOK, "Welcome")
	})
	router.POST("/login", func(c *gin.Context) {
		c.Redirect(http.StatusFound, "/welcome")
	})
	router.GET("/test", func(c *gin.Context) {
		c.Request.URL.Path = "/test2"
		router.HandleContext(c)
	})
	router.GET("/test2", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"hello": "world",
		})
	})
	router.Run(":9090")
}

Run and test

$ curl "http://0.0.0.0:9090/bing"       
<a href="https://www.bing.com">Moved Permanently</a>.
$ curl -X POST -v "http://0.0.0.0:9090/login"
* Uses proxy env variable http_proxy == 'http://127.0.0.1:8889'
*   Trying 127.0.0.1:8889...
* Connected to 127.0.0.1 (127.0.0.1) port 8889 (#0)
> POST http://0.0.0.0:9090/login HTTP/1.1
> Host: 0.0.0.0:9090
> User-Agent: curl/7.80.0
> Accept: */*
> Proxy-Connection: Keep-Alive
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Content-Length: 0
< Connection: keep-alive
< Date: Mon, 22 Nov 2021 10:55:34 GMT
< Keep-Alive: timeout=4
< Location: /welcome
< Proxy-Connection: keep-alive
< 
* Connection #0 to host 127.0.0.1 left intact
$  curl "http://0.0.0.0:9090/test"       
{"hello":"world"}

tips

重定向与转发

重定向是 客户端 行为,客户端会重新发起请求

转发是 服务端 行为,服务端将请求转发至其他路由进行处理

301 和 302

Status 301 means that the resource (page) is moved permanently to a new location. The client/browser should not attempt to request the original location but use the new location from now on.

Status 302 means that the resource is temporarily located somewhere else, and the client/browser should continue requesting the original url.

4.17 Custom Middleware

package main

import (
	"log"
	"time"

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

func main() {
	router := gin.New()
	router.Use(myLogger())
	router.GET("/test", func(c *gin.Context) {
		example := c.MustGet("example")
		log.Println(example)
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

func myLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()
		c.Set("example", "123456")
		c.Next()
		latency := time.Since(t)
		log.Println(latency)
		status := c.Writer.Status()
		log.Println(status)
	}
}

Using BasicAuth() middleware

package main

import (
	"log"
	"net/http"

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

var secrets = gin.H{
	"foo":    gin.H{"email": "[email protected]", "phone": "123"},
	"austin": gin.H{"email": "[email protected]", "phone": "456"},
	"lena":   gin.H{"email": "[email protected]", "phone": "789"},
}

func main() {
	router := gin.Default()
	authorized := router.Group("/admin", gin.BasicAuth(gin.Accounts{
		"foo":    "bar",
		"austin": "1234",
		"lena":   "hello2",
		"kim":    "4321",
	}))

	authorized.GET("/secrets", func(c *gin.Context) {
		user := c.MustGet(gin.AuthUserKey).(string)
		if secret, ok := secrets[user]; ok {
			c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
			return
		}
		c.JSON(http.StatusOK, gin.H{"user": user, "secret": "No Secret :( "})
	})
	log.Println("Listen and serve on 0.0.0.0:9090")
	router.Run(":9090")
}

Run and test

$ curl -u foo:bar "http://0.0.0.0:9090/admin/secrets"
{"secret":{"email":"[email protected]","phone":"123"},"user":"foo"}

Goroutines inside a middleware

When starting new Goroutines inside a middleware or handler, you SHOULD NOT use the original context inside it, you have to use a read-only copy.

package main

import (
	"log"
	"time"

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

func main() {
	router := gin.Default()
	router.GET("/long_async", func(c *gin.Context) {
		cCp := c.Copy()
		go func() {
			time.Sleep(3 * time.Second)
			log.Println("Done! in path " + cCp.Request.URL.Path)
		}()
	})
	router.GET("/long_sync", func(c *gin.Context) {
		time.Sleep(3 * time.Second)
		log.Println("Done! in path " + c.Request.URL.Path)
	})
	router.Run(":9090")
}
  • c.Copy: create a copy to be used inside the goroutine

4.18 Custome HTTP configuration

Use http.ListenAndServe() directly:

func main(){
    router := gin.Default()
    http.ListenAndServe(":9090",router)
}

or

func main(){
    router := gin.Default()
    s := &http.Server{
        Addr: ":9090",
        Handler: router,
        Readtimeout: 10 * time.Second,
        Writetimeout: 10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}

4.19 Support Let’s Encrypt

example for 1-line LetsEncrypt HTTPS servers:

package main

import (
	"log"

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

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

	// Ping handler
	r.GET("/ping", func(c *gin.Context) {
		c.String(200, "pong")
	})

	log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
}

Custom autocert manager

package main

import (
	"log"

	"github.com/gin-gonic/autotls"
	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/acme/autocert"
)

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

	// Ping handler
	r.GET("/ping", func(c *gin.Context) {
		c.String(200, "pong")
	})

	m := autocert.Manager{
		Prompt:     autocert.AcceptTOS,
		HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
		Cache:      autocert.DirCache("/var/www/.cache"),
	}

	log.Fatal(autotls.RunWithManager(r, &m))
}

4.20 Run multiple service using Gin

gin.Run() is blocking so you have to call them in separate goroutines if you want to accomplish that.

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
)

var (
	g errgroup.Group
)

func router01() http.Handler {
	router := gin.New()
	router.Use(gin.Recovery())
	router.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"code":    http.StatusOK,
			"message": "Welcome server 01",
		})
	})
	return router
}

func router02() http.Handler {
	router := gin.New()
	router.Use(gin.Recovery())
	router.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"code":    http.StatusOK,
			"message": "Welcome to server 02",
		})
	})
	return router
}

func main() {
	server01 := &http.Server{
		Addr:         ":9090",
		Handler:      router01(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
	server02 := &http.Server{
		Addr:         ":9091",
		Handler:      router02(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	g.Go(func() error {
		err := server01.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
		return err
	})

	g.Go(func() error {
		err := server02.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
		return err
	})

	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}

Run and test

$ curl "http://0.0.0.0:9090/"  
{"code":200,"message":"Welcome server 01"}
$ curl "http://0.0.0.0:9091/"
{"code":200,"message":"Welcome to server 02"}

4.21 Graceful shutdown or restart

There are a few approaches you can use to perform a graceful shutdown or restart. You can make use of third-party packages specifically build for that, or you can manually do the same with the functions and methods from the built-in packages.

Third-party packages

We can use fvbock/endlessopen in new window to replace the default ListenAndServe. Refer to issue #296open in new window for more details.

router := gin.Default()
router.GET("/", handler)
// [...]
endless.ListenAndServe(":4242", router)

Alternatives:

Manually

In case you are using Go 1.8 or a later version, you may not need to use those libraries. Consider using http.Server's built-in Shutdown()open in new window method for graceful shutdowns. The example below describes its usage, and we've got more examples using gin hereopen in new window.

// +build go1.8

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

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

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "Welcome Gin Server")
	})

	srv := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}

	// Initializing the server in a goroutine so that
	// it won't block the graceful shutdown handling below
	go func() {
		if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
			log.Printf("listen: %s\n", err)
		}
	}()

	// Wait for interrupt signal to gracefully shutdown the server with
	// a timeout of 5 seconds.
	quit := make(chan os.Signal)
	// kill (no param) default send syscall.SIGTERM
	// kill -2 is syscall.SIGINT
	// kill -9 is syscall.SIGKILL but can't be caught, so don't need to add it
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("Shutting down server...")

	// The context is used to inform the server it has 5 seconds to finish
	// the request it is currently handling
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server forced to shutdown:", err)
	}

	log.Println("Server exiting")
}

4.22 Build a single binary with templates

You can build a server into a single binary containing templates by using go-assetsopen in new window.

func main() {
	r := gin.New()

	t, err := loadTemplate()
	if err != nil {
		panic(err)
	}
	r.SetHTMLTemplate(t)

	r.GET("/", func(c *gin.Context) {
		c.HTML(http.StatusOK, "/html/index.tmpl",nil)
	})
	r.Run(":8080")
}

// loadTemplate loads templates embedded by go-assets-builder
func loadTemplate() (*template.Template, error) {
	t := template.New("")
	for name, file := range Assets.Files {
		defer file.Close()
		if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
			continue
		}
		h, err := ioutil.ReadAll(file)
		if err != nil {
			return nil, err
		}
		t, err = t.New(name).Parse(string(h))
		if err != nil {
			return nil, err
		}
	}
	return t, nil
}

See a complete example in the https://github.com/gin-gonic/examples/tree/master/assets-in-binary directory.

4.23 Bind form-data request with custom struct

package main

import (
	"net/http"

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

type StructA struct {
	FieldA string `form:"field_a"`
}

type StructB struct {
	NestedStruct StructA
	FieldB       string `form:"field_b"`
}

type StructC struct {
	NestedStructPointer *StructA
	FieldC              string `form:"field_c"`
}

type StructD struct {
	NestedAnonymousStruct struct {
		FieldX string `form:"field_x"`
	}
	FieldD string `form:"field_d"`
}

func main() {
	router := gin.Default()
	router.GET("/getB", GetDataB)
	router.GET("/getC", GetDataC)
	router.GET("/getD", GetDataD)
	router.Run(":9090")
}

func GetDataB(c *gin.Context) {
	var b StructB
	c.Bind(&b)
	c.JSON(http.StatusOK, gin.H{
		"a": b.NestedStruct,
		"b": b.FieldB,
	})
}

func GetDataC(c *gin.Context) {
	var cs StructC
	c.Bind(&cs)
	c.JSON(http.StatusOK, gin.H{
		"a": cs.NestedStructPointer,
		"c": cs.FieldC,
	})
}

func GetDataD(c *gin.Context) {
	var d StructD
	c.Bind(&d)
	c.JSON(http.StatusOK, gin.H{
		"x": d.NestedAnonymousStruct,
		"d": d.FieldD,
	})
}

4.24 Try to bind body into different structs

The normal methods for binding request body consumes c.Request.Body and they cannot be called multiple times.

package main

import (
	"log"
	"net/http"

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

type FormA struct {
	Foo string `json:"foo" binding:"required"`
}

type FormB struct {
	Bar string `json:"bar" binding:"required"`
}

func main() {
	router := gin.Default()
	router.POST("/test", func(c *gin.Context) {
		objA := FormA{}
		objB := FormB{}
		if errA := c.ShouldBind(&objA); errA == nil {
			c.String(http.StatusOK, "the body should be fromA")
		}
		if errB := c.ShouldBind(&objB); errB == nil {
			c.String(http.StatusOK, "the body should be fromA")
		} else {
			log.Println(errB)
		}
	})
	router.Run(":9090")
}

  • c.ShouldBind(&objA): This c.ShouldBind consumes c.Request.Body and it cannot be resued
  • c.ShouldBind(&objB): Always an error is occurred by this because c.Request.Body is EOF now

For this you can use c.ShouldBindBodyWith

router.POST("/test2", func(c *gin.Context) {
		objA := FormA{}
		objB := FormB{}
		if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
			c.String(http.StatusOK, "the body should be fromA")
		}
		if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
			c.String(http.StatusOK, "the body should be fromB")
		}
	})
  • c.ShouldBindBodyWith: stores body into the context before binding. This has a slight impact to performance, so you should not use this mehtod if you are enough to call binding at once
  • This feature is only needed for some formats -- JSON,XML,MsgPack,ProtoBuf. For other formats, Query,Form,FormPost,FormMultipart can be called by c.ShouldBind multiple times without any damage to performance

4.25 Bind form-data request with custom struct and custom tag

const (
	customerTag = "url"
	defaultMemory = 32 << 20
)

type customerBinding struct {}

func (customerBinding) Name() string {
	return "form"
}

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

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

// Now we can do this!!!
// FormA is a external type that we can't modify it's tag
type FormA struct {
	FieldA string `url:"field_a"`
}

func ListHandler(s *Service) func(ctx *gin.Context) {
	return func(ctx *gin.Context) {
		var urlBinding = customerBinding{}
		var opt FormA
		err := ctx.MustBindWith(&opt, urlBinding)
		if err != nil {
			...
		}
		...
	}
}

目前在 (2021-11-23)v1.7.6中暂时没有binding.MapFormWithTag方法,在master分支中已加入

4.26 http2 server push

http.Pusher is supported only go1.8+. See the golang blogopen in new window for detail information.

package main

import (
	"html/template"
	"log"

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

var html = template.Must(template.New("https").Parse(`
<html>
<head>
  <title>Https Test</title>
  <script src="/assets/app.js"></script>
</head>
<body>
  <h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))

func main() {
	r := gin.Default()
	r.Static("/assets", "./assets")
	r.SetHTMLTemplate(html)

	r.GET("/", func(c *gin.Context) {
		if pusher := c.Writer.Pusher(); pusher != nil {
			// use pusher.Push() to do server push
			if err := pusher.Push("/assets/app.js", nil); err != nil {
				log.Printf("Failed to push: %v", err)
			}
		}
		c.HTML(200, "https", gin.H{
			"status": "success",
		})
	})

	// Listen and Server in https://127.0.0.1:8080
	r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key")
}

4.27 Define format for the log of routes

The default log of routes is:

[GIN-debug] POST   /foo                      --> main.main.func1 (3 handlers)
[GIN-debug] GET    /bar                      --> main.main.func2 (3 handlers)
[GIN-debug] GET    /status                   --> main.main.func3 (3 handlers)

If you want to log this information in given format, then you can define this format with gin.DebugPrintRouteFunc. In the example below, wo log all routes with standard log package but you can use another log tools that suits of your needs.

package main

import (
	"log"
	"net/http"

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

func main() {
	router := gin.Default()
	gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
		log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
	}
	router.GET("/get", func(c *gin.Context) {
		c.String(http.StatusOK, "get")
	})
	router.POST("/post", func(c *gin.Context) {
		c.String(http.StatusOK, "post")
	})
	router.Run(":9090")
}
package main

import (
	"fmt"
	"net/http"

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

func main() {
	router := gin.Default()
	router.GET("/test", 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.Println("cookie value:", cookie)
		c.JSON(http.StatusOK, gin.H{
			"message": "cookie set",
		})
	})
	router.Run(":9090")
}

5. Don't trust all proxies

Gin lets you specify which headers to hold the real client IP (if any), as well as specifying which proxies (or direct clients) you trust to specify one of these headers.

Use function SetTrustedProxies() on your gin.Engine to specify network addresses or network CIDRs from where clients which their request headers related to client IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or IPv6 CIDRs.

Attention: Gin trust all proxies by default if you don't specify a trusted proxy using the function above, this is NOT safe. At the same time, if you don't use any proxy, you can disable this feature by using Engine.SetTrustedProxies(nil), then Context.ClientIP() will return the remote address directly to avoid some unnecessary computation.

import (
	"fmt"

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

func main() {

	router := gin.Default()
	router.SetTrustedProxies([]string{"192.168.1.2"})

	router.GET("/", func(c *gin.Context) {
		// If the client is 192.168.1.2, use the X-Forwarded-For
		// header to deduce the original client IP from the trust-
		// worthy parts of that header.
		// Otherwise, simply return the direct client IP
		fmt.Printf("ClientIP: %s\n", c.ClientIP())
	})
	router.Run()
}

Notice: If you are using a CDN service, you can set the Engine.TrustedPlatform to skip TrustedProxies check, it has a higher priority than TrustedProxies. Look at the example below:

import (
	"fmt"

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

func main() {

	router := gin.Default()
	// Use predefined header gin.PlatformXXX
	router.TrustedPlatform = gin.PlatformGoogleAppEngine
	// Or set your own trusted request header for another trusted proxy service
	// Don't set it to any suspect request header, it's unsafe
	router.TrustedPlatform = "X-CDN-IP"

	router.GET("/", func(c *gin.Context) {
		// If you set TrustedPlatform, ClientIP() will resolve the
		// corresponding header and return IP directly
		fmt.Printf("ClientIP: %s\n", c.ClientIP())
	})
	router.Run()
}

6. Testing

The net/http/httptest package is preferable way for HTTP testing.

package main

func setupRouter() *gin.Engine {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.String(200, "pong")
	})
	return r
}

func main() {
	r := setupRouter()
	r.Run(":8080")
}

Test for code example above:

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPingRoute(t *testing.T) {
	router := setupRouter()

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/ping", nil)
	router.ServeHTTP(w, req)

	assert.Equal(t, 200, w.Code)
	assert.Equal(t, "pong", w.Body.String())
}

Reference

  1. ginopen in new window github
  2. HTTP HEADopen in new window MDN Web Docs
  3. gin框架总结open in new window studygolang
  4. Postman Chrome: What is the difference between form-data, x-www-form-urlencoded and rawopen in new window stackoverflow
  5. Forms in HTML documentsopen in new window
  6. What are protocol buffers?open in new window
  7. HTTP redirect: 301 (permanent) vs. 302 (temporary)open in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2