项目地址:https://github.com/gin-gonic/gin

安装

要安装Gin软件包,您需要先安装Go并设置Go工作区。

  1. 下载并安装

    1
    $ go get -u github.com/gin-gonic/gin
  2. 导入gin

    1
    import "github.com/gin-gonic/gin"
  3. (可选)导入 net/http。可以使用 http.StatusOK 之类的一些常量

    1
    import "net/http"

快速开始

1
2
# assume the following codes in example.go file
$ cat example.go
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

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

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
1
2
# run example.go and visit 0.0.0.0:8080/ping on browser
$ go run example.go

使用jsoniter

Gin使用 encoding/json 作为默认的json包,您可以通过 -tags 参数更改为 jsoniter

1
$ go build -tags=jsoniter .

API示例

使用 GET, POST, PUT, PATCH, DELETE 和 OPTIONS 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
// Disable Console Color
// gin.DisableConsoleColor()

// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()

router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)

// By default it serves on :8080 unless a
// PORT environment variable was defined.
router.Run()
// router.Run(":3000") for a hard coded port
}

获取 Path 中的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
router := gin.Default()

// This handler will match /user/john but will not match /user/ or /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})

// However, this one will match /user/john/ and also /user/john/send
// If no other routers match /user/john, it will redirect to /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})

router.Run(":8080")
}

获取 Query 中的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
router := gin.Default()

// Query string parameters are parsed using the existing underlying request object.
// The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
router.Run(":8080")
}

Multipart/Urlencoded 表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
router := gin.Default()

router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")

c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})
router.Run(":8080")
}

Query + Post表单 请求示例

1
2
3
4
POST /post?id=1234&page=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded

name=manu&message=this_is_great
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
router := gin.Default()

router.POST("/post", func(c *gin.Context) {

id := c.Query("id")
page := c.DefaultQuery("page", "0")
name := c.PostForm("name")
message := c.PostForm("message")

fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
})
router.Run(":8080")
}
1
id: 1234; page: 1; name: manu; message: this_is_great

Query 和 Post表单中的 Map 参数

1
2
3
4
POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded

names[first]=thinkerou&names[second]=tianou
1
2
3
4
5
6
7
8
9
10
11
12
func main() {
router := gin.Default()

router.POST("/post", func(c *gin.Context) {

ids := c.QueryMap("ids")
names := c.PostFormMap("names")

fmt.Printf("ids: %v; names: %v", ids, names)
})
router.Run(":8080")
}
1
ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]

上传文件

单文件上传

References issue #774 and detail example code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// single file
file, _ := c.FormFile("file")
log.Println(file.Filename)

// Upload the file to specific dst.
// c.SaveUploadedFile(file, dst)

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

How to curl:

1
2
3
curl -X POST http://localhost:8080/upload \
-F "file=@/Users/appleboy/test.zip" \
-H "Content-Type: multipart/form-data"

多个文件批量上传

查看详细示例 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["upload[]"]

for _, file := range files {
log.Println(file.Filename)

// Upload the file to specific dst.
// c.SaveUploadedFile(file, dst)
}
c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
})
router.Run(":8080")
}

How to curl:

1
2
3
4
curl -X POST http://localhost:8080/upload \
-F "upload[]=@/Users/appleboy/test1.zip" \
-F "upload[]=@/Users/appleboy/test2.zip" \
-H "Content-Type: multipart/form-data"

分组路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
router := gin.Default()

// Simple group: v1
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}

// Simple group: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}

router.Run(":8080")
}

初始化不包含中间件的默认 Gin

Use

1
r := gin.New()

instead of

1
2
// Default With the Logger and Recovery middleware already attached
r := gin.Default()

使用中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func main() {
// Creates a router without any middleware by default
r := gin.New()

// Global middleware
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
// By default gin.DefaultWriter = os.Stdout
r.Use(gin.Logger())

// Recovery middleware recovers from any panics and writes a 500 if there was one.
r.Use(gin.Recovery())

// Per route middleware, you can add as many as you desire.
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

// Authorization group
// authorized := r.Group("/", AuthRequired())
// exactly the same as:
authorized := r.Group("/")
// per group middleware! in this case we use the custom created
// AuthRequired() middleware just in the "authorized" group.
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)

