5.3. 利用gin搭建一个api框架(下篇)

未匹配的标注

无水印图片找不到了,文中图片来自知乎自己以前的账号,现在账号叫开源到。

上篇我们搭建好了一个骨架,但还有一些东西没有完善,比如常用的日志部分。

整合日志

这里我们先定义下log:

log:
  level: debug # 日志级别,info,debug,error
  file_format: "%Y%m%d" # 文件格式
  max_save_days: 30 # 保存天数
  file_type: one # one, level 单文件存储还是以level级别存储

整合logger:

package logger

import (
    "io"
    "log"
    "time"

    "github.com/lestrrat-go/file-rotatelogs"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
        "github.com/spf13/viper"
)

var Logger *zap.Logger 
var LogLevel string
var FileFormat string

// 初始化日志 logger
func init() {
    // 设置一些基本日志格式
    config := zapcore.EncoderConfig{
        MessageKey:  "msg",
        LevelKey:    "level",
        EncodeLevel: zapcore.CapitalLevelEncoder,
        TimeKey:     "ts",
        EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
            enc.AppendString(t.Format("2006-01-02 15:04:05"))
        },
        CallerKey:    "file",
        EncodeCaller: zapcore.ShortCallerEncoder,
        EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
            enc.AppendInt64(int64(d) / 1000000)
        },
    }
    encoder := zapcore.NewConsoleEncoder(config)

    FileFormat, saveType, LogLevel := "%Y%m%d", "one", "info"

    if viper.IsSet("log.file_format") {
        FileFormat = viper.GetString("log.file_format")
    }

    if viper.IsSet("log.level") {
        LogLevel = viper.GetString("log.level")
    }

    if viper.IsSet("log.save_type") {
        saveType = viper.GetString("log.save_type")
    }

    logLevel := zap.DebugLevel
    switch LogLevel {
        case "debug":
            logLevel = zap.DebugLevel
        case "info":
            logLevel = zap.InfoLevel
        case "error":
            logLevel = zap.ErrorLevel
        default:
            logLevel = zap.InfoLevel
    }

    switch saveType {
        case "level":
            Logger = getLevelLogger(encoder, logLevel, FileFormat)
        default:
            Logger = getOneLogger(encoder, logLevel, FileFormat)
    }

}

func getLevelLogger(encoder zapcore.Encoder, logLevel zapcore.Level, fileFormat string) *zap.Logger {
    infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
        return lvl == zapcore.InfoLevel && lvl >= logLevel
    })

    debugLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
        return lvl == zapcore.DebugLevel && lvl >= logLevel
    })

    errorLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
        return lvl >= zapcore.ErrorLevel && lvl >= logLevel
    })
    // 获取 info、warn日志文件的io.Writer 抽象 getLoggerWriter() 在下方实现
    infoWriter := getLoggerWriter("./log/info", fileFormat)
    errorWriter := getLoggerWriter("./log/error", fileFormat)
    debugWriter := getLoggerWriter("./log/debug", fileFormat)

    // 最后创建具体的Logger
    core := zapcore.NewTee(
        zapcore.NewCore(encoder, zapcore.AddSync(debugWriter), debugLevel),
        zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), infoLevel),
        zapcore.NewCore(encoder, zapcore.AddSync(errorWriter), errorLevel),
    )
    return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.WarnLevel))
}

func getOneLogger(encoder zapcore.Encoder, logLevel zapcore.Level, fileFormat string) *zap.Logger {
    infoWriter := getLoggerWriter("./log/info", fileFormat)

    infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
        return lvl == zapcore.InfoLevel && lvl >= logLevel
    })

    core := zapcore.NewTee(
        zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), infoLevel),
    )
    return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.WarnLevel))
}

func getLoggerWriter(filename, fileFormat string) io.Writer {
    // 生成rotatelogs的Logger 实际生成的文件名 file_YYmmddHH.log
    hook, err := rotatelogs.New(
        filename+fileFormat+".log",
        rotatelogs.WithLinkName(filename),
        // 保存天数
        rotatelogs.WithMaxAge(time.Hour*24*30),
        // 切割频率24小时
        rotatelogs.WithRotationTime(time.Hour*24),
    )
    if err != nil {
        log.Println("日志启动异常")
        panic(err)
    }
    return hook
}

func Debug(format string, v ...interface{}) {
    Logger.Sugar().Debugf(format, v...)
}

func Info(format string, v ...interface{}) {
    Logger.Sugar().Infof(format, v...)
}

func Error(format string, v ...interface{}) {
    Logger.Sugar().Errorf(format, v...)
}

