gin快速入门 Gin Quick Start - gin快速入门 原文:https://github.com/gin-gonic/gin/blob/master/docs/doc.md
构建标签 使用替代的json包构建 gin默认使用encoding/json
作为json包,但你可以通过使用其他标签进行构建来更改它。
jsoniter
1
go build -tags= jsoniter .
go-json
1
go build -tags= go_json .
sonic (你必须确保你的CPU支持AVX指令。)
1
$ go build -tags= "sonic avx" .
不使用MsgPack
渲染特性构建 gin默认启用MsgPack
渲染特性。但你可以通过指定nomsgpack
构建标签来禁用此特性:
1
go build -tags= nomsgpack .
这有助于减小可执行文件的大小。详细信息请参见详细信息 。
API 示例 你可以在Gin示例仓库 中找到许多可直接运行的示例。
使用GET, POST, PUT, PATCH, DELETE and OPTIONS 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main () {
// 创建一个带有默认中间件的gin router:
// logger和recovery(无崩溃 crash-free)中间件
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 )
// 默认情况下它监听 :8080,除非定义了一个 PORT 环境变量。
router . Run ()
// 如果要使用硬编码端口,则使用 router.Run(":3000")
}
路径参数 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 () {
router := gin . Default ()
// 这个handler将匹配 /user/john,但不会匹配 /user/ 或 /user
router . GET ( "/user/:name" , func ( c * gin . Context ) {
name := c . Param ( "name" )
c . String ( http . StatusOK , "Hello %s" , name )
})
// 然而,这个handler将匹配 /user/john/ 和 /user/john/send
// 如果没有其他路由器匹配 /user/john,它将重定向到 /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 )
})
// 对于每个匹配的请求,Context将保存路由定义
router . POST ( "/user/:name/*action" , func ( c * gin . Context ) {
b := c . FullPath () == "/user/:name/*action" // true
c . String ( http . StatusOK , "%t" , b )
})
// 这个handler将为 /user/groups 添加一个新的路由器。
// 精确匹配的路由器在param routes之前解析,无论它们的定义顺序如何。
// 以 /user/groups 开头的路由永远不会被解释为 /user/:name/... 路由
router . GET ( "/user/groups" , func ( c * gin . Context ) {
c . String ( http . StatusOK , "The available groups are [...]" )
})
router . Run ( ":8080" )
}
查询字符串参数 1
2
3
4
5
6
7
8
9
10
11
12
13
func main () {
router := gin . Default ()
// 查询字符串参数使用现有的底层请求对象进行解析。
// 请求响应的 URL 匹配:/welcome?firstname=Jane&lastname=Doe
router . GET ( "/welcome" , func ( c * gin . Context ) {
firstname := c . DefaultQuery ( "firstname" , "Guest" )
lastname := c . Query ( "lastname" ) // 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 ( http . StatusOK , 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
将 Map 作为查询字符串或 post 表单参数 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]
上传文件 单个文件 参考问题#774 和详细的示例代码 。
file.Filename
SHOULD NOT 不应该 被信任。参见Content-Disposition
在MDN上的说明 和#1693 。
文件名始终是可选的,应用程序不应该盲目使用它:应该去除路径信息,并进行服务器文件系统规则的转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main () {
router := gin . Default ()
// 设置 multipart 表单的内存限制(默认是 32 MiB)
router . MaxMultipartMemory = 8 << 20 // 8 MiB
router . POST ( "/upload" , func ( c * gin . Context ) {
// 单个文件
file , _ := c . FormFile ( "file" )
log . Println ( file . Filename )
// 将文件上传到指定的目标位置
c . SaveUploadedFile ( file , dst )
c . String ( http . StatusOK , fmt . Sprintf ( "'%s' uploaded!" , file . Filename ))
})
router . Run ( ":8080" )
}
使用 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 ()
// 设置 multipart 表单的内存限制(默认是 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 )
// 将文件上传到指定的目标位置
c . SaveUploadedFile ( file , dst )
}
c . String ( http . StatusOK , fmt . Sprintf ( "%d files uploaded!" , len ( files )))
})
router . Run ( ":8080" )
}
使用 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 ()
// 简单分组:v1
v1 := router . Group ( "/v1" )
{
v1 . POST ( "/login" , loginEndpoint )
v1 . POST ( "/submit" , submitEndpoint )
v1 . POST ( "/read" , readEndpoint )
}
// 简单分组:v2
v2 := router . Group ( "/v2" )
{
v2 . POST ( "/login" , loginEndpoint )
v2 . POST ( "/submit" , submitEndpoint )
v2 . POST ( "/read" , readEndpoint )
}
router . Run ( ":8080" )
}
默认情况下没有中间件的空白gin 使用
而不是
1
2
// 默认情况下,附带Logger和Recovery中间件
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
36
37
func main () {
// 默认情况下创建一个没有任何中间件的路由器
r := gin . New ()
// 全局中间件
// Logger中间件会将日志写入gin.DefaultWriter,即使你设置了 GIN_MODE=release。
// 默认情况下,gin.DefaultWriter = os.Stdout
r . Use ( gin . Logger ())
// Recovery中间件会在出现恐慌时恢复,并在有恐慌时写入一个500响应。
r . Use ( gin . Recovery ())
// 为每个路由添加中间件,你可以添加任意多个。
r . GET ( "/benchmark" , MyBenchLogger (), benchEndpoint )
// 授权组 - Authorization group
// authorized := r.Group("/", AuthRequired())
// 与上面代码完全等价:
authorized := r . Group ( "/" )
// 在授权组中使用自定义创建的 AuthRequired() 中间件。
// 每个分组的中间件!在这种情况下,我们在"authorized"分组中使用自定义创建的AuthRequired()中间件。
authorized . Use ( AuthRequired ())
{
authorized . POST ( "/login" , loginEndpoint )
authorized . POST ( "/submit" , submitEndpoint )
authorized . POST ( "/read" , readEndpoint )
// 嵌套组
testing := authorized . Group ( "testing" )
// 访问 0.0.0.0:8080/testing/analytics
testing . GET ( "/analytics" , analyticsEndpoint )
}
// 监听并在 0.0.0.0:8080 上提供服务
r . Run ( ":8080" )
}
自定义Recovery 行为 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
func main () {
// 默认情况下创建一个没有任何中间件的路由器
r := gin . New ()
// 全局中间件
// Logger中间件会将日志写入gin.DefaultWriter,即使你设置了 GIN_MODE=release。
// 默认情况下,gin.DefaultWriter = os.Stdout
r . Use ( gin . Logger ())
// Recovery中间件会在出现恐慌时恢复,并在有恐慌时写入一个500响应。
r . Use ( gin . CustomRecovery ( func ( c * gin . Context , recovered any ) {
if err , ok := recovered .( string ); ok {
c . String ( http . StatusInternalServerError , fmt . Sprintf ( "error: %s" , err ))
}
c . AbortWithStatus ( http . StatusInternalServerError )
}))
r . GET ( "/panic" , func ( c * gin . Context ) {
// panic with a string —— 自定义的中间件可以将其保存到数据库或向用户报告。
panic ( "foo" )
})
r . GET ( "/" , func ( c * gin . Context ) {
c . String ( http . StatusOK , "ohai" )
})
// 监听并在 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 () {
// 禁用控制台颜色,当将日志写入文件时,不需要控制台颜色。
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 ( http . StatusOK , "pong" )
})
router . Run ( ":8080" )
}
自定义日志格式 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
func main () {
router := gin . New ()
// LoggerWithFormatter 中间件会将日志写入 gin.DefaultWriter。
// 默认情况下,gin.DefaultWriter = os.Stdout
router . Use ( gin . LoggerWithFormatter ( func ( param gin . LogFormatterParams ) string {
// 你自定义的格式
return fmt . Sprintf ( "%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n" ,
param . ClientIP ,
param . TimeStamp . Format ( time . RFC1123 ),
param . Method ,
param . Path ,
param . Request . Proto ,
param . StatusCode ,
param . Latency ,
param . Request . UserAgent (),
param . ErrorMessage ,
)
}))
router . Use ( gin . Recovery ())
router . GET ( "/ping" , func ( c * gin . Context ) {
c . String ( http . StatusOK , "pong" )
})
router . Run ( ":8080" )
}
示例输出
1
::1 - [ Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs " Mozilla/5.0 ( Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 ( KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
控制日志输出颜色 默认情况下,根据检测到的TTY对控制台上的日志进行颜色化。
永远不要给日志着色:
1
2
3
4
5
6
7
8
9
10
11
12
13
func main () {
// 禁用日志颜色
gin . DisableConsoleColor ()
// 创建一个具有默认中间件的 gin 路由器:日志记录器(logger)和recovery(crash-free)中间件。
router := gin . Default ()
router . GET ( "/ping" , func ( c * gin . Context ) {
c . String ( http . StatusOK , "pong" )
})
router . Run ( ":8080" )
}
始终给日志着色:
1
2
3
4
5
6
7
8
9
10
11
12
13
func main () {
// 强制给日志着色
gin . ForceConsoleColor ()
// // 创建一个具有默认中间件的 gin 路由器:日志记录器(logger)和recovery(crash-free)中间件。
router := gin . Default ()
router . GET ( "/ping" , func ( c * gin . Context ) {
c . String ( http . StatusOK , "pong" )
})
router . Run ( ":8080" )
}
Model绑定和验证 使用模型绑定将请求体绑定到类型上。我们目前支持JSON、XML、YAML、TOML和标准表单值(foo=bar&boo=baz
)的绑定。
gin使用go-playground/validator/v10 进行验证。可以在此处查看有关标签使用的完整文档 here
请注意,您需要在要绑定的所有字段上设置相应的绑定标签。例如,当从JSON绑定时,设置json:"fieldname"
。
此外,Gin提供了两组用于绑定的方法:
Type —— 必须绑定Methods —— Bind
, BindJSON
, BindXML
, BindQuery
, BindYAML
, BindHeader
, BindTOML
Behavior —— 这些方法在内部使用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
。如果您希望更好地控制行为,请考虑使用相应的ShouldBind
方法。Type —— 应该绑定Methods —— ShouldBind
, ShouldBindJSON
, ShouldBindXML
, ShouldBindQuery
, ShouldBindYAML
, ShouldBindHeader
, ShouldBindTOML
,Behavior —— 这些方法在内部使用ShouldBindWith
。如果有绑定错误,将返回错误,开发人员有责任适当处理请求和错误。 当使用Bind-method方法时,gin会根据Content-Type头来推断binder。如果您确定要绑定的内容,可以使用MustBindWith
或ShouldBindWith
。
您还可以指定特定字段是必需的。如果字段被装饰为binding:"required"
并且在绑定时具有空值,将返回错误。
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
// 从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 ()
// 以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" })
})
// 以XML为例进行绑定 (
// <?xml version="1.0" encoding="UTF-8"?>
// <root>
// <user>manu</user>
// <password>123</password>
// </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" })
})
// 以HTML表单为例进行绑定 (user=manu&password=123)
router . POST ( "/loginForm" , func ( c * gin . Context ) {
var form Login
// 这将根据Content-Type头来推断要使用的binder。
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" })
})
// 监听并在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
$ 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" }
跳过验证:使用上面的curl
命令运行上面的示例时,它会返回错误。因为示例中为Password
使用了binding:"required"
。如果为Password
使用binding:"-"
,再次运行上面的代码示例时,将不会返回错误。
自定义验证器 您还可以注册自定义验证器。请参阅示例代码 。
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
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
// Booking 包含绑定和验证的数据。
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"`
}
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 () {
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
6
7
8
$ curl "localhost:8085/bookable?check_in=2030-04-16&check_out=2030-04-17"
{"message":"Booking dates are valid!"}
$ curl "localhost:8085/bookable?check_in=2030-03-10&check_out=2030-03-09"
{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
$ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}%
结构级别验证 也可以通过此方式注册。
请参阅struct-lvl-validation example 了解更多信息。
仅绑定查询字符串 ShouldBindQuery
函数只绑定查询参数而不绑定post数据。请参阅详细信息 。
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"
"net/http"
"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 ( http . StatusOK , "Success" )
}
绑定查询字符串或Post数据 请参阅详细信息 。
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
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:"1"`
CreateTime time . Time `form:"createTime" time_format:"unixNano"`
UnixTime time . Time `form:"unixTime" time_format:"unix"`
}
func main () {
route := gin . Default ()
route . GET ( "/testing" , startPage )
route . Run ( ":8085" )
}
func startPage ( c * gin . Context ) {
var person Person
// 如果是`GET`请求,仅使用`Form`绑定引擎(`query`)。
// 如果是`POST`请求,首先检查`content-type`是否为`JSON`或`XML`,然后使用`Form`(`form-data`)。
// 请参阅 https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L88 了解更多信息。
if c . ShouldBind ( & person ) == nil {
log . Println ( person . Name )
log . Println ( person . Address )
log . Println ( person . Birthday )
log . Println ( person . CreateTime )
log . Println ( person . UnixTime )
}
c . String ( http . StatusOK , "Success" )
}
使用以下命令进行测试:
1
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
绑定 Uri 请参阅详细信息 。
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
package main
import (
"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 () {
route := gin . Default ()
route . GET ( "/:name/:id" , func ( c * gin . Context ) {
var person Person
if err := c . ShouldBindUri ( & person ); err != nil {
c . JSON ( http . StatusBadRequest , gin . H { "msg" : err . Error ()})
return
}
c . JSON ( http . StatusOK , gin . H { "name" : person . Name , "uuid" : person . ID })
})
route . Run ( ":8088" )
}
使用以下命令进行测试:
1
2
curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
curl -v localhost:8088/thinkerou/not-uuid
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 (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
}
func main () {
r := gin . Default ()
r . GET ( "/" , func ( c * gin . Context ) {
h := testHeader {}
if err := c . ShouldBindHeader ( & h ); err != nil {
c . JSON ( http . StatusOK , err )
}
fmt . Printf ( "%#v\n" , h )
c . JSON ( http . StatusOK , gin . H { "Rate" : h . Rate , "Domain" : h . Domain })
})
r . Run ()
// client
// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
// output
// {"Domain":"music","Rate":300}
}
绑定HTML复选框 请参阅详细信息
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
type myForm struct {
Colors [] string `form:"colors[]"`
}
...
func formHandler ( c * gin . Context ) {
var fakeForm myForm
c . ShouldBind ( & fakeForm )
c . JSON ( http . StatusOK , 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 binding 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
type ProfileForm struct {
Name string `form:"name" binding:"required"`
Avatar * multipart . FileHeader `form:"avatar" binding:"required"`
// 或者多个文件
// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
}
func main () {
router := gin . Default ()
router . POST ( "/profile" , func ( c * gin . Context ) {
// 可以使用明确的绑定声明绑定multipart form:
// c.ShouldBindWith(&form, binding.Form)
// 或者可以简单地使用 ShouldBind 方法进行自动绑定:
var form ProfileForm
// 在这种情况下,将自动选择适当的绑定方式
if err := c . ShouldBind ( & form ); err != nil {
c . String ( http . StatusBadRequest , "bad request" )
return
}
err := c . SaveUploadedFile ( form . Avatar , form . Avatar . Filename )
if err != nil {
c . String ( http . StatusInternalServerError , "unknown error" )
return
}
// db.Save(&form)
c . String ( http . StatusOK , "ok" )
})
router . Run ( ":8080" )
}
使用以下命令进行测试:
1
curl -X POST -v --form name = user --form "avatar=@./avatar.png" http://localhost:8080/profile
XML, JSON, YAML, TOML and ProtoBuf rendering 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
func main () {
r := gin . Default ()
// gin.H 是 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 ) {
// 也可以使用结构体
var msg struct {
Name string `json:"user"`
Message string
Number int
}
msg . Name = "Lena"
msg . Message = "hey"
msg . Number = 123
// 注意 msg.Name 在 JSON 中变为 "user"
// 输出:{"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 ( "/someTOML" , func ( c * gin . Context ) {
c . TOML ( http . StatusOK , gin . H { "message" : "hey" , "status" : http . StatusOK })
})
r . GET ( "/someProtoBuf" , func ( c * gin . Context ) {
reps := [] int64 { int64 ( 1 ), int64 ( 2 )}
label := "test"
// protobuf 的具体定义在 testdata/protoexample 文件中
data := & protoexample . Test {
Label : & label ,
Reps : reps ,
}
// 注意 data 在响应中成为二进制数据
// 输出 protoexample.Test 的序列化 protobuf 数据
c . ProtoBuf ( http . StatusOK , data )
})
// 在 0.0.0.0:8080 上监听和服务
r . Run ( ":8080" )
}
SecureJSON 使用 SecureJSON 防止 JSON 劫持。默认情况下,如果给定的结构体是数组值,则在响应体前添加 "while(1),"
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main () {
r := gin . Default ()
// 也可以使用自定义的安全 JSON 前缀
// r.SecureJsonPrefix(")]}',\n")
r . GET ( "/someJSON" , func ( c * gin . Context ) {
names := [] string { "lena" , "austin" , "foo" }
// 输出:while(1);["lena","austin","foo"]
c . SecureJSON ( http . StatusOK , names )
})
// 在 0.0.0.0:8080 上监听和服务
r . Run ( ":8080" )
}
JSONP 使用 JSONP 从不同域请求数据。如果查询参数中存在回调函数,则将回调函数添加到响应体中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main () {
r := gin . Default ()
r . GET ( "/JSONP" , func ( c * gin . Context ) {
data := gin . H {
"foo" : "bar" ,
}
// 回调函数为 x
// 输出:x({\"foo\":\"bar\"})
c . JSONP ( http . StatusOK , data )
})
// 在 0.0.0.0:8080 上监听和服务
r . Run ( ":8080" )
// 客户端
// curl http://127.0.0.1:8080/JSONP?callback=x
}
AsciiJSON 使用 AsciiJSON 生成只包含 ASCII 字符的 JSON,将非 ASCII 字符转义。
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 := gin . H {
"lang" : "GO语言" ,
"tag" : "<br>" ,
}
// 输出: {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
c . AsciiJSON ( http . StatusOK , data )
})
// 在 0.0.0.0:8080 上监听和服务
r . Run ( ":8080" )
}
PureJSON 通常,JSON 会将特殊的 HTML 字符替换为 Unicode 实体,例如 <
会变为 \u003c
。如果要直接编码这些字符,可以使用 PureJSON。
该功能在 Go 1.6 及更低版本不可用。
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 ()
// 服务端以 Unicode 实体形式返回
r . GET ( "/json" , func ( c * gin . Context ) {
c . JSON ( http . StatusOK , gin . H {
"html" : "<b>Hello, world!</b>" ,
})
})
// 服务端以字面字符形式返回
r . GET ( "/purejson" , func ( c * gin . Context ) {
c . PureJSON ( http . StatusOK , gin . H {
"html" : "<b>Hello, world!</b>" ,
})
})
// 监听并在 0.0.0.0:8080 上服务
r . Run ( ":8080" )
}
提供静态文件 Serving static files 1
2
3
4
5
6
7
8
9
10
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" )
router . StaticFileFS ( "/more_favicon.ico" , "more_favicon.ico" , http . Dir ( "my_file_system" ))
// 监听并在 0.0.0.0:8080 上服务
router . Run ( ":8080" )
}
从文件中提供数据 Serving data from file 1
2
3
4
5
6
7
8
9
10
11
12
func main () {
router := gin . Default ()
router . GET ( "/local/file" , func ( c * gin . Context ) {
c . File ( "local/file.go" )
})
var fs http . FileSystem = // ...
router . GET ( "/fs/file" , func ( c * gin . Context ) {
c . FileFromFS ( "fs/file.go" , fs )
})
}
从reader中提供数据 Serving data from reader 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
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 )
})
router . Run ( ":8080" )
}
HTML 渲染 使用 LoadHTMLGlob() 或 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 >
在不同目录中使用相同名称的模板
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
templates/users/index.tmpl ```html
{{ define "posts/index.tmpl" }}
{{ .title }} Using posts/index.tmpl
{{ end }}
```
```html
{{ define "users/index.tmpl" }}
{{ .title }} Using users/index.tmpl
{{ end }}
```
自定义模板渲染器 还可以使用自定义的 HTML 模板渲染器
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" )
}
自定义分隔符 可以使用自定义分隔符
1
2
3
r := gin . Default ()
r . Delims ( "{[{" , "}]}" )
r . LoadHTMLGlob ( "/path/to/templates" )
自定义模板函数 请参考详细的 示例代码 。
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
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
1
Date: {[{.now | formatAsDate}]}
Result:
Multitemplate Gin 默认只允许使用一个 html.Template。可以使用 multitemplate render 来使用类似于 Go 1.6 中的 block template
功能。
重定向 发出(Issuing ) HTTP 重定向很容易。支持内部和外部locations 。
1
2
3
r . GET ( "/test" , func ( c * gin . Context ) {
c . Redirect ( http . StatusMovedPermanently , "http://www.google.com/" )
})
从 POST 请求发出(Issuing ) HTTP 重定向。参考问题:#444
1
2
3
r . POST ( "/test" , func ( c * gin . Context ) {
c . Redirect ( http . StatusFound , "/foo" )
})
发出(Issuing )路由器重定向,使用 HandleContext
如下:
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 ( http . StatusOK , 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
func Logger () gin . HandlerFunc {
return func ( c * gin . Context ) {
t := time . Now ()
// 设置示例变量
c . Set ( "example" , "12345" )
// 请求之前
c . Next ()
// 请求之后
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 )
// 它会打印:"12345"
log . Println ( example )
})
// 监听并在 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
35
// 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 ()
// 使用 gin.BasicAuth() 中间件创建分组
// gin.Accounts 是 map[string]string 的快捷方式
authorized := r . Group ( "/admin" , gin . BasicAuth ( gin . Accounts {
"foo" : "bar" ,
"austin" : "1234" ,
"lena" : "hello2" ,
"manu" : "4321" ,
}))
// /admin/secrets 端点
// hit "localhost:8080/admin/secrets
authorized . GET ( "/secrets" , func ( c * gin . Context ) {
// 获取 user,它由 BasicAuth 中间件设置
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 :(" })
}
})
// 监听并在 0.0.0.0:8080 上服务
r . Run ( ":8080" )
}
中间件内部的 goroutines 在中间件或处理程序内部启动新的 goroutines 时,**SHOULD NOT (不应)**在其中使用原始context,而应使用只读副本。
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 ) {
// 创建一个副本,在 goroutine 内使用副本
cCp := c . Copy ()
go func () {
// 模拟一个耗时的任务,使用 time.Sleep() 休眠 5 秒
time . Sleep ( 5 * time . Second )
// 注意你在这里使用的是副本 "cCp" 的上下文,非常重要
log . Println ( "Done! in path " + cCp . Request . URL . Path )
}()
})
r . GET ( "/long_sync" , func ( c * gin . Context ) {
// 模拟一个耗时的任务,使用 time.Sleep() 休眠 5 秒
time . Sleep ( 5 * time . Second )
// 由于我们没有使用 goroutine,所以不需要复制context
log . Println ( "Done! in path " + c . Request . URL . Path )
})
// 监听并在 0.0.0.0:8080 上服务
r . Run ( ":8080" )
}
自定义 HTTP 配置 可以直接使用 http.ListenAndServe()
进行自定义的 HTTP 配置,如下所示:
1
2
3
4
func main () {
router := gin . Default ()
http . ListenAndServe ( ":8080" , router )
}
或者
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.
使用自动化的一行代码实现 LetsEncrypt HTTPS 服务器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"log"
"net/http"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
)
func main () {
r := gin . Default ()
// Ping 处理程序
r . GET ( "/ping" , func ( c * gin . Context ) {
c . String ( http . StatusOK , "pong" )
})
log . Fatal ( autotls . Run ( r , "example1.com" , "example2.com" ))
}
使用自定义的 autocert 管理器的示例:
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
package main
import (
"log"
"net/http"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/acme/autocert"
)
func main () {
r := gin . Default ()
// Ping 处理程序
r . GET ( "/ping" , func ( c * gin . Context ) {
c . String ( http . StatusOK , "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 运行多个服务 参考问题#346 并尝试以下示例:
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
75
76
77
78
79
80
81
82
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 {
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 )
}
}
优雅地关闭或重启 有几种方法可以实现优雅地关闭或重启。您可以使用专门用于此目的的第三方软件包,也可以使用内置软件包的函数和方法手动执行相同的操作。
第三方包 我们可以使用 fvbock/endless 来替代默认的 ListenAndServe
。有关更多详细信息,请参考问题 #296 。
1
2
3
4
router := gin . Default ()
router . GET ( "/" , handler )
// [...]
endless . ListenAndServe ( ":4242" , router )
其他选择:
grace :Go 服务器的优雅重启和零停机部署。graceful :graceful 是一个 Go 包,用于优雅地关闭 http.Handler 服务器。manners :Manners 是一个友好的 Go HTTP 服务器,能够优雅地关闭。手动操作 如果您使用的是 Go 1.8 或更高版本,可能不需要使用那些库。考虑使用 http.Server
内置的 Shutdown() 方法进行优雅的关闭。下面的示例描述了它的用法,并且我们在这里 有更多使用 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
49
50
51
52
53
54
// +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 ,
}
// 在 goroutine 中初始化服务器,以便不会阻塞下面的优雅关闭处理
go func () {
if err := srv . ListenAndServe (); err != nil && ! errors . Is ( err , http . ErrServerClosed ) {
log . Printf ( "listen: %s\n" , err )
}
}()
// 等待中断信号以便优雅地关闭服务器,超时时间为 5 秒。
quit := make ( chan os . Signal )
// kill (无参数) 默认发送 syscall.SIGTERM
// kill -2 则发送 syscall.SIGINT
// kill -9 则发送 syscall.SIGKILL,但无法被捕获,因此不需要添加
signal . Notify ( quit , syscall . SIGINT , syscall . SIGTERM )
<- quit
log . Println ( "Shutting down server..." )
// context用于通知服务器有 5 秒时间来完成当前正在处理的请求
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" )
}
使用模板构建单个二进制文件 您可以使用 embed 包将服务器构建为包含模板的单个二进制文件。
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
package main
import (
"embed"
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
//go:embed assets/* templates/*
var f embed . FS
func main () {
router := gin . Default ()
templ := template . Must ( template . New ( "" ). ParseFS ( f , "templates/*.tmpl" , "templates/foo/*.tmpl" ))
router . SetHTMLTemplate ( templ )
// 示例: /public/assets/images/example.png
router . StaticFS ( "/public" , http . FS ( f ))
router . GET ( "/" , func ( c * gin . Context ) {
c . HTML ( http . StatusOK , "index.tmpl" , gin . H {
"title" : "Main website" ,
})
})
router . GET ( "/foo" , func ( c * gin . Context ) {
c . HTML ( http . StatusOK , "bar.tmpl" , gin . H {
"title" : "Foo website" ,
})
})
router . GET ( "favicon.ico" , func ( c * gin . Context ) {
file , _ := f . ReadFile ( "assets/favicon.ico" )
c . Data (
http . StatusOK ,
"image/x-icon" ,
file ,
)
})
router . Run ( ":8080" )
}
在https://github.com/gin-gonic/examples/tree/master/assets-in-binary/example02 目录中可以找到完整示例。
使用自定义结构体的示例:
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 ( http . StatusOK , gin . H {
"a" : b . NestedStruct ,
"b" : b . FieldB ,
})
}
func GetDataC ( c * gin . Context ) {
var b StructC
c . Bind ( & b )
c . JSON ( http . StatusOK , gin . H {
"a" : b . NestedStructPointer ,
"c" : b . FieldC ,
})
}
func GetDataD ( c * gin . Context ) {
var b StructD
c . Bind ( & b )
c . JSON ( http . StatusOK , 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 ()
}
使用 curl
命令进行测试:
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" }}
尝试将请求体绑定到不同的结构体中 常规的请求体绑定方法会消耗 c.Request.Body
,而且不能多次调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 {}
// 这个 c.ShouldBind 会消耗 c.Request.Body,并且不能被重复使用。
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.
// 因为 c.Request.Body 现在已经是 EOF,所以总是会发生错误。
} else if errB := c . ShouldBind ( & objB ); errB == nil {
c . String ( http . StatusOK , `the body should be formB` )
} else {
...
}
}
为此,您可以使用 c.ShouldBindBodyWith
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func SomeHandler ( c * gin . Context ) {
objA := formA {}
objB := formB {}
// This reads c.Request.Body and stores the result into the context.
// 这个方法会读取 c.Request.Body 并将结果存储到context中。
if errA := c . ShouldBindBodyWith ( & objA , binding . Form ); errA == nil {
c . String ( http . StatusOK , `the body should be formA` )
// At this time, it reuses body stored in the context.
// 这时,它会重用存储在context中的请求体。
} else if errB := c . ShouldBindBodyWith ( & objB , binding . JSON ); errB == nil {
c . String ( http . StatusOK , `the body should be formB JSON` )
// 它还可以接受其他格式
} else if errB2 := c . ShouldBindBodyWith ( & objB , binding . XML ); errB2 == nil {
c . String ( http . StatusOK , `the body should be formB XML` )
} else {
...
}
}
c.ShouldBindBodyWith
在绑定之前将请求体存储到context中。这对性能有一些影响,所以如果您只需要调用一次绑定,就不应该使用这个方法。这个功能只适用于某些格式,如 JSON
、XML
、MsgPack
、ProtoBuf
。对于其他格式,如 Query
、Form
、FormPost
、FormMultipart
,可以多次调用 c.ShouldBind()
而不会对性能造成任何损害(参见 #1341 )。 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
const (
customerTag = "url"
defaultMemory = 32 << 20
)
type customerBinding struct {}
func ( customerBinding ) Name () string {
return "form"
}
func ( customerBinding ) Bind ( req * http . Request , obj any ) 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 any ) error {
if binding . Validator == nil {
return nil
}
return binding . Validator . ValidateStruct ( obj )
}
// 现在我们可以这样做!!!
// FormA 是一个外部类型,我们无法修改它的标签
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 {
...
}
...
}
}
http2 server push http.Pusher 只支持 go1.8+ 。请参阅 golang 博客 获取详细信息。
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
package main
import (
"html/template"
"log"
"net/http"
"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 {
// 使用 pusher.Push() 进行服务器推送
if err := pusher . Push ( "/assets/app.js" , nil ); err != nil {
log . Printf ( "Failed to push: %v" , err )
}
}
c . HTML ( http . StatusOK , "https" , gin . H {
"status" : "success" ,
})
})
// 在 https://127.0.0.1:8080 上监听和提供服务
r . RunTLS ( ":8080" , "./testdata/server.pem" , "./testdata/server.key" )
}
定义路由日志的格式 默认的路由日志格式如下:
1
2
3
[ 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)
如果你希望以指定格式(如 JSON、键值对或其他格式)记录这些信息,你可以使用 gin.DebugPrintRouteFunc
来定义日志格式。
在下面的示例中,我们使用标准日志包记录所有的路由,但你也可以使用适合你需求的其他日志工具。
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
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func main () {
r := gin . Default ()
gin . DebugPrintRouteFunc = func ( httpMethod , absolutePath , handlerName string , nuHandlers int ) {
log . Printf ( "endpoint %v %v %v %v\n" , httpMethod , absolutePath , handlerName , nuHandlers )
}
r . POST ( "/foo" , func ( c * gin . Context ) {
c . JSON ( http . StatusOK , "foo" )
})
r . GET ( "/bar" , func ( c * gin . Context ) {
c . JSON ( http . StatusOK , "bar" )
})
r . GET ( "/status" , func ( c * gin . Context ) {
c . JSON ( http . StatusOK , "ok" )
})
// 在 http://0.0.0.0:8080 上监听和提供服务
r . Run ()
}
设置和获取 cookie 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main () {
router := gin . Default ()
router . GET ( "/cookie" , 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 . Printf ( "Cookie value: %s \n" , cookie )
})
router . Run ()
}
不信任所有代理 gin 允许您指定要保存真实客户端IP的头部(如果有的话),并指定您信任的代理服务器(或直接客户端)来指定这些头部之一。
使用gin.Engine
上的SetTrustedProxies()
函数,可以指定客户端IP相关请求头部可信任的网络地址或网络CIDR。它们可以是IPv4地址、IPv4 CIDR、IPv6地址或IPv6 CIDR。
注意: 如果您没有使用上述函数指定受信任的代理服务器,默认情况下gin信任所有代理服务器,这是不安全的 。同时,如果您不使用任何代理服务器,可以通过使用Engine.SetTrustedProxies(nil)
来禁用此功能,然后Context.ClientIP()
将直接返回远程地址,避免一些不必要的计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 ) {
// 如果客户端是192.168.1.2,
// 则使用X-Forwarded-For头部来推断原始客户端IP,从该头部的可信任部分获取。
// 否则,直接返回客户端的真实IP。
fmt . Printf ( "ClientIP: %s\n" , c . ClientIP ())
})
router . Run ()
}
注意: 如果你正在使用 CDN 服务,你可以将 Engine.TrustedPlatform
设置为跳过 TrustedProxies 检查,它的优先级比 TrustedProxies 更高。
看下面的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main () {
router := gin . Default ()
// Use predefined header gin.PlatformXXX
// 使用预定义的头部 gin.PlatformXXX
router . TrustedPlatform = gin . PlatformGoogleAppEngine
// 或者设置自己的受信任代理服务的请求头部
// 不要将其设置为任何可疑的请求头部,这是不安全的
router . TrustedPlatform = "X-CDN-IP"
router . GET ( "/" , func ( c * gin . Context ) {
// 如果你设置了 TrustedPlatform,ClientIP() 将解析相应的头部并直接返回 IP
fmt . Printf ( "ClientIP: %s\n" , c . ClientIP ())
})
router . Run ()
}
测试 net/http/httptest
包是进行 HTTP 测试的首选方法。
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"
"github.com/gin-gonic/gin"
)
func setupRouter () * gin . Engine {
r := gin . Default ()
r . GET ( "/ping" , func ( c * gin . Context ) {
c . String ( http . StatusOK , "pong" )
})
return r
}
func main () {
r := setupRouter ()
r . Run ( ":8080" )
}
上述代码示例的测试:
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 ( http . MethodGet , "/ping" , nil )
router . ServeHTTP ( w , req )
assert . Equal ( t , http . StatusOK , w . Code )
assert . Equal ( t , "pong" , w . Body . String ())
}