学习资料:gin项目地址 gin中文文档 Gin入门实战 文中demo
Gin运行流程
engine
:实现了ServeHTTP接口的handlermethodTree
:根据http请求方法分别维护的路由树routerGroup
:将路由表分组,方便中间件统一处理Context
:Gin的上下文,在handler之间传递参数
示例代码 Router :路由规则定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import ( hdl "filestore-server/handler" "github.com/gin-gonic/gin" ) // Router :路由规则定义 func Router() *gin.Engine { // gin framework router := gin.Default() // 静态资源处理 router.Static("/static/" , "./static" ) // 定义接口 router.GET("/user/signup" , hdl.SignupHandler) router.POST("/user/signup" , hdl.DoSignupHandler) return router }
handler
1 2 3 4 // SignupHandler : 处理用户注册请求 func SignupHandler(c *gin.Context) { c.Redirect(http.StatusFound, "http://" +c.Request.Host+"/static/view/signup.html" ) }
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 // DoSignupHandler : 处理用户注册请求 func DoSignupHandler(c *gin.Context) { username := c.Request.FormValue("username" ) passwd := c.Request.FormValue("password" ) if len(username) < 3 || len(passwd) < 5 { c.JSON(http.StatusOK, gin.H{ "msg" : "Invalid parameter" , }) return } // 对密码进行加盐及取Sha1值加密 encPasswd := util.Sha1([]byte(passwd + pwdSalt)) // 将用户信息注册到用户表中 suc := dblayer.UserSignup(username, encPasswd) if suc { c.JSON(http.StatusOK, gin.H{ "code" : 0, "msg" : "注册成功" , "data" : nil, "forward" : "/user/signin" , }) } else { c.JSON(http.StatusOK, gin.H{ "code" : 0, "msg" : "注册失败" , "data" : nil, }) } }
最终main.go可简洁为
1 2 3 4 5 6 7 8 9 10 func main () { // gin framework router := route.Router() // 启动服务并监听端口 err := router.Run(config.UploadServiceHost) if err != nil { fmt.Printf("Failed to start server, err:%s\n" , err.Error()) } }
快速开始 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 package main import ( "fmt" "github.com/gin-gonic/gin" ) func main () { r := gin.New() r.Use(gin.Logger()) r.Use(gin.Recovery()) r.GET("first" , func(c *gin.Context) { fmt.Println("first ........." ) c.JSON(200, gin.H{ "message" : "pong" , }) }) authorized := r.Group("/try" ) authorized.POST("/second" , second) authorized.POST("/third" , third) // 嵌套路由组 testing := authorized.Group("testing" ) testing.GET("/forth" , fourth) // 监听并在 0.0.0.0:8080 上启动服务 r.Run(":8080" ) } func second(c *gin.Context) { fmt.Println("second ........." ) } func third(c *gin.Context) { fmt.Println("third ........." ) } func fourth(c *gin.Context) { fmt.Println("fourth ........." ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ➜ gin-demo git:(master) ✗ lsof -i tcp:8080 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME QQ 327 samtake 29u IPv4 0xfdcbb6b79716f31f 0t0 TCP huangloshansmbp:50202->157.255.13.190:http-alt (ESTABLISHED) QQ 327 samtake 48u IPv4 0xfdcbb6b79716f31f 0t0 TCP huangloshansmbp:50202->157.255.13.190:http-alt (ESTABLISHED) Google 420 samtake 26u IPv4 0xfdcbb6b793e58fb7 0t0 TCP localhost:51492->localhost:http-alt (ESTABLISHED) Postman 66875 samtake 67u IPv4 0xfdcbb6b778668ca7 0t0 TCP localhost:51499->localhost:http-alt (ESTABLISHED) main 67553 samtake 3u IPv6 0xfdcbb6b793ea9b8f 0t0 TCP *:http-alt (LISTEN) main 67553 samtake 7u IPv6 0xfdcbb6b793eac3cf 0t0 TCP localhost:http-alt->localhost:51492 (ESTABLISHED) main 67553 samtake 8u IPv6 0xfdcbb6b7912a55cf 0t0 TCP localhost:http-alt->localhost:51499 (ESTABLISHED) ➜ gin-demo git:(master) ✗ kill -9 67553 ➜ gin-demo git:(master) ✗ go run start/main.go [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /first --> main.main.func1 (3 handlers) [GIN-debug] POST /try/second --> main.second (3 handlers) [GIN-debug] POST /try/third --> main.third (3 handlers) [GIN-debug] GET /try/testing/forth --> main.fourth (3 handlers) [GIN-debug] Listening and serving HTTP on :8080 first ......... [GIN] 2020/02/25 - 11:51:19 | 200 | 176.613µs | 127.0.0.1 | GET /first
1.请求路由 多种请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func main () { r := gin.Default() r.GET("get" , func(c *gin.Context) { c.String(200, "get" ) }) r.POST("post" , func(c *gin.Context) { c.String(200, "post" ) }) r.DELETE("delete" , func(c *gin.Context) { c.String(200, "delete" ) }) r.Any("/any" , func(c *gin.Context) { c.String(200, "any" ) }) r.Run() }
给我们创建的any请求几乎覆盖了所有种类的请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [GIN-debug] GET /get --> main.main.func1 (3 handlers) [GIN-debug] POST /post --> main.main.func2 (3 handlers) [GIN-debug] DELETE /delete --> main.main.func3 (3 handlers) [GIN-debug] GET /any --> main.main.func4 (3 handlers) [GIN-debug] POST /any --> main.main.func4 (3 handlers) [GIN-debug] PUT /any --> main.main.func4 (3 handlers) [GIN-debug] PATCH /any --> main.main.func4 (3 handlers) [GIN-debug] HEAD /any --> main.main.func4 (3 handlers) [GIN-debug] OPTIONS /any --> main.main.func4 (3 handlers) [GIN-debug] DELETE /any --> main.main.func4 (3 handlers) [GIN-debug] CONNECT /any --> main.main.func4 (3 handlers) [GIN-debug] TRACE /any --> main.main.func4 (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080
静态文件夹 1 2 3 4 5 6 7 8 9 func main () { r := gin.Default() // 静态文件夹绑定+路由,有两种写法: r.Static("/assets" , "./assets" ) r.StaticFS("/static" , http.Dir("static" )) //又或者路由+资源 r.StaticFile("/favicon.ico" , "/favicon.ico" ) }
go build -o router_static && ./router_static
这需要在routerstatic文件夹下运行,不然找不到资源文件,最后访问http://localhost:8080/assets/a.html
以及http://localhost:8080/static/b.html
测试即可。
参数作为url Get请求http://localhost:8080/Sam/520
即可得到相应参数
1 2 3 4 { "id" : "520" , "name" : "Sam" }
2.获取请求参数
获取get请求参数
获取post请求参数
获取body值
获取参数绑定结构体
泛绑定 1 2 3 4 5 6 7 8 func main () { r := gin.Default() r.GET("/user/*action" , func(c *gin.Context) { //设置获取name和id的参数 c.String(200, "泛绑定~" ) }) r.Run() }
所有user前缀的请求都能请求到:http://localhost:8080/user/<XXXX>
获取get参数 1 2 3 4 5 6 7 8 9 10 11 func main () { r := gin.Default() r.GET("/testGetParam" , func(c *gin.Context) { //设置获取name和id的参数 param1 := c.Query("parm1" ) param2 := c.Query("parm2" ) c.String(http.StatusOK, "%s, %s" , param1, param2) }) r.Run() }
1 2 3 4 ➜ ~ curl -X GET 'http://127.0.0.1:8080/testGetParam?parm1=11111' 11111, % ➜ ~ curl -X GET 'http://127.0.0.1:8080/testGetParam?parm1=11111&parm2=2222' 11111, 2222%
获取body内容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { r := gin.Default() r.POST("/parmBody" , func(c *gin.Context) { bodyBytes, err := ioutil.ReadAll(c.Request.Body) if err != nil { c.String(http.StatusBadRequest, err.Error()) c.Abort() } c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) //重新回写到缓存才能拿到parm1 parm2 parm1 := c.PostForm("parm1" ) parm2 := c.PostForm("parm2" ) c.String(http.StatusOK, "%s %s %s" , parm1, parm2, string(bodyBytes)) }) r.Run() }
1 2 3 4 ➜ ~ curl -X POST 'http://127.0.0.1:8080/parmBody' -d 'parm1=value1&parm2=value2' value1 value2 parm1=value1&parm2=value2% ➜ ~ curl -X POST 'http://127.0.0.1:8080/parmBody' -d '{"parm1":"value1","parm2":"value2"}' {"parm1" :"value1" ,"parm2" :"value2" }
获取bind参数 同时响应post和get,同时访问到同一个回调方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Person struct { Name string `form:"name" ` //设置tag `form`可以由参数转变成结构体 Address string `form:"address" ` Birthday time.Time `form:"birthday" time_format:"2006-01-03" ` } func main () { r := gin.Default() r.GET("/test" , testHandler) r.POST("/test" , testHandler) r.Run() } func testHandler(c *gin.Context) { var person Person //根据请求content-type来作不同的binding操作 if err := c.ShouldBind(&person); err == nil { c.String(200, "%v" , person) } else { c.String(200, "person bind error" , err) } }
1 2 3 4 5 6 7 8 9 10 ➜ ~ curl -X GET 'http://127.0.0.1:8080/test?name=samtake&adress=gd' {samtake 0001-01-01 00:00:00 +0000 UTC} ➜ ~ curl -X GET 'http://127.0.0.1:8080/test?name=samtake&address=gd' {samtake gd 0001-01-01 00:00:00 +0000 UTC}% ➜ ~ curl -X POST 'http://127.0.0.1:8080/test?name=samtake&address=gd&birthday=2008-09-09' {samtake gd 2008-09-01 09:00:00 +0800 CST}% ➜ ~ curl -X POST 'http://127.0.0.1:8080/test' -d 'name=samtake&address=gd&birthday=2008-09-09' {samtake gd 2008-09-01 09:00:00 +0800 CST}% ➜ ~ curl -H "Content-Type:application/json" -X POST "http://127.0.0.1:8080/test" -d '{"name":"wang"}' {wang 0001-01-01 00:00:00 +0000 UTC}%
这里有个坑:如果时间格式写成像time_format:"2006-01-03"
这种会报错,解析不了。
1 person bind error%!(EXTRA *time.ParseError=parsing time "2008-00-08" as "2011-01-03" : cannot parse "-00-08" as "1" )
验证请求参数
结构体验证 validate规则 binding条件满足
坑的记录:
1 2 3 4 5 type Person struct { Age int `form:"age" binding:"required,gt=10" ` Name string `form:"name" binding:"required" ` Address string `form:"address" 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 type Person struct { Age int `form:"Age" binding:"required,gt=10" ` Name string `form:"Name" binding:"required" ` Address string `form:"Address" binding:"required" ` } func main () { r := gin.Default() r.GET("/test" , testHandler) r.Run() } func testHandler(c *gin.Context) { var person Person //根据请求content-type来作不同的binding操作 if err := c.ShouldBind(&person); err != nil { c.String(500, "%v" , err) c.Abort() return } c.String(200, "%v" , person) }
1 2 3 4 5 6 ➜ ~ curl -X GET "http://127.0.0.1:8080/test?age=19&name=samtake&address=gd" Key: 'Person.Age' Error:Field validation for 'Age' failed on the 'required' tag Key: 'Person.Name' Error:Field validation for 'Name' failed on the 'required' tag Key: 'Person.Address' Error:Field validation for 'Address' failed on the 'required' tag% ➜ ~ curl -X GET "http://127.0.0.1:8080/test?Age=19&Name=samtake&Address=gd" {19 samtake gd}%
自定义验证 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 package main import ( "reflect" "time" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "gopkg.in/go-playground/validator.v9" ) type Booking struct { CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-01"` CheckOut time.Time `form:"check_out" binding:"required,gtfield=checkIn" time_format:"2006-01-01"` } func customFunc(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 date.Unix() > today.Unix() { return true } } return false } func main() { r := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("bookabledate", customFunc) } r.GET("/bookable", testHandler) r.Run() } func testHandler(c *gin.Context) { var b Booking if err := c.ShouldBind(&b); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{"message": "ok!", "booking": b}) }
这里有个报错,我自己也还没弄明白,然而我看了下源码,上面的使用方法是正确的:
1 2 3 4 validCustom/validCustom.go:30:24: cannot use customFunc (type func(*validator.Validate, reflect.Value, reflect.Value, reflect.Value, reflect.Typ e, reflect.Kind, string) bool) as type validator.Func in argument to v.RegisterValidation `
1 2 3 4 5 6 7 8 // RegisterValidation adds a validation with the given tag // // NOTES: // - if the key already exists, the previous validation function will be replaced. // - this method is not thread-safe it is intended that these all be registered prior to any validation func (v *Validate) RegisterValidation(tag string, fn Func) error { return v.RegisterValidationCtx(tag, wrapFunc(fn)) }
支持多语言错误信息 中间件
Gin中间件 Logger
日志Recovery
捕获panic
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { f, _ := os.Create("gin.log" ) gin.DefaultWriter = io.MultiWriter(f) gin.DefaultErrorWriter = io.MultiWriter(f) r := gin.New() r.Use(gin.Logger(),gin.Recovery()) r.GET("/test" , func(c *gin.Context) { name := c.DefaultQuery("name" , "default_name" ) c.String(200, "%s" , name) }) r.Run() }
白名单 r.User(IPAuthMiddleware())
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 func IPAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { ipList := []string{ "127.0.0.1" , } flag := false clientIP := c.ClientIP() for _, host := range ipList { if clientIP == host { flag = true break } } if !flag { c.String(401, "%s,not in iplist" , clientIP) c.Abort() } } } func main () { r := gin.New() r.Use(IPAuthMiddleware()) r.Use(gin.Logger(), gin.Recovery()) r.GET("/test" , func(c *gin.Context) { c.String(200, "%s" , "hello test" ) }) r.Run() }
Gin延展
服务器关停
模版渲染 1 2 3 4 5 6 7 8 9 10 func main () { r := gin.Default() r.LoadHTMLGlob("template/*" ) r.GET("/index" , func(c *gin.Context) { c.HTML(200, "index.html" , gin.H{ "title" : "index.html" , }) }) r.Run() }
1 2 3 4 5 6 7 ➜ ~ curl -X GET "http://127.0.0.1:8080/index" <html> <h1> index.html </h1> </html>% ➜ ~
自动证书配置 脚手架 项目地址:gin_scaffold golang_common vue-admin
轻量级Golang类库 GORM:https://gorm.io/zh_CN/ redigo:https://godoc.org/github.com/gomodule/redigo/redis
├── README.md ├── conf 配置文件夹 │ └── dev │ ├── base.toml │ ├── mysql_map.toml │ └── redis_map.toml ├── controller 控制器 │ └── demo.go ├── dao DB数据访问层 │ └── demo.go ├── dto Bind结构体层 │ └── demo.go ├── gin_scaffold.inf.log info日志 ├── gin_scaffold.wf.log warning日志 ├── go.mod go module管理文件 ├── go.sum ├── main.go ├── middleware 中间件层 │ ├── panic.go │ ├── response.go │ ├── token_auth.go │ └── translation.go ├── public 公共文件 │ ├── log.go │ ├── mysql.go │ └── validate.go ├── router 路由层 │ ├── httpserver.go │ └── route.go └── tmpl
输出格式统一封装 自定义中间件日志打印 请求数据绑定结构体与校验