前言

Gin是一款基于Go语言开发的轻量级、高性能WebHTTP框架,API风格兼容Martini框架,但性能实现跨越式提升。

Gin底层采用httprouter路由库,路由执行速度相较传统框架提升近40倍,是Go生态中追求高性能、高并发、高效率的首选Web框架。

作为目前 Go 语言社区最流行、使用最广泛的Web 框架。

Gin GitHub 上收获32K+ Star,生态成熟、中文文档完善、学习成本极低,是构建微服务、API 服务、后台管理系统的主流选择。

Gin依赖包下载

D:\learning-gin>set GOPROXY=https://goproxy.cn
-------------------------------------------------------
D:\learning-gin>go get -u github.com/gin-gonic/gin
go: google.golang.org
/protobuf upgrade => v1.25.0 go: gopkg.in/yaml.v2 upgrade => v2.4.0 go: github.com/golang/protobuf upgrade => v1.4.3 go: github.com/ugorji/go/codec upgrade => v1.2.1 go: golang.org/x/sys upgrade => v0.0.0-20201211090839-8ad439b19e0f go: github.com/json-iterator/go upgrade => v1.1.10 go: github.com/modern-go/reflect2 upgrade => v1.0.1 go: github.com/go-playground/validator/v10 upgrade => v10.4.1 go: github.com/modern-go/concurrent upgrade => v0.0.0-20180306012644-bacd9c7ef1dd go: golang.org/x/crypto upgrade => v0.0.0-20201208171446-5f87f3452ae9

 启动Gin服务器

package main

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

func index(c *gin.Context) {
    //返回json类型的数据,h=type H map[string]interface{}
    c.JSON(200, gin.H{"msg": "您好呀!"},
    )
}

func main() {
    //定义1个默认路由(基于httprouter的)
    router := gin.Default()
    //增加url
    router.GET("/index", index)
    //server段开始linsten运行
    router.Run("127.0.0.1:8000")

}

请求头

// 获取请求头 Origin
c.GetHeader("X-Token")
//设置请求头
c.Request.Header.Set("X-Token", "新值")

响应头

// 获取响应头
c.Writer.Header().Get("Access-Control-Allow-Origin")
// 设置响应头
c.Writer.Header().Set("Access-Control-Allow-Origin", "...")

请求体

我们可以通过gin的context获取到客户端请求携带的url参数、form表单、json数据、文件等。

package main

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


type user struct {
    Name string `json:"name"`
    City string `json:"city"`
}
var person=&user{}
//从url获取参数
func urlData(c *gin.Context) {
    //name:=c.Query("name") //获取url参数,获取不到获取空字符串
    name:=c.DefaultQuery("name","zhanggen") ////获取url参数,获取不到获取默认!
    city:=c.DefaultQuery("city","bj")
    person.Name=name
    person.City=city
    c.JSON(200,person)
}

//从form表单中获取数据
func formData(c *gin.Context) {
    c.PostForm("name")
    person.Name=c.DefaultPostForm("name","Martin")
    person.City=c.DefaultPostForm("city","London")
    c.JSON(200,person)
}

//获取url地址参数
func pathData(c *gin.Context){
    person.City=c.Param("city")
    person.Name=c.Param("name")
    c.JSON(200,*person)
}
//获取json数据
func jsonData(c *gin.Context){
    c.Bind(person)
    fmt.Println("-----------------",*person)
    c.JSON(200,person)
}


func main()  {
    r:=gin.Default()
    //http://127.0.0.1:8001/user?name=zhanggen&city=beijing
    r.GET("/user",urlData)
    r.POST("/user",formData)
    //http://127.0.0.1:8001/user/bj/zhanggen
    r.GET("/user/:city/:name",pathData)
    r.POST("/user/json/",jsonData)
    r.Run(":8001")
}

Gin shouldBind

默认情况下,我们需要根据客户端请求的content-type,在后端使用不同的方式获取客户端请求参数。

获取请求参还需要c.Query、c.PostForm、c.Bind、C.Param,这也太麻烦了~

shouldBind可帮助我们根据客户端request的content-type,把requestBody中数据自动绑定到后台Struct的字段,但不支持重复绑定。

BindXXX:会自动返回信息,输入无效时,在header写入状态码=400。

ShouldBindXXX:返回消息,输入无效时,不会在header写入400状态码,这时候可以自定义返回信息,在使用上相对来说更加灵活。

