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 有以下说明:
- init 函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
- 每个包可以拥有多个 init 函数
- 包的每个源文件也可以拥有多个 init 函数
- 同一个包中多个 init 函数的执行顺序 go 语言没有明确的定义 (说明)
- 不同包的 init 函数按照包导入的依赖关系决定该初始化函数的执行顺序
- init 函数不能被其他函数调用,而是在 main 函数执行之前,自动被调用
我们直接使用:
logger.Info("i'm log123-----Info")
logger.Error("i'm log123-----Error")
平滑重启#
当程序在线上稳定运行后,我们可能会去更新一些功能,但发布代码的同时,假如有用户正在使用,盲目发布代码可能会造成用户短暂失真,这时候平滑重启就来了。
对于平滑重启,其实有很多方案,这里我们只从自身代码级别来完成,而即便是代码级别,目前也有多种实现方案,比如第三方库 endless 这种,我这里主要参考了
https://github.com/kuangchanglang/gracefulgithub.com/kuangchanglang/graceful
简单说明下处理步骤:
- 监听信号(USR2,可自定义其他信号)
- 收到信号时 fork 子进程(使用相同的启动命令),将服务监听的 socket 文件描述符传递给子进程
- 子进程监听父进程的 socket,这个时候父进程和子进程都可以接收请求
- 子进程启动成功之后,父进程停止接收新的连接,等待旧连接处理完成(或超时)
- 父进程退出,重启完成
详细分析可看底部参考 - 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 就结束了。下一轮我们来玩玩钉钉智能机器人。
参考:
- golang zap 日志库使用(含文件切割、分级别存储和全局使用等)
- 基于 Go 语言构建企业级的 RESTful API 服务
- golang init 函数 - Go 语言中文网 - Golang 中文社区
- Golang 服务器热重启、热升级、热更新 (safe and graceful hot-restart/reload http server) 详解
关注和赞赏都是对笔者最大的支持
推荐文章: