Gin微服务框架_golang web框架_完整示例Demo

Gin简介

Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点。其实对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。框架更像是一些常用函数或者工具的集合。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。

gin特点
  • 性能优秀
  • 基于官方的net/http的有限封装
  • 方便 灵活的中间件
  • 数据绑定很强大
  • 社区比较活跃

官方源代码地址: https://github.com/gin-gonic/gin

gin web微服务框架示例

总体功能

  • 集成logrus.Logger日志按天切割,json格式打印
  • 集成swagger文档
  • 指定yml配置文件启动
  • 异常处理
  • 拦截器打印请求和响应参数
main.go项目入口

init方法: 初始化相关配置
main方法: 上面的注释定义了swagger信息,然后gin初始化,路由初始化,是否启用swagger


package main

import (
    "flag"
    "fmt"
    . "gin_demo/config"
    _ "gin_demo/docs"
    . "gin_demo/log"
    "gin_demo/router"
    "github.com/gin-gonic/gin"
    "github.com/swaggo/gin-swagger"
    "github.com/swaggo/gin-swagger/swaggerFiles"
    "runtime"
    "time"
)

var version = flag.Bool("version", true, "是否打印版本,默认打印")
var swagger = flag.Bool("swagger", true, "是否启动swagger接口文档,默认不启动")
var configFile = flag.String("configFile", "config/config.yml", "配置文件路径")
var projectPath = flag.String("projectPath", "/gin_demo", "项目访问路径前缀")

func init(){
    flag.Parse()

    ConfigRead(*configFile)

    LogInit()
}

//@title gin示例 API
//@version 0.0.1
//@description  相关接口文档
//@host 127.0.0.1:8080
//@BasePath
func main() {
    if *version {
        showVersion := fmt.Sprintf("%s %s@%s", "gin_demo", "1.0.0", time.Now().Format("2006-01-02 15:04:05"))
        fmt.Println(showVersion)
        fmt.Println("go version: " + runtime.Version())
    }

    Log.Info("start server...")

    gin.SetMode(gin.DebugMode) //全局设置环境,此为开发环境,线上环境为gin.ReleaseMode
    router.GinInit()

    //gin工程实例 *gin.Engine
    r := router.Router

    //路由初始化
    router.SetupRouter(*projectPath)

    if *swagger {
        //启动访问swagger文档
        r.GET(*projectPath + "/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    }

    Log.Info("listen on :%s", Cfg.ListenPort)
    //监听端口
    r.Run(":" + Cfg.ListenPort)

}


router.go 路由
package router

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

var Router *gin.Engine

func GinInit()  {
    // 禁用控制台颜色
    //gin.DisableConsoleColor()

    //gin.New()返回一个*Engine 指针
    //而gin.Default()不但返回一个*Engine 指针,而且还进行了debugPrintWARNINGDefault()和engine.Use(Logger(), Recovery())其他的一些中间件操作
    Router = gin.Default()
    //Router = gin.New()
}

func SetupRouter(projectPath string) {

    //使用日志
    //Router.Use(gin.Logger())
    //使用Panic处理方案
    //Router.Use(gin.Recovery())

    Router.Use(InitErrorHandler)
    Router.Use(InitAccessLogMiddleware)

    // 未知调用方式
    Router.NoMethod(InitNoMethodJson)
    // 未知路由处理
    Router.NoRoute(InitNoRouteJson)

    // Ping
    Router.GET(projectPath + "/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "ping": "pong",
        })
    })

    Router.POST(projectPath + "/pp", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "ping": "post",
        })
    })

}

middleware.go 中间件拦截器
package router

import (
    "encoding/json"
    . "gin_demo/log"
    . "gin_demo/threadlocal"
    "github.com/gin-gonic/gin"
    . "github.com/jtolds/gls"
    "github.com/sirupsen/logrus"
    "io/ioutil"
    "net/http"
    "strconv"
    "time"
)

