闭包日志器核心是利用闭包捕获前缀字符串并返回兼容标准日志签名的函数,如func(...interface{}),通过append拼接前缀后调用fmt.Println,实现零依赖、无锁、轻量级自动加前缀;也可包装log.Logger的Writer()实现无缝集成。

闭包日志器的核心实现原理
Go 中的闭包能捕获外部作用域变量,正好用来固化前缀。关键不是“封装一个函数”,而是让每次调用 log.Println 时都自动拼上前缀——这要求闭包返回的是一个可直接调用的函数,且该函数内部持有前缀字符串。
常见错误是直接在闭包里调用 fmt.Printf 并返回结果,这样无法复用;或者把前缀塞进全局变量,破坏了闭包的隔离性。
- 前缀必须作为参数传入闭包外层函数,不能硬编码
- 返回的闭包函数签名应与标准日志函数一致(如
func(...interface{})),方便替换现有调用 - 避免在闭包内做 I/O 或锁操作,否则会掩盖前缀逻辑的轻量本质
最简可用的闭包日志器写法
不需要依赖第三方库,用原生 fmt 就够。下面这个函数返回带前缀的 func(...interface{}):
func NewPrefixLogger(prefix string) func(...interface{}) {
return func(v ...interface{}) {
fmt.Println(append([]interface{}{"[" + prefix + "]"}, v...)...)
}
}
使用方式:infoLog := NewPrefixLogger("INFO"),然后直接 infoLog("user login", "id=123")。输出:[INFO] user login id=123。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 注意
append后面的...:必须展开切片,否则fmt.Println会把整个切片当单个参数打印 - 如果要用
fmt.Printf做格式化,闭包内需额外处理v[0]是否为格式字符串,复杂度上升,一般不推荐 - 这个版本不支持
log.SetOutput等配置,纯函数式,适合轻量场景
对接标准 log.Logger 的闭包包装法
当项目已用 log.New 创建了 logger,又想加前缀,不能改调用点时,用闭包包装 log.Output 是更稳妥的做法:
func WithPrefix(logger *log.Logger, prefix string) *log.Logger {
return log.New(&prefixWriter{logger: logger, prefix: prefix}, "", 0)
}
type prefixWriter struct {
logger *log.Logger
prefix string
}
func (w *prefixWriter) Write(p []byte) (n int, err error) {
return w.logger.Writer().Write(append([]byte("["+w.prefix+"] "), p...))
}
这样生成的 *log.Logger 可以无缝替代原 logger,所有 logger.Printf、logger.Println 都自动带前缀。
-
log.Logger的Writer()方法返回底层io.Writer,直接复用避免重复缓冲 - 不要在
Write方法里调用logger.Print,否则递归死循环 - 注意
prefixWriter没有并发保护,若 logger 本身非线程安全,需确保外部已加锁
为什么不用装饰器模式或接口重写
有人试图定义 Logger 接口并实现带前缀的结构体,但 Go 标准库的 log.Logger 是具体类型,不是接口,强行包装反而增加间接调用和内存分配。
闭包的优势在于零分配(小对象逃逸少)、无接口抽象开销、调用链路最短。真正需要扩展功能(如分级、异步、采样)时,再考虑封装结构体;仅加前缀,闭包就是最直接的解法。
容易被忽略的是:前缀字符串若来自用户输入或配置,务必做 strings.TrimSpace 处理,否则可能产生 [ INFO ] 这类难调试的空白问题。

