ShouldBindWith:

在gin 1.4 之前,重复使用ShouldBind绑定会报错EOF。

gin 1.4 之后官方提供了一个 ShouldBindBodyWith 的方法,可以支持重复绑定,原理就是将body的数据缓存了下来,但是二次取数据的时候还是得用 ShouldBindBodyWith 才行,直接用 ShouldBind 还是会报错的。

package main

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

//0.contentType对应ShouldBind对应的结构体
type UserInfo struct {
	Username string `form:"username" json:"username"`
	Password string `form:"password" json:"password"`
}

func index(c *gin.Context) {
	requestMethod := c.Request.Method
	//1.声明1个值类型uerinfo类型的变量u
	var user UserInfo
	//2.把客户端request请求的参数和后端的结合体字段进行绑定
	err := c.ShouldBind(&user)
	if err != nil {
		c.JSON(400, gin.H{"err": err.Error()})
		return
	}
	//3.可以通过反射的方式,根据客户端request的contentType自动获取数据了
	if requestMethod == "GET" {
		fmt.Println(user)
		c.HTML(200, "index.html", gin.H{})
	}
	if requestMethod == "POST" {
		fmt.Println(user)
		c.JSON(200, gin.H{"data": "postOkay"})
	}

}

func main() {
	router := gin.Default()
	router.Static("/static", "./static")
	router.LoadHTMLGlob("templates/*")
	router.GET("/user", index)
	router.POST("/user", index)
	router.Run(":8002")
}

自定义验证的错误信息

对前端传入的参数进行校验,当验证不通过时,会给出错误的信息。

package main

import (
    "fmt"
    "reflect"

    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
)


type LoginDto struct {
    Username string `json:"username" binding:"required" msg:"用户名不能为空"`
    Password string `json:"password" binding:"min=3,max=6" msg:"密码长度不能小于3大于6"`
    Email    string `json:"email" binding:"email" msg:"邮箱地址格式不正确"`
}

func GetValidMsg(err error, obj interface{}) string {
    fmt.Println("sherlock")
    getObj := reflect.TypeOf(obj)
    if errs, ok := err.(validator.ValidationErrors); ok {
        for _, e := range errs {
            if f, exist := getObj.Elem().FieldByName(e.Field()); exist {
                return f.Tag.Get("msg")
            }
        }
    }
    return err.Error()
}

func main() {
    r := gin.Default()

    r.POST("/login", func(c *gin.Context) {
        userDto := &LoginDto{}
        if err := c.ShouldBindJSON(userDto); err != nil {
            c.JSON(200, gin.H{
                "status":  "err",
                "message": GetValidMsg(err, userDto),
            })

        } else {
            c.JSON(200, gin.H{
                "status":  "change",
                "message": userDto,
            })
        }
    })
    r.Run(":8080")
}

Gin response

web开发过程中,大型项目会采用MVVM(前后端分离)的架构,小型项目会采用MTV(模板渲染)的架构。

疏通同归其目的都是完成数据驱动视图,不同的是数据驱动视图的地方不一样。

貌似web开发玩得就是这6个字,数据----》 驱动-----》视图。空谈误国,怎么才能更好的驱动视图才是关键。

MTV模式(模板渲染):后端使用模板语法也就是字符串替换的方式,在后端直接完成数据和HTML的渲染,直接返回给客户端。

MVVM(前后端分离架构):后端返回json数据,前端使用axios/ajax的方式获取到数据,使用vue等前端框架完成数据到HTML的渲染。

 

1.RESTful API

只要API程序遵循了REST风格,那就可以称其为RESTful API。

其实核心思想是1个资源对应1个URL,客户端对这1资源操作时(不同的request.get/post/put/delete方法)对应后端的增/删/改/查操作。

例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作。

我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:

请求方法URL含义
GET /book 查询书籍信息
POST /create_book 创建书籍记录
POST /update_book 更新书籍信息
POST /delete_book 删除书籍信息

我们按照RESTful API设计如下:

请求方法URL含义
GET /book 查询书籍信息
POST /book 创建书籍记录
PUT /book 更新书籍信息
DELETE /book 删除书籍信息

 

 

c.JSON响应json数据

package main