// ErrorHandler is a middleware to handle errors encountered during requests
func InitErrorHandler(c *gin.Context) {
    c.Next()
    if len(c.Errors) > 0 {
        c.JSON(http.StatusBadRequest, gin.H{
            "errors": c.Errors,
        })
    }
}

//未知路由处理 返回json
func InitNoRouteJson(c *gin.Context) {
    c.JSON(http.StatusNotFound, gin.H{
        "code": http.StatusNotFound,
        "msg":  "path not found",
    })
}

//未知调用方式 返回json
func InitNoMethodJson(c *gin.Context) {
    c.JSON(http.StatusMethodNotAllowed, gin.H{
        "code": http.StatusMethodNotAllowed,
        "msg":  "method not allowed",
    })
}

//打印请求和响应日志
func InitAccessLogMiddleware(c *gin.Context) {
    //request id
    requestId := c.Request.Header.Get("X-RequestId")
    if requestId == "" {
        requestId = strconv.FormatInt(time.Now().UnixNano(), 10)
    }
    //response requestId
    c.Writer.Header().Set("X-RequestId", requestId)

    // 开始时间
    startTime := time.Now()

    //处理请求 do chian
    Mgr.SetValues(Values{Rid: requestId}, func() {
        c.Next()
    })

    // 结束时间
    endTime := time.Now()
    // 执行时间
    latencyTime := endTime.Sub(startTime)
    // 请求方式
    reqMethod := c.Request.Method
    // 请求路由
    reqUri := c.Request.RequestURI
    // 状态码
    statusCode := c.Writer.Status()
    // 请求IP
    clientIP := c.ClientIP()
    //请求参数
    body, _ := ioutil.ReadAll(c.Request.Body)
    //返回参数
    responseMap := c.Keys
    responseJson, _ := json.Marshal(responseMap)

    //日志格式
    //LogAccess.Infof("| %3d | %13v | %15s | %s | %s | %s | %s | %s |",
    //  statusCode,
    //  latencyTime,
    //  clientIP,
    //  reqMethod,
    //  reqUri,
    //  requestId,
    //  string(body),
    //  string(responseJson),
    //)

    // 日志格式
    LogAccess.WithFields(logrus.Fields{
        "status_code":  statusCode,
        "latency_time": latencyTime,
        "client_ip":    clientIP,
        "req_method":   reqMethod,
        "req_uri":      reqUri,
        "req_Id":       requestId,
        "req_body":     string(body),
        "res_body":     string(responseJson),
    }).Info()

}

logger.go 日志定义和配置
package log

import (
    "fmt"
    "gin_demo/config"
    "github.com/sirupsen/logrus"
    rotatelogs "github.com/lestrrat-go/file-rotatelogs"
    "github.com/rifflock/lfshook"
    "os"
    "path"
    "time"
)

var Log *logrus.Logger
var LogAccess *logrus.Logger

func LogInit() {
    logFilePath := ""
    logPath := config.Cfg.LogPath
    if len(logPath) == 0 {
        //获取当前目录
        if dir, err := os.Getwd(); err == nil {
            logFilePath = dir + "/logs/"
        }
    } else {
        //指定目录
        logFilePath = logPath + "/logs/"
    }

    if err := os.MkdirAll(logFilePath, 0777); err != nil {
        fmt.Println(err.Error())
    }

    rootLogInit(logFilePath)
    accessLogInit(logFilePath)
}