这里注意init函数,我们直接调用logger其中函数即可,程序加载包的过程中会自动执行init函数。关于init有以下说明:

  1. init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
  2. 每个包可以拥有多个init函数
  3. 包的每个源文件也可以拥有多个init函数
  4. 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)
  5. 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
  6. init函数不能被其他函数调用,而是在main函数执行之前,自动被调用

我们直接使用:

logger.Info("i'm log123-----Info")
logger.Error("i'm log123-----Error")

平滑重启

当程序在线上稳定运行后,我们可能会去更新一些功能,但发布代码的同时,假如有用户正在使用,盲目发布代码可能会造成用户短暂失真,这时候平滑重启就来了。

对于平滑重启,其实有很多方案,这里我们只从自身代码级别来完成,而即便是代码级别,目前也有多种实现方案,比如第三方库endless这种,我这里主要参考了

https://github.com/kuangchanglang/graceful​github.com/kuangchanglang/graceful

简单说明下处理步骤:

  1. 监听信号(USR2,可自定义其他信号)
  2. 收到信号时fork子进程(使用相同的启动命令),将服务监听的socket文件描述符传递给子进程
  3. 子进程监听父进程的socket,这个时候父进程和子进程都可以接收请求
  4. 子进程启动成功之后,父进程停止接收新的连接,等待旧连接处理完成(或超时)
  5. 父进程退出,重启完成

详细分析可看底部参考-Golang服务器热重启、热升级、热更新

启动检查

结合上面的优雅重启,我们在启动时配置上启动健康检查:

package main

// import 这里我习惯把官方库,开源库,本地module依次分开列出
import (
    "fmt"
    "time"
    "errors"
    "net/http"

    "github.com/spf13/pflag"
    "github.com/spf13/viper"
    "github.com/gin-gonic/gin"

    "local.com/sai0556/demo2-gin-frame/config"
    "local.com/sai0556/demo2-gin-frame/db"
    "local.com/sai0556/demo2-gin-frame/router"
    "local.com/sai0556/demo2-gin-frame/logger"
    "local.com/sai0556/demo2-gin-frame/graceful"
)

var (
    conf = pflag.StringP("config", "c", "", "config filepath")
)

func main() {
    pflag.Parse()

    // 初始化配置
    if err := config.Run(*conf); err != nil {
        panic(err)
    }

    // logger.Info("i'm log123-----Info")
    // logger.Error("i'm log123-----Error")

    // 连接mysql数据库
    DB := db.GetDB()
    defer db.CloseDB(DB)

    // redis
    db.InitRedis()

    gin.SetMode(viper.GetString("mode"))
    g := gin.New()
    g = router.Load(g)

    // g.Run(viper.GetString("addr"))

    go func() {
        if err := pingServer(); err != nil {
            fmt.Println("fail:健康检测失败", err)
        }
        fmt.Println("success:健康检测成功")
    }()

    logger.Info("启动http服务端口%s\n", viper.GetString("addr"))

    if err := graceful.ListenAndServe(viper.GetString("addr"), g); err != nil && err != http.ErrServerClosed {
        logger.Error("fail:http服务启动失败: %s\n", err)
    }
}

// 健康检查
func pingServer() error {
    for i := 0; i < viper.GetInt("max_ping_count"); i++ {
        url := fmt.Sprintf("%s%s%s", "http://127.0.0.1", viper.GetString("addr"), viper.GetString("healthCheck"))
        fmt.Println(url)
        resp, err := http.Get(url)
        if err == nil && resp.StatusCode == 200 {
            return nil
        }
        time.Sleep(time.Second)
    }
    return errors.New("健康检测404")
}

这里就比较简单,另外启动一个协程,去ping健康检测的url即可。

打包脚本

shell

#!/bin/bash
SERVER="demo2-gin-frame"

function status() 
{
    if [ "`pgrep $SERVER -u $UID`" != "" ];then
        echo $SERVER is running
    else
        echo $SERVER is not running
    fi
}

function build() 
{
    echo "build..."

    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./$SERVER main.go
    if [ $? -ne "0" ];then  
        echo "built error!!!"
        return 
    fi  

    echo "built success!"
}

case "$1" in
    'status')
    status
    ;; 
    'build')
    build
    ;;  
    *)  
    echo "unknown, please: $0 {status or build}"
    exit 1
    ;;  
esac

bat

echo "build..."
SET CGO_ENABLED=0
SET GOOS=linux
go build -o demo2-gin-frame
echo commitid=%commitid%
if %errorlevel% == 0 (
    echo "built successfully"
) else (
    echo "built fail!!!"
)

对于程序的重启和保活,建议配合supervisor使用。

好,到这里我们的round 2就结束了。下一轮我们来玩玩钉钉智能机器人。

点击直达完整代码,有用不妨star一个


参考:


关注和赞赏都是对笔者最大的支持

关注和赞赏都是对笔者最大的支持

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~