import "github.com/gin-gonic/gin"
//结构体
type user struct {
    Name string `json:"name"`
    Age int     `json:"age"`
}
//视图函数
func perosn(c *gin.Context)  {
    var userInfor=user{Name: "张根",Age:18}
    c.JSON(200,userInfor)
}
func main(){
    r:=gin.Default()
    r.GET("/person/",perosn)
    r.Run(":8002")
}

 

2.MVC模板渲染

如果是小型项目、历史原因、SEO优化我们使用模板渲染,Gin也是支持MTV模式的。

package main

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

func index(c *gin.Context) {
    //3.gin 模板渲染
    c.HTML(200, "index.html", gin.H{"title": "首页", "body": "hello"})

}

func main() {
    //1.创建1个默认的路由引擎
    router := gin.Default()
    router.GET("/", index)
    //2.gin模板解析
    router.LoadHTMLGlob("templates/*")    //正则匹配templates/所有文件
    router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件
    err := router.Run(":9001")
    if err != nil {
        fmt.Println("gin启动失败", err)
    }
}

 

3.文件上传

http请求也可以传输文件,有时候我们可以使用gin搭建1个ftp服务器。

单个文件上传

package main

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

func handleFile(c *gin.Context) {
	method := c.Request.Method
	if method == "GET" {
		c.HTML(200, "file.html", gin.H{})
	}

	if method == "POST" {
		//1.从客户端请求中获取文件
		fileObj, err := c.FormFile("localFile")
		if err != nil {
			c.JSON(400, gin.H{"err": err.Error()})
			return
		}
		//2.保存到服务端
		fileStorePath := path.Join("./upload/", fileObj.Filename)
		err = c.SaveUploadedFile(fileObj, fileStorePath)
		if err != nil {
			errMsg := fmt.Sprintf("文件保存失败:%s\n", err.Error())
			c.JSON(200, gin.H{"err": errMsg})
		}
		c.JSON(200, gin.H{"data": "上传成功"})
	}
}

func main() {
	router := gin.Default()
	router.Static("/static", "./static")
	router.LoadHTMLGlob("templates/*")
	router.GET("/file/", handleFile)
	router.POST("/file/", handleFile)
	err := router.Run(":8002")
	if err != nil {
		fmt.Println("gin启动失败", err)
		return
	}
}

 

多个文件上传

func main() {
	router := gin.Default()
	// 处理multipart forms提交文件时默认的内存限制是32 MiB
	// 可以通过下面的方式修改
	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// Multipart form
		form, _ := c.MultipartForm()
		files := form.File["file"]

		for index, file := range files {
			log.Println(file.Filename)
			dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
			// 上传文件到指定的目录
			c.SaveUploadedFile(file, dst)
		}
		c.JSON(http.StatusOK, gin.H{
			"message": fmt.Sprintf("%d files uploaded!", len(files)),
		})
	})
	router.Run()
}

 

Gin模板渲染 

现在大部分都是前后端分离的架构,除了seo优化我们基本不会使用gin做模板渲染。

 

1.扩展gin模板函数

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "html/template"
)

func index(c *gin.Context) {
    //3.gin 模板渲染
    c.HTML(200, "index.html", gin.H{"title": "首页", "name": "Martin", "age": "hello", "url": `<a href="https://www.cnblogs.com/sss4/">主页</a>`})

}

func main() {
    //1.创建1个默认的路由引擎
    router := gin.Default()
    router.GET("/", index)
    //1.5 gin框架模板自定义模板函数
    router.SetFuncMap(template.FuncMap{
        "safe": func(safeString string) template.HTML {
            return template.HTML(safeString)
        },
    })
    //2.gin模板解析
    //router.LoadHTMLGlob("templates/*")    //正则匹配templates/所有文件
    router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件

    err := router.Run(":9001")
    if err != nil {
        fmt.Println("gin启动失败", err)
    }
}

模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>

</head>
<body>
<ul>
    <li>{{.name}}</li>
    <li>{{.age}}</li>
    <li>{{.url | safe}}</li>
</ul>
</body>
</html>

 

2.加载静态文件路径

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"html/template"
)

func index(c *gin.Context) {
	//5.gin 模板渲染
	c.HTML(200, "index.html", gin.H{"title": "首页", "name": "Martin", "age": "hello", "url": `<a href="https://www.cnblogs.com/sss4/">主页</a>`})

}