func rootLogInit(logFilePath string) {
    logFileName := "root.log"

    //日志文件
    fileName := path.Join(logFilePath, logFileName)
    if _, err := os.Stat(fileName); err != nil {
        if _, err := os.Create(fileName); err != nil {
            fmt.Println(err.Error())
        }
    }

    //写入文件
    src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
    if err != nil {
        fmt.Println("err", err)
    }

    //实例化
    Log = logrus.New()
    //设置输出
    Log.Out = src
    Log.Out = os.Stdout
    //设置日志级别
    Log.SetLevel(logrus.DebugLevel)

    // 设置 rotatelogs
    logWriter, err := rotatelogs.New(
        // 分割后的文件名称
        fileName + "-%Y%m%d.log",

        // 生成软链,指向最新日志文件
        rotatelogs.WithLinkName(fileName),

        // 设置最大保存时间(2天)
        rotatelogs.WithMaxAge(2*24*time.Hour),

        // 设置日志切割时间间隔(1天)
        rotatelogs.WithRotationTime(24*time.Hour),
    )

    writeMap := lfshook.WriterMap{
        logrus.InfoLevel:  logWriter,
        logrus.FatalLevel: logWriter,
        logrus.DebugLevel: logWriter,
        logrus.WarnLevel:  logWriter,
        logrus.ErrorLevel: logWriter,
        logrus.PanicLevel: logWriter,
    }

    //设置日志格式
    lfHook := lfshook.NewHook(writeMap, &logrus.JSONFormatter{
        TimestampFormat:"2006-01-02 15:04:05",
    })

    // 新增 Hook
    Log.AddHook(lfHook)

}

func accessLogInit(logFilePath string) {
    logFileNameAccess := "access.log"

    fileNameAccess := path.Join(logFilePath, logFileNameAccess)
    if _, err := os.Stat(fileNameAccess); err != nil {
        if _, err := os.Create(fileNameAccess); err != nil {
            fmt.Println(err.Error())
        }
    }

    srcAccess, err := os.OpenFile(fileNameAccess, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
    if err != nil {
        fmt.Println("err", err)
    }

    //实例化
    LogAccess = logrus.New()
    //设置输出
    LogAccess.Out = srcAccess
    LogAccess.Out = os.Stdout
    //设置日志级别
    LogAccess.SetLevel(logrus.DebugLevel)

    // 设置 rotatelogs
    logWriterAccess, err := rotatelogs.New(
        // 分割后的文件名称
        fileNameAccess + "-%Y%m%d.log",

        // 生成软链,指向最新日志文件
        rotatelogs.WithLinkName(fileNameAccess),

        // 设置最大保存时间(2天)
        rotatelogs.WithMaxAge(2*24*time.Hour),

        // 设置日志切割时间间隔(1天)
        rotatelogs.WithRotationTime(24*time.Hour),
    )

    writeMapAccess := lfshook.WriterMap{
        logrus.InfoLevel:  logWriterAccess,
        logrus.FatalLevel: logWriterAccess,
        logrus.DebugLevel: logWriterAccess,
        logrus.WarnLevel:  logWriterAccess,
        logrus.ErrorLevel: logWriterAccess,
        logrus.PanicLevel: logWriterAccess,
    }

    lfHookAccess := lfshook.NewHook(writeMapAccess, &logrus.JSONFormatter{
        TimestampFormat:"2006-01-02 15:04:05",
    })

    // 新增 Hook
    LogAccess.AddHook(lfHookAccess)
}

Demo运行

swag 的安装使用后续会讲解

#执行:swag init 生成swagger文件
gin_demo git:(main) swag init
#显示如下,会在项目生成docs文件夹
2021/07/23 21:30:36 Generate swagger docs....
2021/07/23 21:30:36 Generate general API Info
2021/07/23 21:30:36 create docs.go at  docs/docs.go

#启动项目
go run main.go 
#打印如下,表示成功启动8080端口
Listening and serving HTTP on :8080
在这里插入图片描述

浏览器访问接口:
http://127.0.0.1:8080/gin_demo/ping

{"ping":"pong"}

浏览器访问swagger:

在这里插入图片描述

Demo源代码地址:https://github.com/tw-iot/gin_demo

参考链接地址:
http://www.topgoer.com/gin%E6%A1%86%E6%9E%B6/%E7%AE%80%E4%BB%8B.html
https://zhuanlan.zhihu.com/p/165633941
https://github.com/skyhee/gin-doc-cn
https://www.jianshu.com/p/98965b3ff638

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容