// nested group
testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
}

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

输出日志文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
// Disable Console Color, you don't need console color when writing the logs to file.
gin.DisableConsoleColor()

// Logging to a file.
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)

// Use the following code if you need to write the logs to file and console at the same time.
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)

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

   router.Run(":8080")
}

实体对象的绑定和校验

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

Gin uses go-playground/validator.v8 for validation. Check the full docs on tags usage here.

Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set json:"fieldname".

Also, Gin provides two sets of methods for binding:

  • Type - Must bind
    • Methods - Bind, BindJSON, BindXML, BindQuery
    • 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
    • Behavior - These methods use ShouldBindWith under the hood. If there is a binding error, the error is returned and it is the developer’s responsibility 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 that specific fields are required. If a field is decorated with binding:"required" and has a empty value when binding, an error will be returned.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Binding from JSON
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()

// Example for binding JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(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 != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})

// Example for binding XML (
// <?xml version="1.0" encoding="UTF-8"?>
// <root>
// <user>user</user>
// <password>123</user>
// </root>)
router.POST("/loginXML", func(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 != "manu" || xml.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})

// Example for binding a HTML form (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// This will infer what binder to use depending on the content-type header.
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})

// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}

Sample request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ curl -v -X POST \
http://localhost:8080/loginJSON \
-H 'content-type: application/json' \
-d '{ "user": "manu" }'
> POST /loginJSON HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.51.0
> Accept: */*
> content-type: application/json
> Content-Length: 18
>
* upload completely sent off: 18 out of 18 bytes
< HTTP/1.1 400 Bad Request
< Content-Type: application/json; charset=utf-8
< Date: Fri, 04 Aug 2017 03:51:31 GMT
< Content-Length: 100
<
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}

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 use binding:"-" for Password, then it will not return error when running the above example again.

自定义检验方法

It is also possible to register custom validators. See the example code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

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

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

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

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

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

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

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

func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
1
2
3
4
5
$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
{"message":"Booking dates are valid!"}

$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}

Struct level validations can also be registered this way.
See the struct-lvl-validation example to learn more.

仅绑定 Query

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"log"

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

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

func main() {
route := gin.Default()
route.Any("/testing", startPage)
route.Run(":8085")
}

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

绑定 Query 或者 Post 数据

See the detail information.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"log"
"time"

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

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

func main() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}

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

c.String(200, "Success")
}

Test it with:

1
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"

绑定 HTML 多选框

See the detail information

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...

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

...

func formHandler(c *gin.Context) {
var fakeForm myForm
c.ShouldBind(&fakeForm)
c.JSON(200, gin.H{"color": fakeForm.Colors})
}

...

form.html

1
2
3
4
5
6
7
8
9
10
<form action="/" 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>

result:

1
{"color":["red","green","blue"]}

绑定 Multipart/Urlencoded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

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

type LoginForm struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
}

func main() {
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration:
// c.ShouldBindWith(&form, binding.Form)
// or you can simply use autobinding with ShouldBind method:
var form LoginForm
// in this case proper binding will be automatically selected
if c.ShouldBind(&form) == nil {
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
}
})
router.Run(":8080")
}

Test it with:

1
$ curl -v --form user=user --form password=password http://localhost:8080/login

支持 XML, JSON, YAML 和 ProtoBuf 协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func main() {
r := gin.Default()

// gin.H is a shortcut for map[string]interface{}
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})

r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
var msg struct {
Name string `json:"user"`
Message string
Number int
}
msg.Name = "Lena"
msg.Message = "hey"
msg.Number = 123
// Note that msg.Name becomes "user" in the JSON
// Will output : {"user": "Lena", "Message": "hey", "Number": 123}
c.JSON(http.StatusOK, msg)
})

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

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

r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// The specific definition of protobuf is written in the testdata/protoexample file.
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// Note that data becomes binary data in the response
// Will output protoexample.Test protobuf serialized data
c.ProtoBuf(http.StatusOK, data)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

使用 SecureJSON

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
r := gin.Default()

// You can also use your own secure json prefix
// r.SecureJsonPrefix(")]}',\n")

r.GET("/someJSON", func(c *gin.Context) {
names := []string{"lena", "austin", "foo"}

// Will output : while(1);["lena","austin","foo"]
c.SecureJSON(http.StatusOK, names)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

使用 JSONP

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
r := gin.Default()

r.GET("/JSONP?callback=x", func(c *gin.Context) {
data := map[string]interface{}{
"foo": "bar",
}

//callback is x
// Will output : x({\"foo\":\"bar\"})
c.JSONP(http.StatusOK, data)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

使用 AsciiJSON

Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
r := gin.Default()

r.GET("/someJSON", func(c *gin.Context) {
data := map[string]interface{}{
"lang": "GO语言",
"tag": "<br>",
}

// will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
c.AsciiJSON(http.StatusOK, data)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

使用 PureJSON

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
r := gin.Default()

// Serves unicode entities
r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})

// Serves literal characters
r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})

// listen and serve on 0.0.0.0:8080
r.Run(":8080)
}

静态文件服务

1
2
3
4
5
6
7
8
9
func main() {
router := gin.Default()
router.Static("/assets", "./assets")
router.StaticFS("/more_static", http.Dir("my_file_system"))
router.StaticFile("/favicon.ico", "./resources/favicon.ico")

// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}

数据流服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
return
}

reader := response.Body
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)
})
router.Run(":8080")
}

使用 HTML 模板渲染

Using LoadHTMLGlob() or LoadHTMLFiles()

1
2
3
4
5
6
7
8
9
10
11
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})
router.Run(":8080")
}

templates/index.tmpl

1
2
3
4
5
<html>
<h1>
{{ .title }}
</h1>
</html>

Using templates with same name in different directories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/**/*")
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(":8080")
}

