/gin-demo

gin框架学习

Primary LanguageGo

gin-demo

gin框架学习

学习资料: gin项目地址 gin中文文档 Gin入门实战 文中demo

Gin运行流程

engine:实现了ServeHTTP接口的handler methodTree:根据http请求方法分别维护的路由树 routerGroup:将路由表分组,方便中间件统一处理 Context:Gin的上下文,在handler之间传递参数

示例代码

Router :路由规则定义

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

// SignupHandler : 处理用户注册请求
func SignupHandler(c *gin.Context) {
	c.Redirect(http.StatusFound, "http://"+c.Request.Host+"/static/view/signup.html")
}
// 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可简洁为

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())
	}
}

快速开始

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 .........")
}
➜  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.请求路由

多种请求

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请求几乎覆盖了所有种类的请求

[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

静态文件夹

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即可得到相应参数

{
    "id": "520",
    "name": "Sam"
}

2.获取请求参数

  • 获取get请求参数
  • 获取post请求参数
  • 获取body值
  • 获取参数绑定结构体

泛绑定

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参数

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()
}
~ 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内容

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()
}
~ 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,同时访问到同一个回调方法。

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)
	}

}
~ 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"这种会报错,解析不了。

person bind error%!(EXTRA *time.ParseError=parsing time "2008-00-08" as "2011-01-03": cannot parse "-00-08" as "1")

验证请求参数

  • 结构体验证
  • 自定义验证
  • 支持多语言错误信息

结构体验证

validate规则 binding条件满足

坑的记录:

type Person struct {
	Age     int    `form:"age"  binding:"required,gt=10"`
	Name    string `form:"name" binding:"required"`
	Address string `form:"address" binding:"required"`
}

正确的完整源码:

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)

}
~ 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}%        

自定义验证

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})
}

这里有个报错,我自己也还没弄明白,然而我看了下源码,上面的使用方法是正确的:

# command-line-arguments
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
// 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中间件
  • 自定义ip白名单中间件

Gin中间件

Logger日志 Recovery捕获panic

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())

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延展

  • 服务器优雅关停
  • 模版渲染
  • 自动证书配置

服务器关停

模版渲染

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()
}
~ 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

输出格式统一封装

自定义中间件日志打印

请求数据绑定结构体与校验