func main() {
	//1.创建1个默认的路由引擎
	router := gin.Default()
	router.GET("/", index)
	//2.加载静态文件路径 .css
	router.Static("/static","./static")
	//3. 扩展gin框架模板自定义模板函数
	router.SetFuncMap(template.FuncMap{
		"safe": func(safeString string) template.HTML {
			return template.HTML(safeString)
		},
	})
	//4.gin模板解析
	//router.LoadHTMLGlob("templates/*")    //正则匹配templates/所有文件
	router.LoadHTMLGlob("templates/**/*") //正则匹配template/目录/所有文件


	err := router.Run(":9001")
	if err != nil {
		fmt.Println("gin启动失败", err)
	}
}

 模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
    <link rel="stylesheet" href="/static/dist/css/bootstrap.min.css">
    <script src="/static/jquery-3.2.1.min.js"></script>
    <script src="/static/dist/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <table class="table table-hover">
        <theader>
            <tr>
                <td>姓名</td>
                <td>年龄</td>
                <td>主页</td>
            </tr>
        </theader>
        <tbody>
        <tr>
            <td>{{.name}}</td>
            <td>{{.age}}</td>
            <td>{{.url|safe}}</td>
        </tr>
        </tbody>
    </table>
</div>
</body>
</html>

 

3.Gin模板继承

html/template实现了模板的嵌套和继承,但是gin不包含此功能。但是我们使用第第三方包。github.com/gin-contrib/multitemplate

Gin 路由组

URL路由太多了就需要分组管理,类似Flask的蓝图、Django里面的include URL。这些都是基于反射实现的。

但是Gin框架中的路由使用的是httprouter这个库,其基本原理就是构造一个路由地址的前缀树。

1.单支路由

//所有请求方式都汇聚到handleBook
router.Any("/book/", handleBook)
//处理404错误
router.NoRoute(handle404)

 

2.路由组

    //cmdb路由组
    cmdbRouter := router.Group("/cmdb")
    {
        cmdbRouter.GET("/list/")
        cmdbRouter.GET("/hosts/")
        cmdbRouter.GET("/option/")
    }
    //工单路由组
    workOrder := router.Group("/works")
    {
        workOrder.GET("/daily/")
        cmdbRouter.GET("/momthly")
        cmdbRouter.GET("/quarterly")
    }

 

3.路由嵌套

虽然gin的路由支持嵌套,但是出于对查询性能的考虑我们一般都会不会嵌套很多层路由。

//cmdb路由组
    cmdbRouter := router.Group("/cmdb")
    {
        cmdbRouter.GET("/list/")
        cmdbRouter.GET("/hosts/")
        //1.cmdb的主机
        hostRouter := cmdbRouter.Group("/host")
        {
            //1.1主机的cpu
            hostRouter.GET("/cup/")
            //1.2主机的内存
            hostRouter.GET("/memory/")
            //1.3主机的硬盘
            hostRouter.GET("/disks/")
            //1.4主机运行的服务
            hostRouter.GET("/process/")
            //1.5网络流量
            hostRouter.GET("/networks/")

        }

        cmdbRouter.GET("/option/")
    }
    //2.工单路由组
    workOrder := router.Group("/works")
    {
        workOrder.GET("/daily/")
        cmdbRouter.GET("/momthly")
        cmdbRouter.GET("/quarterly")
    }

 

Gin中间件

中间件就像写字楼里的闸机,刷卡进入------>工作------>出去也得刷卡。

当请求进入服务时,中间件第一时间生成唯一的 TraceID,将其注入请求上下文、日志、响应头。

让整条请求链路拥有统一标识,实现浏览器请求跨域、全链路追踪、问题排查、流量染色等功能。

中间件执行流程

 

执行流程

 

 

 调用context.Next(),继续调用下一个视图函数进行压栈。(子弹装满弹匣)

调用context.Abort() 阻止继续调用后续的函数,执行完当前栈针(函数)之后出栈。(1发子弹就够了)

调用context.Abort() + return,当前位置返回,当前位置之后的代码都不需要不执行了。(1发哑弹)

2.全局和局部中间间

全局中间件(全国宪法)

在根路由直接通过 router.Use() 注册,作用于项目所有接口、所有路由分组。

无论后续划分多少子路由组,请求都会优先执行全局中间件。

局部中间件(地方政策)

局部中间件在子路由组通过 group.Use() 注册,仅当前路由组下的接口生效。

不会影响其他独立路由组,但依然会先走全局中间件,再执行当前组的局部中间件。

