Skip to content

bddbnet/gin_readme_zh

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 

Repository files navigation

Build Status codecov Go Report Card GoDoc Join the chat at https://gitter.im/gin-gonic/gin Open Source Helpers

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

Gin console logger

项目地址Gin Web Framework

目录

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

  • Zero allocation router.
  • Still the fastest http router and framework. From routing to writing.
  • Complete suite of unit tests
  • Battle tested
  • API frozen, new releases will not break your code.

Start using it

开始使用

  1. 下载并安装它:
$ go get github.com/gin-gonic/gin
  1. 将其导入到您的代码中:
import "github.com/gin-gonic/gin"
  1. (可选的) 导入 net/http. 如果您要使用诸如http.StatusOK的常量.
import "net/http"

Use a vendor tool like Govendor

使用包管理工具 Govendor

  1. 使用go get获取 govendor
$ go get github.com/kardianos/govendor
  1. 创建并进入你的项目文件夹
$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
  1. 初始化你的项目并添加gin
$ govendor init
$ govendor fetch github.com/gin-gonic/gin@v1.2
  1. 复制一个初始模板到你的项目中
$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go
  1. 运行你的项目
$ 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头推断绑定数据的类型. 如果你能够确认绑定数据的类型, 可以使用 MustBindWithShouldBindWith绑定数据.

你可以指定需要绑定数据的字段. 如果这个字段存在这个 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": "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 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.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).

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 Sourcegraph

Awesome project lists using Gin web framework.

  • drone: Drone is a Continuous Delivery platform built on Docker, written in Go
  • gorush: A push notification server written in Go.

About

Gin web框架 使用说明中文版本

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published