templates/posts/index.tmpl

1
2
3
4
5
6
7
{{ define "posts/index.tmpl" }}
<html><h1>
{{ .title }}
</h1>
<p>Using posts/index.tmpl</p>
</html>
{{ end }}

templates/users/index.tmpl

1
2
3
4
5
6
7
{{ define "users/index.tmpl" }}
<html><h1>
{{ .title }}
</h1>
<p>Using users/index.tmpl</p>
</html>
{{ end }}

自定义模板渲染

You can also use your own html template render

1
2
3
4
5
6
7
8
import "html/template"

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

自定义分割符

You may use custom delims

1
2
3
r := gin.Default()
r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates"))

自定义模板方法

See the detail example code.

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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", map[string]interface{}{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})

router.Run(":8080")
}

raw.tmpl

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

Result:

1
Date: 2017/07/01

多模板

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

页面重定向

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

1
2
3
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})

Issuing a Router redirect, use HandleContext like below.

1
2
3
4
5
6
7
r.GET("/test", func(c *gin.Context) {
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(200, gin.H{"hello": "world"})
})

自定义中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()

// Set example variable
c.Set("example", "12345")

// before request

c.Next()

// after request
latency := time.Since(t)
log.Print(latency)

// access the status we are sending
status := c.Writer.Status()
log.Println(status)
}
}

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

r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)

// it would print: "12345"
log.Println(example)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

使用 BasicAuth() 中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// simulate some private data
var secrets = gin.H{
"foo": gin.H{"email": "foo@bar.com", "phone": "123433"},
"austin": gin.H{"email": "austin@example.com", "phone": "666"},
"lena": gin.H{"email": "lena@guapa.com", "phone": "523443"},
}

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

// Group using gin.BasicAuth() middleware
// gin.Accounts is a shortcut for map[string]string
authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
"foo": "bar",
"austin": "1234",
"lena": "hello2",
"manu": "4321",
}))

