Gin Web Framework
参考 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 httprouter.
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 repository(之后会单独学习这些示例)
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) middlewarerouter.Run
: By default it serves on :8080 unless a PORT environment variable was defined. You can userouter.Run(":9090")
for a hard coded port
tips:
HTTP HEAD
方法 请求资源的头部信息, 并且这些头部与 HTTP GET
方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源
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 matchuser/john/
and/user/john/send
c.FullPath
: for each matched requestContext
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 MDN and #1693
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
注意
这里特别强调不要信任任何用户定义的文件名,非常危险#1693
比如用户设定用户名为../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.Stdoutrouter.Use(gin.Recovery())
: Recovery middleware recovers from any panics and writes a 500(http code) if there was onerouter.GET("/req1", MyRouteMiddleware(), func(c *gin.Context){...})
: Per route middleware, you can add as many as you desiregrp.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/v10 for validation. Check the full docs on tags usage here.
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 withc.AbortWithError(400,err).SetType(ErrorTypeBind)
. This sets the response status code to 400 and theContent-Type
header is set totext/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 theShouldBind
equivalent method.
- Methods -
- 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.
- Methods -
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 类型的数据有对应的方法 ShouldBindJSON
和 ShouldBindXML
,那是对于 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
, ShouldBindJSON
和 ShouldBindXML
相较于 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:
ShouldBindQuery
是ShouldBindWith(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-Type
为application/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 filec.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?
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 JavaScript technique for requesting data by loading a
<script>
element,[1] 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 render 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/endless to replace the default ListenAndServe
. Refer to issue #296 for more details.
router := gin.Default()
router.GET("/", handler)
// [...]
endless.ListenAndServe(":4242", router)
Alternatives:
- manners: A polite Go HTTP server that shuts down gracefully.
- graceful: Graceful is a Go package enabling graceful shutdown of an http.Handler server.
- grace: Graceful restart & zero downtime deploy for Go servers.
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() method for graceful shutdowns. The example below describes its usage, and we've got more examples using gin here.
// +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-assets.
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 resuedc.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 byc.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 blog 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")
}
4.28 Set and get a cookie
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())
}