引言 在开发Go后端项目时,我们总会用到两大王牌工具:Zap-标准化日志管理系统 和 Viper-配置管理系统
Zap 在构建可靠、可维护的Go应用程序时,直接使用fmt.Println或log标准库虽然简单,但难以满足生产环境对高性能、结构化和灵活配置的需求
Zap是一个能够进行标准化日志管理的第三方库 ,可以实现结构化,等级化且高自定义性的日志输出,无论是控制台还是日志文档都不在话下
快速学习 安装依赖:
1 go get -u go.uber.org/zap
基本使用 Zap使用日志器 (Logger )作为对象实例进行日志管理方法
在使用Logger前,需要初始化创建Logger :
而Zap提供了不同模式的Logger以供选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func dev () { logger, _ := zap.NewDevelopment() logger.Info("dev this is info" ) logger.Warn("dev this is warn" ) logger.Error("dev this is error" ) } func test () { logger := zap.NewExample() logger.Info("exam this is info" ) logger.Warn("exam this is warn" ) logger.Error("exam this is error" ) } func prod () { logger, _ := zap.NewProduction() logger.Info("prod this is info" ) logger.Warn("prod this is warn" ) logger.Error("prod this is error" ) }
dev模式 下,日志格式是text格式,并且warn和error会有栈信息
example模式 下,格式是json,并且字段只有level和msg
prod模式 下,格式也是json,多一个时间和函数位置字段,生产环境上用json格式的日志更方便排查
日志级别 1 2 3 4 5 6 7 8 9 10 cfg := zap.NewDevelopmentConfig() cfg.Level = zap.NewAtomicLevelAt(zap.WarnLevel) logger, _ := cfg.Build() logger.Debug("this is dev debug log" ) logger.Info("this is dev info log" ) logger.Warn("this is dev warn log" ) logger.Error("this is dev error log" ) logger.Fatal("this is dev fatal log" )
时间格式化 Zap默认的时间格式并不美观和实用,我们可以自己更改日志时间格式 :
1 2 3 4 5 6 7 8 cfg := zap.NewDevelopmentConfig() cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05" ) logger, _ := cfg.Build() logger.Info("dev this is info" )
这样,Zap输出的日志时间格式就是形如2026-2-20 15:12:32样式的了
Sugar日志 它提供了两种主要的日志器(Logger):
SugaredLogger : 在性能与易用性间取得平衡,支持printf风格的格式化(如Infof)和松散的键值对(如Info(“failed to fetch URL”, “url”, url, “attempt”, 3)),性能远超标准库。
Logger : 追求极致性能,要求所有日志字段都必须是强类型的(使用zap.Field,如zap.String(“url”, url)),性能比SugaredLogger更高。
一般的Logger只有Error,Info等字符串方法,使得插入变量这一操作更加复杂
而SugaredLogger(以下简称Slog)是一个加强版的Logger实例 ,可以支持Printf类似的格式化字符串 方法,比如Errorf,Infof等
使用 Slog := logger.Sugar()得到Logger的Slog
结构化日志 zap支持通过Field的形式记录结构化日志,方便分析和查询
1 2 3 4 5 6 logger, _ := zap.NewDevelopment() logger.Info("this is info" , zap.String("username" , "admin" ), zap.Int("user_id" , 42 ), zap.Bool("active" , true ), )
自定义解码器 对于更深层级的Zap使用,我们会用到自定义解码器 ,允许我们对Zap默认解码器的内部进行配置和功能修改
颜色控制 让zap输出指定颜色的日志,本质上是对默认配置NewDevelopmentConfig()中的解码器配置EncoderConfig里的EncodeLevel定制解码器Encoder
以及,我们需要了解控制台的颜色控制字符
1 2 3 4 5 6 fmt.Printf("\033[31mthis is 红色\n\033[0m" ) fmt.Printf("\033[32mthis is 绿色\n\033[0m" ) fmt.Printf("\033[33mthis is 黄色\n\033[0m" ) fmt.Printf("\033[34mthis is 蓝色\n\033[0m" ) fmt.Printf("\033[35mthis is 紫色\n\033[0m" ) fmt.Printf("\033[36mthis is 青色\n\033[0m" )
Encoder代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const ( colorRed = "\033[31m" colorGreen = "\033[32m" colorYellow = "\033[33m" colorBlue = "\033[34m" colorReset = "\033[0m" ) func coloredLevelEncoder (level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { switch level { case zapcore.DebugLevel: enc.AppendString(colorBlue + "DEBUG" + colorReset) case zapcore.InfoLevel: enc.AppendString(colorGreen + "INFO" + colorReset) case zapcore.WarnLevel: enc.AppendString(colorYellow + "WARN" + colorReset) case zapcore.ErrorLevel, zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel: enc.AppendString(colorRed + "ERROR" + colorReset) default : enc.AppendString(level.String()) } } func dev () { cfg := zap.NewDevelopmentConfig() cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05" ) cfg.EncoderConfig.EncodeLevel = coloredLevelEncoder logger, _ := cfg.Build() logger.Info("dev this is info" ) logger.Warn("dev this is warn" ) logger.Error("dev this is error" ) }
此时日志等级的输出效果被美化
自定义日志输出 通过自定义Encoder ,对原始日志行进行字符串方法处理
以下是为日志行增加项目名称前缀的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const logPrefix = "[MyApp] " type prefixedEncoder struct { zapcore.Encoder } func (e *prefixedEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error ) { buf, err := e.Encoder.EncodeEntry(entry, fields) if err != nil { return nil , err } logLine := buf.String() buf.Reset() buf.AppendString(logPrefix + logLine) return buf, nil } func dev () { cfg := zap.NewDevelopmentConfig() cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05" ) encoder := &prefixedEncoder{ Encoder: zapcore.NewConsoleEncoder(cfg.EncoderConfig), } core := zapcore.NewCore( encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel, ) logger := zap.New(core, zap.AddCaller()) logger.Info("dev this is info" ) logger.Warn("dev this is warn" ) logger.Error("dev this is error" ) }
效果预览
全局日志 有些时候想要在应用程序的任何地方都可以直接使用的日志实例,就需要用到全局日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func initLogger () { cfg := zap.NewDevelopmentConfig() cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05" ) logger, _ := cfg.Build() zap.ReplaceGlobals(logger) } func dev () { zap.L().Info("dev this is info" ) zap.L().Warn("dev this is warn" ) zap.L().Error("dev this is error" ) zap.S().Infof("dev this is info %s" , "xxx" ) zap.S().Warnf("dev this is warn %s" , "xxx" ) zap.S().Errorf("dev this is error %s" , "xxx" ) }
L方法返回的是标准zap实例 ,S方法返回的是SuperedZap的实例
日志双写 日志双写就是同时把日志行写出在控制台和日志文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func initLogger () { cfg := zap.NewDevelopmentConfig() cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05" ) consoleCore := zapcore.NewCore( zapcore.NewConsoleEncoder(cfg.EncoderConfig), zapcore.AddSync(os.Stdout), zapcore.DebugLevel, ) file, _ := os.OpenFile("app.log" , os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644 ) fileCore := zapcore.NewCore( zapcore.NewConsoleEncoder(cfg.EncoderConfig), zapcore.AddSync(file), zapcore.DebugLevel, ) core := zapcore.NewTee(consoleCore, fileCore) logger := zap.New(core, zap.AddCaller()) zap.ReplaceGlobals(logger) }
使用zapcore.NewTee可以组合多个core实例
日志分片 将日志文件以不同的策略进行分片处理,常见的为按时间分片 和按等级分片
以下代码实现了按照日期进行日志分片,并单独分理处ERR等级的日志行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 package logimport ( "errors" "os" "path/filepath" "sync" "time" "go.uber.org/zap" "go.uber.org/zap/buffer" "go.uber.org/zap/zapcore" ) const ( colorRed = "\033[31m" colorGreen = "\033[32m" colorYellow = "\033[33m" colorBlue = "\033[34m" colorReset = "\033[0m" ) type MyEncoder struct { AppName string zapcore.Encoder errFile *os.File writer MyLogWriter } type MyLogWriter struct { mu sync.Mutex logDate string file *os.File logPath string } func (m *MyEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error ) { buf, err := m.Encoder.EncodeEntry(entry, fields) if err != nil { return nil , err } DataStr := buf.String() buf.Reset() buf.AppendString("[" + m.AppName + "] " + DataStr) m.writer.mu.Lock() defer m.writer.mu.Unlock() currentDate := time.Now().Format("2006-01-02" ) if m.writer.logDate != currentDate { if m.writer.file != nil { m.writer.file.Close() } newPath := filepath.Join(m.writer.logPath, "/" , currentDate) if err := os.MkdirAll(newPath, 0755 ); err != nil { return nil , errors.New("创建文件夹错误" ) } filePath := filepath.Join(newPath, " INFO" ) file, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644 ) if err != nil { return nil , errors.New("打开文件错误" + filePath) } m.writer.file = file m.writer.logDate = currentDate } if entry.Level >= zapcore.ErrorLevel { if m.errFile == nil { newPath := filepath.Join(m.writer.logPath, "/" , currentDate) filePath := filepath.Join(newPath, " ERR" ) errFile, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644 ) if err != nil { return nil , err } m.errFile = errFile } m.errFile.WriteString(buf.String()) } if m.writer.logDate == currentDate { m.writer.file.WriteString(buf.String()) } return buf, nil } func InitLogManager (appName string , logPath string ) { cfg := zap.NewDevelopmentConfig() cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05" ) myEncoder := &MyEncoder{ AppName: appName, Encoder: zapcore.NewConsoleEncoder(cfg.EncoderConfig), writer: MyLogWriter{ logPath: logPath, }, } core := zapcore.NewCore( myEncoder, zapcore.AddSync(os.Stdout), zapcore.InfoLevel, ) Logger := zap.New(core, zap.AddCaller()) zap.ReplaceGlobals(Logger) }
效果预览
使用标准 在后端web项目中,应当对主程序的开始/结束 ,路由的注册 ,路由的请求开始/结束 ,缓存的命中信息 ,以及所有的错误触发点 进行日志写入,并搭配合理的错误抛出机制,快速定位错误来源
注意避免暴露敏感信息
项目日志示例:
Viper Viper则是配置管理的万能钥匙,支持多种配置源管理 和动态热更新
Viper 使用如下的优先级来读取配置:
显示的值设置
命令行标记标记
环境变量
配置文件
键值存储
默认值
注:Viper 配置中的键不区分大小写
快速学习 安装依赖
1 go get -u github.com/spf13/viper
设置默认值 1 2 viper.SetDefault("filePath" ,"./dir/img/usr" ) viper.SetDefault("root" ,"123456" )
以下以环境变量.env管理和配置文件.yaml管理做代码演示
环境变量 viper可以轻松地绑定环境变量
1 2 viper.AutomaticEnv() viper.BindEnv("database.user" , "DB_USER" )
配置文件 viper可以轻松地读写配置文件
读取 首先需要配置viper,让viper知道在哪里查找配置文件
Viper 支持 JSON、 TOML、 YAML、 HCL、 INI、 envfile 和 JavaProperties 文件
Viper 可以同时搜索多个路径,但目前单个 Viper 实例只支持单个配置文件
下面是使用 Viper 读取配置文件的一个示例,不需要指定一个完整路径,但在使用时至少应当提供一个配置文件
1 2 3 4 5 6 7 8 9 10 11 func TestReadConfigFile (t *testing.T) { viper.SetConfigName("config.yml" ) viper.SetConfigType("yaml" ) viper.AddConfigPath("./" ) viper.AddConfigPath("$HOME/" ) viper.AddConfigPath("." ) err := viper.ReadInConfig() if err != nil { log.Fatalln(err) } }
当未找到配置文件时:
1 2 3 4 5 6 7 if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { } else { } }
当访问嵌套配置的时候通过.分隔符 进行访问,例如:
1 2 3 4 5 6 7 { "server" : { "database" : { "url" : "mysql...." } } }
可以通过GetString("server.database.url")来进行嵌套访问
访问配置的所有函数:
Get(key string) : interface{}
GetBool(key string) : bool
GetFloat64(key string) : float64
GetInt(key string) : int
GetIntSlice(key string) : []int
GetString(key string) : string
GetStringMap(key string) : map[string]interface{}
GetStringMapString(key string) : map[string]string
GetStringSlice(key string) : []string
GetTime(key string) : time.Time
GetDuration(key string) : time.Duration
IsSet(key string) : bool
AllSettings() : map[string]interface{}
演示 对于这样一个依赖于环境变量.env的yaml配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 app: name: ${APP_NAME} env: ${APP_ENV} log_path: ${LOG_PATH} max_requests_every_minute: ${MAX_REQUESTS_EVERY_MINUTE} http: host: ${APP_HOST} port: ${APP_PORT} file: cloud_file_dir: ${CLOUD_FILE_DIR} avatar_dir: ${AVATAR_DIR} default_avatar_dir: ${DEFAULT_AVATAR_PATH} max_file_size: ${MAX_FILE_SIZE} normal_user_max_storage: ${NORMAL_USER_MAX_STORAGE} limited_speed: ${LIMITED_SPEED} jwt: secret_key: ${SECRET_KEY} issuer: ${ISSUER} exp_time_hours: ${EXP_TIME_HOURS} database: mysql: root_password: ${MYSQL_ROOT_PASSWORD} database: ${MYSQL_DATABASE} user: ${MYSQL_USER} password: ${MYSQL_PASSWORD} dsn: ${DB_DSN} redis: addr: ${REDIS_ADDR} password: ${REDIS_PASSWORD} db: ${REDIS_DB} minIO: root_user: ${MINIO_ROOT_USER} password: ${MINIO_ROOT_PASSWORD} endpoint: ${MINIO_ENDPOINT} bucket_name: ${MINIO_BUCKET_NAME} email: SMTP_host: ${SMTP_HOST} SMTP_port: ${SMTP_PORT} SMTP_user: ${SMTP_USER} SMTP_pass: ${SMTP_PASS} from_name: ${FROM_NAME} from_email: ${FROM_EMAIL}
使用如下代码进行配置管理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 package configimport ( "os" "os/signal" "strings" "syscall" "time" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" "go.uber.org/zap" ) type EmailConfig struct { SMTPHost string SMTPPort int SMTPUser string SMTPPass string FromName string FromEmail string } type RedisConfig struct { Addr string Password string DB int } type MinIOConfig struct { MinIORootName string MinIOPassword string MinIOEndpoint string MinIOBucketName string } type Config struct { AppName string LogPath string MaxRequests int JWTSecret string JWTIssuer string JWTExpireHours int CloudFileDir string AvatarDIR string DefaultAvatarPath string MaxFileSize int64 NormalUserMaxStorage int64 LimitedSpeed int64 DSN string Redis RedisConfig MinIO MinIOConfig Email EmailConfig Host string Port int } func InitConfigByViper () *Config { viper.SetConfigName("config" ) viper.SetConfigType("yaml" ) viper.AddConfigPath("./config/config.yaml" ) godotenv.Load() configContent, err := os.ReadFile("./config/config.yaml" ) if err != nil { zap.S().Fatalf("os读取config.yaml失败: %v" , err) } expandedContent := os.ExpandEnv(string (configContent)) if err := viper.ReadConfig(strings.NewReader(expandedContent)); err != nil { zap.S().Fatalf("viper提取config.yaml失败: %v" , err) } viper.SetDefault("app.name" , "ClaranCloudDisk" ) viper.SetDefault("app.file.cloud_file_dir" , "./CloudFiles" ) viper.SetDefault("app.file.avatar_dir" , "./Avatars" ) viper.SetDefault("app.file.default_avatar_path" , "./Avatars/DefaultAvatar/DefaultAvatar.png" ) viper.SetDefault("app.log_path" , "./log./logs" ) return &Config{ AppName: viper.GetString("app.name" ), LogPath: viper.GetString("app.log_path" ), MaxRequests: viper.GetInt("app.max_requests_every_minute" ), JWTSecret: viper.GetString("jwt.secret_key" ), JWTIssuer: viper.GetString("jwt.issuer" ), JWTExpireHours: viper.GetInt("jwt.exp_time_hours" ), CloudFileDir: viper.GetString("app.file.cloud_file_dir" ), AvatarDIR: viper.GetString("app.file.avatar_dir" ), DefaultAvatarPath: viper.GetString("app.file.default_avatar_dir" ), MaxFileSize: viper.GetInt64("app.file.max_file_size" ), NormalUserMaxStorage: viper.GetInt64("app.file.normal_user_max_storage" ), LimitedSpeed: viper.GetInt64("app.file.limited_speed" ), DSN: viper.GetString("database.mysql.dsn" ), Redis: RedisConfig{ Addr: viper.GetString("database.redis.addr" ), Password: viper.GetString("database.redis.password" ), DB: viper.GetInt("database.redis.db" ), }, MinIO: MinIOConfig{ MinIORootName: viper.GetString("minio.root_user" ), MinIOPassword: viper.GetString("minio.password" ), MinIOEndpoint: viper.GetString("minio.endpoint" ), MinIOBucketName: viper.GetString("minio.bucket_name" ), }, Email: EmailConfig{ SMTPHost: viper.GetString("email.SMTP_host" ), SMTPPort: viper.GetInt("email.SMTP_port" ), SMTPUser: viper.GetString("email.SMTP_user" ), SMTPPass: viper.GetString("email.SMTP_pass" ), FromName: viper.GetString("email.from_name" ), FromEmail: viper.GetString("email.from_email" ), }, Host: viper.GetString("app.http.host" ), Port: viper.GetInt("app.http.port" ), } }
写入 Viper 提供了一系列函数来方便开发者将运行时存储的配置写入配置文件 中
1 2 3 4 5 6 7 8 9 10 11 func WriteConfig () error { return v.WriteConfig() }func SafeWriteConfig () error { return v.SafeWriteConfig() }func WriteConfigAs (filename string ) error { return v.WriteConfigAs(filename) }func SafeWriteConfigAs (filename string ) error { return v.SafeWriteConfigAs(filename) }
监控和重载配置 Viper 允许应用程序在运行时动态读取配置文件 ,即不需要重新启动应用程序也可以使更新的配置生效
只需要简单地告诉 Viper 实例去监视配置变化,或者可以提供一个函数给 viper 以便每次发生变化时运行该函数
1 2 3 4 5 6 func TestWatchingConfig (t *testing.T) { viper.OnConfigChange(func (e fsnotify.Event) { fmt.Println("配置文件已更改:" , e.Name) }) viper.WatchConfig() }
以下为项目中监控和重载配置的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 func WatchConfig () { zap.L().Info("开始监控配置文件" ) watcher, err := fsnotify.NewWatcher() if err != nil { zap.S().Fatalf("创建监控器失败: %v" , err) return } defer watcher.Close() if err := watcher.Add("./config/config.yaml" ); err != nil { zap.S().Fatalf("监控文件失败: %v" , err) return } zap.L().Info("监控配置文件中: ./config/config.yaml" ) signalChan := make (chan os.Signal, 1 ) signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) for { select { case event, ok := <-watcher.Events: if !ok { return } if event.Op&fsnotify.Write == fsnotify.Write { zap.S().Warnf("检测到配置文件变化: %s" , event.Name) time.Sleep(100 * time.Millisecond) configContent, err := os.ReadFile("./config/config.yaml" ) if err != nil { zap.S().Fatalf("os读取config.yaml失败: %v" , err) } expandedContent := os.ExpandEnv(string (configContent)) if err := viper.ReadConfig(strings.NewReader(expandedContent)); err != nil { zap.S().Fatalf("viper提取config.yaml失败: %v" , err) } zap.L().Warn("配置已热重载,请及时重启服务器" ) } case err, ok := <-watcher.Errors: if !ok { return } zap.S().Errorf("监控出错: %v" , err) case <-signalChan: zap.L().Info("停止监控" ) return } } }
参考文章 Viper | Golang学习中文文档
枫枫知道 | Zap第三方库