快速开始

依赖引入

1
go get -u github.com/gin-gonic/gin
1
import "github.com/gin-gonic/gin"

创建服务

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

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

func main() {
	engine := gin.Default()
	engine.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello World!",
		})
	})
	engine.Run(":52100")
}

框架基础

请求参数

路由参数

绑定路由参数时,使用:作为前缀,例如:paramName

通过context.Param("paramName")获取参数

 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
package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
)

func main() {
	e := gin.Default()
	e.GET("/findUser/:username/:userid", FindUser)
	e.GET("/downloadFile/*filepath", UserPage)

	log.Fatalln(e.Run(":8080"))
}

// 命名参数示例
func FindUser(c *gin.Context) {
	username := c.Param("username")
	userid := c.Param("userid")
	c.String(http.StatusOK, "username is %s\n userid is %s", username, userid)
}

// 路径参数示例
func UserPage(c *gin.Context) {
	filepath := c.Param("filepath")
	c.String(http.StatusOK, "filepath is  %s", filepath)
}

请求参数

context.DefaultQuery("username", "defaultUser")获取请求参数,如果参数不存在则返回默认值

context.Query("username")获取请求参数,如果参数不存在则返回空字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
)

func main() {
	e := gin.Default()
	e.GET("/findUser", FindUser)
	log.Fatalln(e.Run(":8080"))
}

func FindUser(c *gin.Context) {
	username := c.DefaultQuery("username", "defaultUser")
	userid := c.Query("userid")
	c.String(http.StatusOK, "username is %s\nuserid is %s", username, userid)
}

表单参数

var form map[string]string定义表单类型

context.ShouldBind(&form)绑定表单参数

context.PostForm("FormName")获取表单参数

 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 (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	e := gin.Default()
	e.POST("/register", RegisterUser)
	e.POST("/update", UpdateUser)
	e.Run(":8080")
}

func RegisterUser(c *gin.Context) {
	username := c.PostForm("username")
	password := c.PostForm("password")
	c.String(http.StatusOK, "successfully registered,your username is [%s],password is [%s]", username, password)
}

func UpdateUser(c *gin.Context) {
	var form map[string]string
	c.ShouldBind(&form)
	c.String(http.StatusOK, "successfully update,your username is [%s],password is [%s]", form["username"], form["password"])
}

数据解析

解析函数

gin的数据解析依靠Bind()ShouldBind()方法

Bind()方法解析数据,如果解析失败,会返回错误

ShouldBind()方法解析数据,如果解析失败,不会返回错误,而是返回空值

也可以自行制定绑定的方法,如BindWith,ShouldBindWith(),MustBindWith()等方法指定解析方式

支持以下解析方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
)

JSON解析

 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
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

type LoginUser struct {
	Username string `bind:"required" json:"username" form:"username" uri:"username"`
	Password string `bind:"required" json:"password" form:"password" uri:"password"`
}

func main() {
	e := gin.Default()
	e.POST("/loginWithJSON", Login)
	e.POST("/loginWithForm", Login)
	e.GET("/loginWithQuery/:username/:password", Login)
	e.Run(":8080")
}

func Login(c *gin.Context) {
	var login LoginUser
	// 使用ShouldBind来让gin自动推断
	if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
		c.String(http.StatusOK, "login successfully !")
	} else {
		c.String(http.StatusBadRequest, "login failed !")
	}
	fmt.Println(login)
}

多次绑定

