日志系统
引入
现在已经2026年了,现在的日志不只是简单的文本文件,而是可观测性的基石,我们当然不能用Println去打印日志了,我们需要一个更加结构化,更加高效的日志系统。
基本介绍
时至今日,Uber-go/zap依旧是高性能日志的常用库,Lumbjack是文件切割的标准组件。
我们再终端执行
go get -u go.uber.org/zap
go get -u github.com/natefinch/lumberjack
来引入
部署
更新配置
在配置文件中加入:
log:
level :"info"
root_path:"./logs"
filename:"server.log"
max_size:10
max_age:30
compress:true #是否开启gzip压缩
format:"console" #输出格式在生产环境为json
- 日志级别:debug < info < warn < error
- 生产环境(Prod)通常要求日志格式为 JSON,以便被 ELK 或 Datadog 等日志平台自动抓取分析。
完善模型
我们需要结构体来接受YAML的配置
package model
type Config struct {
Server Server `mapstructure:"server"`
Mysql Mysql `mapstructure:"mysql"`
Log Log `mapstructure:"log"`
}
type Server struct {
Port int `mapstructure:"port"`
}
type Mysql struct {
Path string `mapstructure:"path"`
Config string `mapstructure:"config"`
DbName string `mapstructure:"db-name"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
}
type Log struct {
Level string `mapstructure:"level"`
RootPath string `mapstructure:"root_path"`
Filename string `mapstructure:"filename"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
Format string `mapstructure:"format"`
}
增加全局变量
package global
import (
"my_gin_app/internal/model"
"go.uber.org/zap"
"gorm.io/gorm"
)
var (
Config model.Config
DB *gorm.DB
Logger *zap.Logger
)
初始化
我们要实现下面的功能:
- 多路输出,要在控制台输出也要写入文件
- 文件切割,文件打了自动切分,不要一个文件写的特别长然后炸了
- 上线的时候输出json
package initialize
import (
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"my-gin-app/internal/global"
"os"
)
func InitLogger() {
cfg := global.Config.Log
/*切割*/
hook := &lumberjack.Logger{
Filename: fmt.Sprintf("%s%s", cfg.RootPath, cfg.Filename),
MaxSize: cfg.MaxSize,
MaxBackups: 3,
MaxAge: cfg.MaxAge,
Compress: cfg.Compress,
}
/*时间格式*/
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
/*判断输出格式*/
var encoder zapcore.Encoder
if cfg.Format == "json" {
encoder = zapcore.NewJSONEncoder(encoderConfig)
} else {
encoder = zapcore.NewConsoleEncoder(encoderConfig)
}
/*设置日志级别*/
level := zap.InfoLevel
if cfg.Level == "debug" {
level = zap.DebugLevel
}
/*核心——怎么编码,写到哪,什么级别*/
core := zapcore.NewCore(
encoder,
zapcore.NewMultiWriteSyncer(
zapcore.AddSync(os.Stdout),
zapcore.AddSync(hook),
),
level,
)
global.Logger = zap.New(core, zap.AddCaller())
/*替换全局标准库log*/
zap.ReplaceGlobals(global.Logger)
}
启动
package main
import (
"fmt"
"my-gin-app/internal/global"
"my-gin-app/internal/initialize"
"my-gin-app/internal/router"
"go.uber.org/zap"
)
func main() {
initialize.InitConfig()
initialize.InitLogger()
global.Logger.Info("启动")
// 1. 初始化配置和数据库
initialize.InitConfig()
initialize.InitDB()
global.Logger.Info("数据库连接成功")
// 2. 加载路由配置
r := router.SetupRouter()
port := global.Config.Server.Port
global.Logger.Info("服务运行ing",zap.Int("端口",port))
// 3. 启动服务
r.Run(fmt.Sprintf(":%d", global.Config.Server.Port))
}
然后我们要把 fmt.Println 替换为 global.Logger.Info 或 global.Logger.Error
在service添加日志
package service
import (
"errors"
"my-gin-app/internal/dao"
"my-gin-app/internal/global"
"my-gin-app/internal/model"
"go.uber.org/zap"
"gorm.io/gorm"
)
type UserService struct{}
var User = new(UserService)
/*创建新用户业务逻辑*/
func (s *UserService) CreateUser(user *model.User) error {
global.Logger.Info("创建用户:", zap.String("name", user.Name))
/*查一下是否存在*/
existUser, err := dao.User.GetByName(user.Name)
if err == nil && existUser.ID > 0 {
global.Logger.Warn("用户创建失败:用户名已占用", zap.String("name", user.Name))
return errors.New("用户名已存在")
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
global.Logger.Error("用户查重时数据库出错", zap.Error(err))
return err
}
if user.Age < 0 {
global.Logger.Warn("用户创建失败:年龄不合法", zap.Int("age", user.Age))
return errors.New("年龄不能为负数")
}
if err := dao.User.Create(user); err != nil {
// 创建失败
global.Logger.Error("保存用户到数据库失败", zap.Error(err))
return err
}
// 成功
global.Logger.Info("用户创建成功", zap.Int("new_id", int(user.ID)))
return nil
}
func (s *UserService) DeleteUser(id int) error {
if id == 1 {
return errors.New("无法删除")
}
return dao.User.Delete(id)
}
JWT中间件鉴权
基本介绍
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
部署
安装JWT库
go get -u github.com/golang-jwt/jwt/v5
更新配置
jwt:
signing_key: "gin_rank_secret_key_2026" # 生产环境用复杂的随机字符串
expires_time: "24h" # Token 有效期
完善模型
package model
type Config struct {
Server Server `mapstructure:server"`
Mysql Mysql `mapstructure:mysql`
Log Log `mapstructure:"log"`
JWT JWT `mapstructure:"jwt"`
}
type Server struct {
Port int `mapstructure:"port"`
}
type Mysql struct {
Path string `mapstructure:"path"`
Config string `mapstructure:"config"`
DbName string `mapstructure:"db-name"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
}
type Log struct {
Level string `mapstructure:"level"`
RootPath string `mapstructure:"root_path"`
Filename string `mapstructure:"filename"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
Format string `mapstructure:"format"`
}
type JWT struct {
SigningKey string `mapstructure:"signing_key"`
ExpiresTime string `mapstructure:"expires_time"`
}
编写生成和解析工具
新建文件:internal/utils/jwt.go
package utils
import (
"errors"
"my-gin-app/internal/global"
"time"
"github.com/golang-jwt/jwt/v5"
)
type MycustomClaims struct {
UserId int `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
func GenereateToken(userID int, username string) (string, error) {
jwtCfg := global.Config.JWT
duration, err := time.ParseDuration(jwtCfg.ExpiresTime)
if err != nil {
duration = 24 * time.Hour
}
claims := MycustomClaims{
UserId: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
Issuer: "gin_rank_system", /*签发人*/
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) /*加密算法和数据*/
return token.SignedString([]byte(jwtCfg.SigningKey))
/*注意必须转成切片*/
}
/*校验*/
func ParseToken(tokenStr string) (*MycustomClaims, error) {
cfg := global.Config.JWT
claims := &MycustomClaims{}
token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(cfg.SigningKey), nil
})
if err != nil{
return nil,err
}
if !token.Valid{
return nil, errors.New("token is invalid")
}
return claims ,nil
}
中间件
internal/service/user.go
package middleware
import (
"my-gin-app/internal/utils"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
c.Abort()
return
}
/*格式校验*/
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusUnauthorized, gin.H{"error:": "格式错误"})
c.Abort()
return
}
/*解析token*/
claims, err := utils.ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token 无效或已过期"})
c.Abort()
return
}
/*存入上下文 */
c.Set("userID", claims.UserId)
c.Set("username", claims.Username)
c.Next() // 放行
}
}
路由配置
package router
import (
"my-gin-app/internal/controller"
"my-gin-app/internal/middleware"
"github.com/gin-gonic/gin"
)
func SetupRouter() *gin.Engine {
r := gin.Default()
/*公开接口*/
public := r.Group("/api/v1")
{
public.POST("/login", controller.Login) // 登录接口
public.POST("/register", controller.Register) // 注册接口
}
/*受保护接口(需要 JWT)*/
protected := r.Group("/api/v1")
protected.Use(middleware.JWTAuth()) // 挂载中间件 🛡️
{
/*带了Token 才能访问下面*/
protected.GET("/user/profile", controller.GetUserList)
protected.PUT("/user", controller.UpdateUser)
}
return r
}
业务层
internal/service/user.go
func Login(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数格式错误"})
return
}
token, err := service.User.Login(input.Username, input.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "登录成功",
"token": token,
})
}
func Register(c *gin.Context) {
C
设计哲学
为什么我们要这么麻烦引入 JWT?这背后体现了三个架构思想:
- 无状态:服务器什么都不记。所有信息都刻在 Token 里发给用户自己保管。 Go 程序可以随便启动 100 个实例,甚至重启服务器,只要密钥不变,用户依然处于登录状态。这是云原生和微服务的基础。
- **以算力换空间 :解析 Token 需要进行加密运算(消耗 CPU),但不需要查数据库(节省 I/O),CPU 极其廉价,而数据库连接是瓶颈。JWT 让鉴权过程完全在内存中完成,速度极快。
- 信任链:
ParseWithClaims的核心逻辑不是“解密”(JWT 的内容其实是 Base64 编码,谁都能看),而是“检验”。只要签名对得上,我们就绝对信任里面的UserID是真的,不需要再去数据库查一遍“这个用户是不是真的叫张三”。这体现了零信任架构中的凭证思维。
自动化接口文档(swagger)
每次写完接口去APIFOX里面一点一点填参数调试,好麻烦,Swagger可以根据你的代码注释把你的代码注释直接变成一个可交互的 UI 界面。
安装
# 1. 安装命令行工具 (生成器)
go install github.com/swaggo/swag/cmd/swag@latest
# 2. 安装 Gin 的适配器
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files
配置
我们只需要在接口上方添加注释
// @Summary 用户注册
// @Description 创建一个新用户账号
// @Tags 用户模块
// @Accept json
// @Produce json
// @Param request body RegisterReq true "注册信息"
// @Success 200 {object} Response "成功"
// @Router /api/v1/register [post]
func Register(c *gin.Context) {
CreateUser(c)
}
在项目根目录运行 swag init,它会生成一个 docs 文件夹。
我们在main中启动
package main
import (
"fmt"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"go.uber.org/zap"
"my-gin-app/internal/global"
"my-gin-app/internal/initialize"
"my-gin-app/internal/router"
_ "my_gin_app/docs"
)
func main() {
initialize.InitConfig()
initialize.InitLogger()
global.Logger.Info("启动")
// 1. 初始化配置和数据库
initialize.InitConfig()
initialize.InitDB()
global.Logger.Info("数据库连接成功")
// 2. 加载路由配置
r := router.SetupRouter()
port := global.Config.Server.Port
global.Logger.Info("服务运行ing", zap.Int("端口", port))
// 3. 启动服务
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
r.Run(fmt.Sprintf(":%d", global.Config.Server.Port))
}
然后我们运行。
访问http://localhost:8080/swagger/index.html

我们可以测试接口。
请求参数校验
Gin 框架内置了 validator 库,我们不需要用if去判断,只需要结构体上贴“标签”就能搞定一切。
Username string `json:"username" binding:"required,min=3"`
| 标签 | 含义 | 例子 |
|---|---|---|
required | 必填,不能是零值 | binding:"required" |
min / max | 字符串最小/最大长度 | binding:"min=5,max=10" |
len | 字符串或数组的固定长度 | binding:"len=6" |
eq | 必须等于某个值 | binding:"eq=admin" |
oneof | 枚举,必须是列出的值之一 | binding:"oneof=red green blue" |
email | 邮箱格式 | binding:"email" |
url | URL链接格式 | binding:"url" |
datetime | 日期格式 | binding:"datetime=2006-01-02" |
上面是一个对照表










