在Golang项目开发中,日志是排查问题、监控运行状态的核心工具。很多场景下我们不需要引入复杂的第三方日志库,自己实现一个简单的日志系统就能满足需求,还能根据项目特性做定制化调整。

核心需求分析
一个简单的日志系统需要覆盖以下几个基础功能:
- 支持日志分级,比如DEBUG、INFO、WARN、ERROR四个级别,不同级别日志可以按需输出
- 日志内容包含时间戳、日志级别、具体日志信息,方便后续检索
- 支持输出到控制台和文件,文件日志需要按日期或者大小切割,避免单个文件过大
- 并发场景下保证日志写入安全,不会出现内容错乱
基础结构与常量定义
首先我们定义日志级别和相关的基础结构,代码如下:
package logger
import (
"fmt"
"os"
"sync"
"time"
)
// 日志级别常量
const (
DEBUG = iota
INFO
WARN
ERROR
)
// 日志级别对应的字符串
var levelStr = map[int]string{
DEBUG: "DEBUG",
INFO: "INFO",
WARN: "WARN",
ERROR: "ERROR",
}
// Logger 日志结构体
type Logger struct {
level int // 当前日志级别
file *os.File // 日志文件句柄
mu sync.Mutex // 互斥锁保证并发安全
filePath string // 日志文件路径
}
初始化日志实例
我们需要一个初始化函数,用来创建日志实例,设置日志级别和日志文件路径:
// NewLogger 创建新的日志实例
// level: 日志级别,低于该级别的日志不会输出
// filePath: 日志文件路径,如果为空则只输出到控制台
func NewLogger(level int, filePath string) (*Logger, error) {
l := &Logger{
level: level,
filePath: filePath,
}
// 如果指定了日志文件路径,打开或创建文件
if filePath != "" {
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("打开日志文件失败: %v", err)
}
l.file = file
}
return l, nil
}
核心日志写入方法
日志写入需要统一处理格式,同时输出到控制台和文件(如果配置了文件输出),并且要保证并发安全:
// writeLog 写入日志的核心方法
func (l *Logger) writeLog(level int, format string, args ...interface{}) {
// 日志级别不够,直接返回
if level < l.level {
return
}
// 构造日志内容
now := time.Now().Format("2006-01-02 15:04:05")
levelName := levelStr[level]
msg := fmt.Sprintf(format, args...)
logContent := fmt.Sprintf("%s [%s] %sn", now, levelName, msg)
// 加锁保证并发安全
l.mu.Lock()
defer l.mu.Unlock()
// 输出到控制台
fmt.Print(logContent)
// 输出到文件
if l.file != nil {
_, err := l.file.WriteString(logContent)
if err != nil {
fmt.Printf("写入日志文件失败: %vn", err)
}
}
}
对外暴露的日志方法
为了方便使用,我们给四个日志级别分别提供对应的方法:
// Debug 输出DEBUG级别日志
func (l *Logger) Debug(format string, args ...interface{}) {
l.writeLog(DEBUG, format, args...)
}
// Info 输出INFO级别日志
func (l *Logger) Info(format string, args ...interface{}) {
l.writeLog(INFO, format, args...)
}
// Warn 输出WARN级别日志
func (l *Logger) Warn(format string, args ...interface{}) {
l.writeLog(WARN, format, args...)
}
// Error 输出ERROR级别日志
func (l *Logger) Error(format string, args ...interface{}) {
l.writeLog(ERROR, format, args...)
}
日志文件切割优化
上面的实现中日志会一直写入同一个文件,文件会越来越大,我们可以增加按日期切割的逻辑,每天生成一个新的日志文件:
// rotateFile 按日期切割日志文件
func (l *Logger) rotateFile() error {
l.mu.Lock()
defer l.mu.Unlock()
// 关闭旧文件
if l.file != nil {
l.file.Close()
}
// 生成新的文件名,按日期区分
now := time.Now().Format("20060102")
newFilePath := fmt.Sprintf("%s_%s.log", l.filePath, now)
file, err := os.OpenFile(newFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("打开新日志文件失败: %v", err)
}
l.file = file
return nil
}
// 可以在每次写入前检查日期,或者启动一个定时任务每天切割
// 这里以定时任务为例,每天凌晨切割日志
func (l *Logger) StartRotate() {
go func() {
for {
now := time.Now()
// 计算到明天凌晨的时间
nextDay := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
time.Sleep(nextDay.Sub(now))
// 执行切割
if err := l.rotateFile(); err != nil {
fmt.Printf("日志切割失败: %vn", err)
}
}
}()
}
使用示例
下面是使用该日志系统的完整示例:
package main
import (
"test/logger" // 替换为你的logger包路径
)
func main() {
// 初始化日志,设置INFO级别,日志文件路径为./app.log
log, err := logger.NewLogger(logger.INFO, "./app.log")
if err != nil {
panic(err)
}
defer func() {
if log.File != nil { // 这里需要在Logger结构体中增加File的导出方法,或者调整结构
log.File.Close()
}
}()
// 启动日志按日切割
log.StartRotate()
// 写入不同级别的日志
log.Debug("这是一条DEBUG日志,不会输出")
log.Info("这是一条INFO日志,会输出")
log.Warn("这是一条WARN日志")
log.Error("这是一条ERROR日志")
// 模拟并发场景下的日志写入
for i := 0; i < 5; i++ {
go func(index int) {
log.Info("并发写入的日志,序号: %d", index)
}(i)
}
// 等待 goroutine 执行完成,实际项目中可以用sync.WaitGroup
time.Sleep(time.Second)
}
注意事项
- 如果需要在Logger结构体中导出文件句柄,可以给Logger增加一个GetFile方法,避免直接导出字段
- 日志文件路径如果需要支持相对路径,建议转换为绝对路径后再操作,避免路径错误
- 如果项目日志量很大,可以进一步优化写入逻辑,比如增加缓冲区批量写入,减少IO次数
- 正式项目中可以根据需求增加更多功能,比如日志压缩、按大小切割、日志上报等