基础绑定函数只能绑定一次,如果需要多次绑定,可以使用context.ShouldBindBodyWith()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// 读取 c.Request.Body 并将结果存入上下文。
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// 这时, 复用存储在上下文中的 body。
}
if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// 可以接受其他格式
}
if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`)
} 
}

数据校验

使用结构体的tag进行数据校验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type LoginUser struct {
Username string `binding:"required"  json:"username" form:"username" uri:"username"`
Password string `binding:"required" json:"password" form:"password" uri:"password"`
}

func main() {
e := gin.Default()
e.POST("/register", Register)
log.Fatalln(e.Run(":8080"))
}

func Register(ctx *gin.Context) {
newUser := &LoginUser{}
if err := ctx.ShouldBind(newUser); err == nil {
ctx.String(http.StatusOK, "user%+v", *newUser)
} else {
ctx.String(http.StatusBadRequest, "invalid user,%v", err)
}
}

数据响应

HTML渲染

使用engine,LoadHTMLFiles()指定静态文件路径

使用context.HTML()渲染HTML文件

路径是用go.mod所在路径开始计算

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
e := gin.Default()
// 加载HTML文件,也可以使用Engine.LoadHTMLGlob()
e.LoadHTMLFiles("index.html")
e.GET("/", Index)
log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
}

响应函数

 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
// 使用Render写入响应头,并进行数据渲染
func (c *Context) Render(code int, r render.Render)

// 渲染一个HTML模板,name是html路径,obj是内容
func (c *Context) HTML(code int, name string, obj any)

// 以美化了的缩进JSON字符串进行数据渲染,通常不建议使用这个方法,因为会造成更多的传输消耗。
func (c *Context) IndentedJSON(code int, obj any)

// 安全的JSON,可以防止JSON劫持,详情了解:https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)

// JSONP方式进行渲染
func (c *Context) JSONP(code int, obj any)

// JSON方式进行渲染
func (c *Context) JSON(code int, obj any)

// JSON方式进行渲染,会将unicode码转换为ASCII码
func (c *Context) AsciiJSON(code int, obj any)

// JSON方式进行渲染,不会对HTML特殊字符串进行转义
func (c *Context) PureJSON(code int, obj any)

// XML方式进行渲染
func (c *Context) XML(code int, obj any)

// YML方式进行渲染
func (c *Context) YAML(code int, obj any)

// TOML方式进行渲染
func (c *Context) TOML(code int, obj interface{})

// ProtoBuf方式进行渲染
func (c *Context) ProtoBuf(code int, obj any)

// String方式进行渲染
func (c *Context) String(code int, format string, values ...any)

// 重定向到特定的位置
func (c *Context) Redirect(code int, location string)

// 将data写入响应流中
func (c *Context) Data(code int, contentType string, data []byte)

// 通过reader读取流并写入响应流中
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// 高效的将文件写入响应流
func (c *Context) File(filepath string)

// 以一种高效的方式将fs中的文件流写入响应流
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// 以一种高效的方式将fs中的文件流写入响应流,并且在客户端会以指定的文件名进行下载
func (c *Context) FileAttachment(filepath, filename string)

// 将服务端推送流写入响应流中
func (c *Context) SSEvent(name string, message any)

// 发送一个流响应并返回一个布尔值,以此来判断客户端是否在流中间断开
func (c *Context) Stream(step func (w io.Writer) bool) bool

异步处理

使用goroutine进行处理需要控制安全作用范围,因此需要先使用Copy()函数创建一个Context副本

创建协程后要在函数体后加上()以立即执行

1
func (c *Context) Copy() *Context
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
e := gin.Default()
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}

func Hello(c *gin.Context) {
ctx := c.Copy()
go func() {
// 子协程应该使用Context的副本,不应该使用原始Context
log.Println("异步处理函数: ", ctx.HandlerNames())
}()
log.Println("接口处理函数: ", c.HandlerNames())
c.String(http.StatusOK, "hello")
}

文件传输

使用context.File()获取文件

使用context.MultipartForm()获取多文件

使用context.SaveUploadedFile()保存文件

使用context.FileAttachment(filename, filename)返回对应文件下载

文件传输最大内存通过Engine.MaxMultipartMemory()设置

单文件传输

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
log.Fatalln(e.Run(":8080"))
}

func uploadFile(ctx *gin.Context) {
// 获取文件
file, err := ctx.FormFile("file")
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// 保存在本地
err = ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// 返回结果
ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}

多文件传输

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
log.Fatalln(e.Run(":8080"))
}

func uploadFiles(ctx *gin.Context) {
// 获取gin解析好的multipart表单
form, _ := ctx.MultipartForm()
// 根据键值取得对应的文件列表
files := form.File["files"]
// 遍历文件列表,保存到本地
for _, file := range files {
err := ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "upload failed")
return
}
}
// 返回结果
ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}

文件下载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
e.GET("/download/:filename", download)
log.Fatalln(e.Run(":8080"))
}

func download(ctx *gin.Context) {
// 获取文件名
filename := ctx.Param("filename")
// 返回对应文件
ctx.FileAttachment(filename, filename)
}

路由管理

路由组

使用Engine.Group()创建路由组.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
e := gin.Default()
v1 := e.Group("v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
}

异常路由

使用Engine.NoRoute()创建404路由

使用Engine.NoMethod()创建405路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
e := gin.Default()
// 需要将其设置为true
e.HandleMethodNotAllowed = true
v1 := e.Group("/v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
e.NoRoute(func(context *gin.Context) {
context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
})
// 注册处理器
e.NoMethod(func(context *gin.Context) {
context.String(http.StatusMethodNotAllowed, "method not allowed")
})
log.Fatalln(e.Run(":8080"))
}

重定向

使用Engine.Redirect()创建重定向路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
e := gin.Default()
e.GET("/", Index)
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/hello")
}

func Hello(c *gin.Context) {
c.String(http.StatusOK, "hello")
}

中间件

全局中间件

一个函数返回值为gin.HandlerFunc,可以定义中间件。

使用Engine.Use()注册中间件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func GlobalMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("全局中间件被执行...")
    }
}

func main() {
e := gin.Default()
// 注册全局中间件
e.Use(GlobalMiddleware())
v1 := e.Group("/v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}

局部中间件

只需要在路由参数传入即可定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func main() {
   e := gin.Default()
   // 注册全局中间件
   e.Use(GlobalMiddleware())
   // 注册路由组局部中间件
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // 注册单个路由局部中间件
      v2.POST("/update", LocalMiddleware(), Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

责任链

通过context.Next()调用下一个对应的中间件,执行完毕后返回并执行后面的业务内容

服务配置

HTTP配置

可以通过net/http创建Server配置服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
   router := gin.Default()
   server := &http.Server{
      Addr:           ":8080",
      Handler:        router,
      ReadTimeout:    10 * time.Second,
      WriteTimeout:   10 * time.Second,
      MaxHeaderBytes: 1 << 20,
   }
   log.Fatal(server.ListenAndServe())
}

静态资源配置

relativePath是映射到网页URL上的相对路径,root是文件在项目中的实际路径

1
2
3
4
5
6
7
8
// 加载某一静态文件夹 
func (group *RouterGroup) Static(relativePath, root string) IRoutes

// 加载某一个fs
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes

// 加载某一个静态文件
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

跨域配置

原则就是通过全局中间件拦截OPTIONS请求,设置响应头。

 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
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // 生产环境中的服务端通常都不会填 *,应当填写指定域名
         c.Header("Access-Control-Allow-Origin", origin)
         // 允许使用的HTTP METHOD
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // 允许使用的请求头
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // 允许客户端访问的响应头
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // 是否需要携带认证信息 Credentials 可以是 cookies、authorization headers 或 TLS client certificates 
         // 设置为true时,Access-Control-Allow-Origin不能为 *
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // 放行OPTION请求,但不执行后续方法
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // 放行
      c.Next()
   }
}

会话控制

 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
import (
"fmt"

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

func main() {

router := gin.Default()

router.GET("/cookie", func(c *gin.Context) {

// 获取对应的cookie
cookie, err := c.Cookie("gin_cookie")

if err != nil {
cookie = "NotSet"
// 设置cookie 参数:key,val,存在时间,目录,域名,是否允许他人通过js访问cookie,仅http
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}

fmt.Printf("Cookie value: %s \n", cookie)
})

router.Run()
}

session

gin 默认不支持session,需要使用第三方中间件。

1
go get github.com/gin-contrib/sessions
 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()
   // 创建基于Cookie的存储引擎
   store := cookie.NewStore([]byte("secret"))
   // 设置Session中间件,mysession即session名称,也是cookie的名称
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // 初始化session
      session := sessions.Default(c)
      var count int
      // 获取值
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // 设置
      session.Set("count", count)
      // 保存
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

日志管理

日志写入文件

自带日志支持写入多个文件,但内容相同,默认不会将请求日志写入文件,可以自定义中间将将请求日志写入文件

多文件写入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	e := gin.Default()
    // 关掉控制台颜色
	gin.DisableConsoleColor()
    // 创建两个日志文件
	log1, _ := os.Create("info1.log")
	log2, _ := os.Create("info2.log")
    // 同时记录进两个日志文件
	gin.DefaultWriter = io.MultiWriter(log1, log2)
	e.GET("/hello", Hello)
	log.Fatalln(e.Run(":8080"))
}

带请求日志文件写入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
   e := gin.Default()
   gin.SetMode(gin.DebugMode)
   gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
      if gin.IsDebugging() {
         log.Printf("路由 %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
      }
   }
   e.GET("/hello", Hello)
   log.Fatalln(e.Run(":8080"))
}

路由调试日志格式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
   e := gin.Default()
   gin.SetMode(gin.DebugMode)
   gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
      if gin.IsDebugging() {
         log.Printf("路由 %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
      }
   }
   e.GET("/hello", Hello)
   log.Fatalln(e.Run(":8080"))
}