日志服务

web开发日志输出可以方便定位根因。

package log

import (
    "context"
    "fmt"
    "os"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/spf13/viper"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "gopkg.in/natefinch/lumberjack.v2"
)

const ctxLoggerKey = "zapLogger"

type Logger struct {
    *zap.Logger
}

var DefaultLogger *Logger

func NewLog(conf *viper.Viper) *Logger {
    // log address "out.log" User-defined
    lp := conf.GetString("log.log_file_name")
    lv := conf.GetString("log.log_level")
    var level zapcore.Level
    //debug<info<warn<error<fatal<panic
    switch lv {
    case "debug":
        level = zap.DebugLevel
    case "info":
        level = zap.InfoLevel
    case "warn":
        level = zap.WarnLevel
    case "error":
        level = zap.ErrorLevel
    default:
        level = zap.InfoLevel
    }
    hook := lumberjack.Logger{
        Filename:   lp,                             // Log file path
        MaxSize:    conf.GetInt("log.max_size"),     // Maximum size unit for each log file: M
        MaxBackups: conf.GetInt("log.max_backups"),  // The maximum number of backups that can be saved for log files
        MaxAge:     conf.GetInt("log.max_age"),      // Maximum number of days the file can be saved
        Compress:   conf.GetBool("log.compress"),   // Compression or not
    }
    encoder := zapcore.NewJSONEncoder(zapcore.EncoderConfig{
        TimeKey:        "ts",
        LevelKey:       "level",
        NameKey:        "Logger",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.CapitalLevelEncoder,
        EncodeTime:     timeEncoder,
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    })
    multiWriteSyncer := zapcore.NewMultiWriteSyncer(zapcore.AddSync(&hook), zapcore.AddSync(os.Stdout))
    core := zapcore.NewCore(
        encoder,
        multiWriteSyncer,
        level,
    )
    DefaultLogger = &Logger{zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))}
    return DefaultLogger
}

func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
}

// WithValue Adds a field to the specified context
func (l *Logger) WithValue(ctx context.Context, fields ...zapcore.Field) context.Context {
    if c, ok := ctx.(*gin.Context); ok {
        ctx = c.Request.Context()
        c.Request = c.Request.WithContext(context.WithValue(ctx, ctxLoggerKey, l.WithContext(ctx).With(fields...)))
        return c
    }
    return context.WithValue(ctx, ctxLoggerKey, l.WithContext(ctx).With(fields...))
}

// WithContext Returns a zap instance from the specified context
func (l *Logger) WithContext(ctx context.Context) *Logger {
    if c, ok := ctx.(*gin.Context); ok {
        ctx = c.Request.Context()
    }
    zl := ctx.Value(ctxLoggerKey)
    ctxLogger, ok := zl.(*zap.Logger)
    if ok {
        return &Logger{ctxLogger}
    }
    return l
}

// Printf 实现 elastic.Logger 接口
func (l *Logger) Printf(format string, v ...interface{}) {
    logLine := fmt.Sprintf(format, v...)
    if strings.HasPrefix(logLine, "GET ") || strings.HasPrefix(logLine, "POST ") ||
        strings.HasPrefix(logLine, "PUT ") || strings.HasPrefix(logLine, "DELETE ") {
        l.Debug("[ES-TRACE] 请求详情", zap.String("detail", logLine))
        return
    }
    if strings.Contains(logLine, "Request: ") {
        dsl := strings.TrimPrefix(logLine, "Request: ")
        l.Debug("[ES-TRACE] 查询 DSL", zap.String("dsl", dsl))
        return
    }
}
log

使用

func main() {
    //生成logger对象配置
    conf := viper.New()
    conf.Set("log.log_file_name", "out.log")
    conf.Set("log.log_level", "info")
    //创建logger对象
    logger := log.NewLog(conf)
    //gin自带的context接口
    ginctx := context.Background()
    //gin项目打印日志
    ginctx = logger.WithValue(ginctx, zap.String("step1", "getInfo"))
    ginctx = logger.WithValue(ginctx, zap.String("step2", "getOrderinfo"))
    //日志输出
    logger.WithContext(ginctx).Warn("本次请求完成")
}

 

 

 

 

 

 

 

 

参考

posted on 2020-12-13 13:18  运维体系建设之路  阅读(1146)  评论(0)    收藏  举报