gin_readme_zh
gin_readme_zh copied to clipboard
Gin web框架 使用说明中文版本
Gin Web Framework
Gin是一个使用Go语言写的web框架.它拥有与Martini相似的API,但它比Martini快40多倍.Gin内部使用 Golang最快的HTTP路由器httprouter.如果你需要更高的性能,更快的开发效率,你会喜欢上Gin.

目录
- 快速开始
- 性能基准
- Gin v1.稳定版
- 开始使用
- 使用jsoniter
- API示例
- GET,POST,PUT,PATCH,DELETE和OPTIONS
- url路径中的参数
- url中的查询参数
- Multipart/Urlencoded Form
- url中查询参数+form表单数据
- 文件上传
- 路由分组
- 不使用默认的中间件
- 使用中间件
- 如何编写日志文件
- 模型绑定和验证
- 自定义验证
- 仅绑定查询字符串
- 绑定查询字符串或Post数据
- 绑定 HTML checkboxes
- 绑定 Multipart/Urlencoded
- XML, JSON 和 YAML 绑定
- JSONP
- 静态文件
- Serving data from reader
- HTML模板渲染
- Multitemplate
- 重定向
- 自定义中间件
- 使用 BasicAuth() 中间件
- 中间件内的Goroutines
- 自定义HTTP配置
- 使用Let's Encrypt证书
- 使用Gin运行多个服务
- 优雅的重启或停止
- 用模板构建一个二进制文件
- 使用自定义结构绑定表单数据请求
- 尝试将body绑定到不同的结构中
- 测试
- Users
Quick start
快速开始
# 在example.go文件中假设有以下代码
$ cat example.go
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
}
# 运行 example.go 并在浏览器中访问 0.0.0.0:8080/ping
$ go run example.go
Benchmarks
基准测试
Gin 使用自定义版本的 HttpRouter
| Benchmark name | (1) | (2) | (3) | (4) |
|---|---|---|---|---|
| BenchmarkGin_GithubAll | 30000 | 48375 | 0 | 0 |
| BenchmarkAce_GithubAll | 10000 | 134059 | 13792 | 167 |
| BenchmarkBear_GithubAll | 5000 | 534445 | 86448 | 943 |
| BenchmarkBeego_GithubAll | 3000 | 592444 | 74705 | 812 |
| BenchmarkBone_GithubAll | 200 | 6957308 | 698784 | 8453 |
| BenchmarkDenco_GithubAll | 10000 | 158819 | 20224 | 167 |
| BenchmarkEcho_GithubAll | 10000 | 154700 | 6496 | 203 |
| BenchmarkGocraftWeb_GithubAll | 3000 | 570806 | 131656 | 1686 |
| BenchmarkGoji_GithubAll | 2000 | 818034 | 56112 | 334 |
| BenchmarkGojiv2_GithubAll | 2000 | 1213973 | 274768 | 3712 |
| BenchmarkGoJsonRest_GithubAll | 2000 | 785796 | 134371 | 2737 |
| BenchmarkGoRestful_GithubAll | 300 | 5238188 | 689672 | 4519 |
| BenchmarkGorillaMux_GithubAll | 100 | 10257726 | 211840 | 2272 |
| BenchmarkHttpRouter_GithubAll | 20000 | 105414 | 13792 | 167 |
| BenchmarkHttpTreeMux_GithubAll | 10000 | 319934 | 65856 | 671 |
| BenchmarkKocha_GithubAll | 10000 | 209442 | 23304 | 843 |
| BenchmarkLARS_GithubAll | 20000 | 62565 | 0 | 0 |
| BenchmarkMacaron_GithubAll | 2000 | 1161270 | 204194 | 2000 |
| BenchmarkMartini_GithubAll | 200 | 9991713 | 226549 | 2325 |
| BenchmarkPat_GithubAll | 200 | 5590793 | 1499568 | 27435 |
| BenchmarkPossum_GithubAll | 10000 | 319768 | 84448 | 609 |
| BenchmarkR2router_GithubAll | 10000 | 305134 | 77328 | 979 |
| BenchmarkRivet_GithubAll | 10000 | 132134 | 16272 | 167 |
| BenchmarkTango_GithubAll | 3000 | 552754 | 63826 | 1618 |
| BenchmarkTigerTonic_GithubAll | 1000 | 1439483 | 239104 | 5374 |
| BenchmarkTraffic_GithubAll | 100 | 11383067 | 2659329 | 21848 |
| BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 | 609 |
- (1): 持续时间达到的总重复次数越多,意味着结果越好
- (2): 单次重复持续时间(ns / op)越低越好
- (3): 堆内存(B / op)越低越好
- (4): 平均每次重复分配 (allocs/op) 越低越好
Gin v1. stable
- [x] Zero allocation router.
- [x] Still the fastest http router and framework. From routing to writing.
- [x] Complete suite of unit tests
- [x] Battle tested
- [x] API frozen, new releases will not break your code.
Start using it
开始使用
- 下载并安装它:
$ go get github.com/gin-gonic/gin
- 将其导入到您的代码中:
import "github.com/gin-gonic/gin"
- (可选的) 导入
net/http. 如果您要使用诸如http.StatusOK的常量.
import "net/http"
Use a vendor tool like Govendor
使用包管理工具 Govendor
- 使用
go get获取govendor
$ go get github.com/kardianos/govendor
- 创建并进入你的项目文件夹
$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
- 初始化你的项目并添加gin
$ govendor init
$ govendor fetch github.com/gin-gonic/[email protected]
- 复制一个初始模板到你的项目中
$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go
- 运行你的项目
$ go run main.go
Build with jsoniter
使用jsoniter
Gin使用dncoding/json 作为默认的json包,但是你可以在构建的时候用jsoniter 替换它
$ go build -tags=jsoniter .
API示例
Using GET, POST, PUT, PATCH, DELETE and OPTIONS
func main() {
// 禁用控制台颜色
// gin.DisableConsoleColor()
// 用默认的中间件创建一个gin路由器:
// 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.
// 服务默认使用8080端口,除非你自定义了端口号的环境变量
router.Run()
// router.Run(":3000") 指定端口号为 :3000
}
Parameters in path
url路径中的参数
func main() {
router := gin.Default()
// 路由1 匹配 /user/john,但是不匹配 /user/ 或 /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// 路由2 这个会匹配 /user/john/ 和 /user/john/send
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)
})
// 注意 /user/:name 和 /user/:name/是俩个完全不同的路由
router.Run(":8080")
}
➜ ~ curl 127.0.0.1:8080/user/jack
Hello jack
➜ ~ curl 127.0.0.1:8080/user/jack/
jack is
➜ ~ curl 127.0.0.1:8080/user/jack/do
jack is /do
Querystring parameters
url中的查询参数
如?q=123&key=789
func main() {
router := gin.Default()
// Query string parameters are parsed using the existing underlying request object.
// 匹配url: /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
// 取firstname的值,不存在则设为Guest
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")
}
➜ ~ curl -XGET "127.0.0.1:8080/welcome?firstname=Jane&lastname=Doe"
Hello Jane Doe
➜ ~ curl -XGET "127.0.0.1:8080/welcome"
Hello Guest
➜ ~ curl -XGET "127.0.0.1:8080/welcome?lastname=Doe"
Hello Guest Doe
Multipart/Urlencoded Form
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")
}
➜ ~ curl -X POST http://127.0.0.1:8080/form_post -F nick=bddbnet -F message=hello
{"message":"hello","nick":"bddbnet","status":"posted"}
➜ ~ curl -X POST http://127.0.0.1:8080/form_post -F message=hello
{"message":"hello","nick":"anonymous","status":"posted"}
➜ ~ curl -X POST http://127.0.0.1:8080/form_post -F nick=bddbnet
{"message":"","nick":"bddbnet","status":"posted"}
Another example: query + post form
url中查询参数+form表单数据
POST /post?id=1234&page=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
name=manu&message=this_is_great
func main() {
router := gin.Default()
router.POST("/post", func(c *gin.Context) {
// url中查询数据
id := c.Query("id")
page := c.DefaultQuery("page", "0")
// post表单中数据
name := c.PostForm("name")
message := c.PostForm("message")
c.JSON(200, gin.H{
"status": "posted",
"id": id,
"page": page,
"name": name,
"message": message,
})
fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
})
router.Run(":8080")
}
➜ ~ curl -X POST 'http://127.0.0.1:8080/post?id=123&page=1' -F name=bddbnet -F message=hello
{"id":"123","message":"hello","name":"bddbnet","page":"1","status":"posted"}
➜ ~ curl -X POST 'http://127.0.0.1:8080/post?id=123' -F name=bddbnet -F message=hello
{"id":"123","message":"hello","name":"bddbnet","page":"0","status":"posted"}
➜ ~ curl -X POST 'http://127.0.0.1:8080/post' -F name=bddbnet -F message=hello
{"id":"","message":"hello","name":"bddbnet","page":"0","status":"posted"}
➜ ~ curl -X POST 'http://127.0.0.1:8080/post' -F name=bddbnet
{"id":"","message":"","name":"bddbnet","page":"0","status":"posted"}
Upload files
文件上传
Single file
单个文件
参考问题 #774 和细节example code.
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)
savePath := "/tmp/"
dst := savePath + file.Filename
// Upload the file to specific dst.
err := c.SaveUploadedFile(file, dst)
if err != nil {
panic(err)
}
c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
})
router.Run(":8080")
}
➜ ~ curl -X POST http://localhost:8080/upload \
-F "file=@/home/bddbnet/Pictures/bg002.jpg" \
-H "Content-Type: multipart/form-data"
'bg002.jpg' uploaded!
Multiple files
多个文件
See the detail example code.
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[]"]
savePath := "/tmp/"
for _, file := range files {
log.Println(file.Filename)
dst := savePath + 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")
}
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"
2 files uploaded!
Grouping routes
路由分组
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")
}
Blank Gin without middleware by default
不使用默认的中间件
用
r := gin.New()
替代
// Default With the Logger and Recovery middleware already attached
// 默认情况已启用了log和恢复中间件
r := gin.Default()
Using middleware
使用中间件
func main() {
// 默认情况下创建一个没有任何中间件的路由器
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())
// 每个路由中,你可以使用任意多个中间件.
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)
// 权限组
// authorized := r.Group("/", AuthRequired())
// 等同于:
authorized := r.Group("/")
// 在这组路由中,我们使用自定义的中间件
// AuthRequired() 中间件只在 "authorized" 组中使用.
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)
// 嵌套组
testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
}
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
How to write log file
如何写日志文件
func main() {
// 禁用控制台颜色,写入日志文件时不需要添加颜色
gin.DisableConsoleColor()
// 写入到文件.
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// 如果您需要同时将日志写入文件和控制台,请使用以下代码
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
Model binding and validation
模型绑定和验证
使用模型绑定,将请求主体绑定到一个类型.我们目前支持JSON的绑定,XML和标准表单值(foo=bar&boo=baz).
Gin 采用 go-playground/validator.v8进行验证. 点击 这里查看所有文档.
请注意,您需要在所有要绑定的字段上设置相应的绑定标签.例如从JSON绑定时, 添加结构体字段标签 json:"fieldname".
此外,Gin提供了两种绑定方法:
-
种类 - Must bind
- 方法 -
Bind,BindJSON,BindQuery - 特性 - 这些方法在底层使用
MustBindWith。如果存在绑定错误,则使用c.AbortWithError(400,err).SetType(ErrorTypeBind)中止请求。这将响应状态码设置为400,并且将Content-Type标头设置为text/plain; charset=utf-8。请注意,如果您尝试在此之后设置响应代码,则会导致警告[GIN-debug] [WARNING] Headers were already written(请求头已经设置). Wanted to override status code 400 with 422(企图用422覆盖状态码400)。如果你希望更好地控制行为,可以考虑使用ShouldBind等价的方法。
- 方法 -
-
种类 - Should bind
- 方法 -
ShouldBind,ShouldBindJSON,ShouldBindQuery - 特性 - 这些方法使用
ShouldBindWith。如果存在绑定错误,则返回错误,并且开发人员有责任正确处理请求和错误
- 方法 -
使用绑定方法时, Gin试图根据Content-Type头推断绑定数据的类型. 如果你能够确认绑定数据的类型, 可以使用 MustBindWith 或 ShouldBindWith绑定数据.
你可以指定需要绑定数据的字段. 如果这个字段存在这个 binding:"required"的结构体字段标签并且在绑定时字段的值为空值时, 将会返回一个错误.
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"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 {
if json.User == "manu" && json.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 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 {
if form.User == "manu" && form.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
Sample request
$ 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"}
Custom Validators
自定义验证
你也可以注册自定义验证器. 点这里查看 例子.
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()})
}
}
$ 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"}
结构级别验证 也可以用这种方式注册. 点击这里查看 例子 .
Only Bind Query String
仅绑定Url查询字符串
ShouldBindQuery 函数只绑定查询参数而不是post的数据. 详情点击这里 查看.
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.JSON(http.StatusOK, gin.H{"Name": person.Name, "Address": person.Address})
}
~ curl 'http://127.0.0.1:8080/testing?name=tom&address=none'
{"Address":"none","Name":"tom"}
Bind Query String or Post Data
绑定Url查询字符串或Post的数据
详情见 这里.
package main
import "log"
import "github.com/gin-gonic/gin"
import "time"
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:
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
Bind HTML checkboxes
绑定 HTML checkboxes
See the detail information
main.go
...
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
<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:
{"color":["red","green","blue"]}
Multipart/Urlencoded binding
绑定 Multipart/Urlencoded
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:
$ curl -v --form user=user --form password=password http://localhost:8080/login
XML, JSON and YAML rendering
XML, JSON and YAML 绑定
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})
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
SecureJSON
使用SecureJSON来防止json劫持. 如果返回的结果是数组则会在返回数据前添加默认的前缀 "while(1),".
func main() {
r := gin.Default()
// 你也可以自定义你自己的安全json前缀
// 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")
}
➜ hosttodo curl 'http://127.0.0.1:8080/someJSON'
while(1);["lena","austin","foo"]
JSONP
使用JSONP从不同域中的服务器请求数据。如果查询参数回调存在,请将回调添加到响应主体。
func main() {
r := gin.Default()
// 这里假定访问的url地址是 /JSONP?callback=x
// url中必须存在callback= 才会返回jsonp,否则返回json
r.GET("/JSONP", 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")
}
➜ ~ curl 'http://127.0.0.1:8080/JSONP?callback=x'
x({"foo":"bar"})
➜ ~ curl 'http://127.0.0.1:8080/JSONP?call=x'
{"foo":"bar"}
Serving static files
静态文件
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")
}
Serving data from reader
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 rendering
HTML模板渲染
用 LoadHTMLGlob() 或 LoadHTMLFiles() 函数加载模板文件
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
<html>
<h1>
{{ .title }}
</h1>
</html>
使用不同目录中具有相同名称的模板
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
{{ define "posts/index.tmpl" }}
<html><h1>
{{ .title }}
</h1>
<p>Using posts/index.tmpl</p>
</html>
{{ end }}
templates/users/index.tmpl
{{ define "users/index.tmpl" }}
<html><h1>
{{ .title }}
</h1>
<p>Using users/index.tmpl</p>
</html>
{{ end }}
Custom Template renderer
自定义模板渲染
你也可以自己定义模板渲染方式
import "html/template"
func main() {
router := gin.Default()
html := template.Must(template.ParseFiles("file1", "file2"))
router.SetHTMLTemplate(html)
router.Run(":8080")
}
Custom Delimiters
自定义分隔符
你可以自己定义分隔符
r := gin.Default()
r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates"))
Custom Template Funcs
自定义模板函数
细节看 这里.
main.go
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("./fixtures/basic/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
// 使用formatAsDate函数
Date: {[{.now | formatAsDate}]}
Result:
Date: 2017/07/01
Multitemplate
使用多个模板文件
Gin 默认情况下只允许使用一个模板文件. 点击 这里 看如何使用如 go 1.6 block template来实现多模板渲染.
Redirects
重定向
HTTP重定向实现很容易:
r.GET("/test", func(c *gin.Context) {
// 重定向
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})
站内站外的重定向都被支持
Custom Middleware
自定义中间件
// 定义一个Looger中间件
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// Set example variable
c.Set("example", "12345")
// request请求之前做什么的代码写在这里
c.Next()
// request请求之后做什么的代码写在这里
latency := time.Since(t)
log.Print(latency)
// 获取我们正在发送的状态
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")
}
Using BasicAuth() middleware
使用认证中间件
// 模拟一些私有数据
var secrets = gin.H{
"foo": gin.H{"email": "[email protected]", "phone": "123433"},
"austin": gin.H{"email": "[email protected]", "phone": "666"},
"lena": gin.H{"email": "[email protected]", "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 inside a middleware
中间件内的Goroutines
在中间件或处理程序中启动新的Goroutines时, 你 一定不要 使用它内部的原始上下文, 你必须使用只读副本.
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() {
// 模拟一个耗时任务
time.Sleep(5 * time.Second)
// 一定要使用复制的cCp
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)
// 不使用Goroutines则不需要复制
log.Println("Done! in path " + c.Request.URL.Path)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
Custom HTTP configuration
自定义HTTP配置
Use http.ListenAndServe() directly, like this:
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
or
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()
}
Support Let's Encrypt
使用Let's Encrypt证书
1行代码实现 LetsEncrypt HTTPS服务器.
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"))
}
自定义autocert管理器的示例.
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))
}
Run multiple service using Gin
使用Gin运行多个服务
请参阅问题并尝试以下示例:
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)
}
}
Graceful restart or stop
优雅的重启或停止
以下方式可以让你优雅的重启或停止你的web服务器。
我们可以用 fvbock/endless 取代默认的 ListenAndServe. 请参阅 问题#296获得更多细节.
router := gin.Default()
router.GET("/", handler)
// [...]
endless.ListenAndServe(":4242", router)
其他的替代方案:
- 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.
// +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")
}
Build a single binary with templates
将服务器构建为一个包含模板文件的二进制文件
您可以通过使用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 {
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.
Bind form-data request with custom struct
使用自定义结构绑定表单数据
以下使用自定义结构的示例:
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:
$ 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:
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.
Try to bind body into different structs
尝试将body绑定到不同的结构中
The normal methods for binding request body consumes c.Request.Body and they
cannot be called multiple times.
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.
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.ShouldBindBodyWithstores 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 byc.ShouldBind()multiple times without any damage to performance (See #1341).
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())
}
Users 
Awesome project lists using Gin web framework.