// /admin/secrets endpoint
// hit "localhost:8080/admin/secrets
authorized.GET("/secrets", func(c *gin.Context) {
// get user, it was set by the BasicAuth middleware
user := c.MustGet(gin.AuthUserKey).(string)
if secret, ok := secrets[user]; ok {
c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
} else {
c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
}
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

在中间件中使用 Goroutines 协程

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
r := gin.Default()

r.GET("/long_async", func(c *gin.Context) {
// create copy to be used inside the goroutine
cCp := c.Copy()
go func() {
// simulate a long task with time.Sleep(). 5 seconds
time.Sleep(5 * time.Second)

// note that you are using the copied context "cCp", IMPORTANT
log.Println("Done! in path " + cCp.Request.URL.Path)
}()
})

r.GET("/long_sync", func(c *gin.Context) {
// simulate a long task with time.Sleep(). 5 seconds
time.Sleep(5 * time.Second)

// since we are NOT using a goroutine, we do not have to copy the context
log.Println("Done! in path " + c.Request.URL.Path)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}

自定义 HTTP 配置

Use http.ListenAndServe() directly, like this:

1
2
3
4
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}

or

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
router := gin.Default()

s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}

支持 Let’s Encrypt

example for 1-line LetsEncrypt HTTPS servers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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"))
}

example for custom autocert manager.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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))
}

使用 Gin 运行多个服务

See the question and try the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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 {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})

return e
}

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

return e
}

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

server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

g.Go(func() error {
return server01.ListenAndServe()
})

g.Go(func() error {
return server02.ListenAndServe()
})

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

优雅的重启和停止服务

Do you want to graceful restart or stop your web server?
There are some ways this can be done.

We can use fvbock/endless to replace the default ListenAndServe. Refer issue #296 for more details.

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

An alternative to endless:

  • 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.

If you are using Go 1.8, you may not need to use this library! Consider using http.Server’s built-in Shutdown() method for graceful shutdowns. See the full graceful-shutdown example with gin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// +build go1.8

package main

import (
"context"
"log"
"net/http"
"os"
"os/signal"
"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,
}

go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()

// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutdown Server ...")

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exiting")
}

构建包含模板的可执行文件

You can build a server into a single binary containing templates by using go-assets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 {
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 examples/assets-in-binary directory.

自定义 struct 绑定表单请求参数

The follow example using custom struct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 {
NestedAnonyStruct struct {
FieldX string `form:"field_x"`
}
FieldD string `form:"field_d"`
}

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

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

func GetDataD(c *gin.Context) {
var b StructD
c.Bind(&b)
c.JSON(200, gin.H{
"x": b.NestedAnonyStruct,
"d": b.FieldD,
})
}

func main() {
r := gin.Default()
r.GET("/getb", GetDataB)
r.GET("/getc", GetDataC)
r.GET("/getd", GetDataD)

r.Run()
}

Using the command curl command result:

1
2
3
4
5
6
$ curl "http://localhost:8080/getb?field_a=hello&field_b=world"
{"a":{"FieldA":"hello"},"b":"world"}
$ curl "http://localhost:8080/getc?field_a=hello&field_c=world"
{"a":{"FieldA":"hello"},"c":"world"}
$ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
{"d":"world","x":{"FieldX":"hello"}}

NOTE: NOT support the follow style struct:

1
2
3
4
5
6
7
8
9
10
11
type StructX struct {
X struct {} `form:"name_x"` // HERE have form
}

type StructY struct {
Y StructX `form:"name_y"` // HERE hava form
}

type StructZ struct {
Z *StructZ `form:"name_z"` // HERE hava form
}

In a word, only support nested custom struct which have no form now.

绑定请求 Body 到不同的 struct

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type formA struct {
Foo string `json:"foo" xml:"foo" binding:"required"`
}

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

func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This c.ShouldBind consumes c.Request.Body and it cannot be reused.
if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// Always an error is occurred by this because c.Request.Body is EOF now.
} else if errB := c.ShouldBind(&objB); errB == nil {
c.String(http.StatusOK, `the body should be formB`)
} else {
...
}
}

For this, you can use c.ShouldBindBodyWith.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This reads c.Request.Body and stores the result into the context.
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// At this time, it reuses body stored in the context.
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// And it can accepts other formats
} else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`)
} else {
...
}
}
  • c.ShouldBindBodyWith stores body into the context before binding. This has
    a slight impact to performance, so you should not use this method 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 (See #1341).

http2 服务端推送

http.Pusher is supported only go1.8+. See the golang blog for detail information.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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")
}

测试

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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())
}