快速开始#
依赖引入#
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()
}
}
|
会话控制#
cookie#
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"))
}
|