同样是把消息输出,fmt 与 log/slog 有什么不同?
为什么 Go 里大家都用 log/slog,而不是 fmt ------ 完全不是玄学,是工程必须。
一句话核心结论
fmt 是给人看的输出工具,不是日志工具;
log/slog 是给程序用的日志系统,能上线、能排查、能运维。
你在 PHP 里绝对不会用 echo 当线上日志吧?
fmt 就等于 PHP 的 echo / print
log/slog 等于 PHP 的 error_log()、Monolog
一. 最关键的区别:自带时间
你刚才纠结时区,就是因为这个:
go
func main() {
fmt.Println("app running")
log.Println("app running")
slog.Info("app runing ")
}
打印输出
app running
2026/05/19 10:15:39 app running
2026/05/19 10:15:39 INFO app runing
二. 日志级别(线上必备)
go
slog.Info("启动成功")
slog.Error("连接失败", err)
slog.Debug("调试信息")
日志可以:
- 只看错误
- 屏蔽调试
- 按级别过滤
- 告警系统自动抓错误
fmt 做不到,它只会无脑输出文字。
三. 格式统一(分布式系统必须)
log/slog 可以统一:
- 时间格式(你刚配置的东八区)
- 输出结构(JSON)
- 增加字段:服务名、追踪ID、IP
json
{"time":"2025-04-05 15:30:25","level":"INFO","msg":"启动成功"}
运维、监控、ELK 只认这种日志。
fmt 输出乱七八糟的字符串,机器无法解析。
四. 输出位置可以改(fmt 只能打印屏幕)
log可以输出到:文件、远程日志服务、控制台- 生产环境不可能一直看屏幕
fmt永远只能输出到控制台
五. 并发安全
log 在多 goroutine 并发下不会乱码
fmt 在高并发下会打印错乱
为什么 fmt 并发会乱,log/slog 不会乱。
你是 PHP 转 Go,这个点非常关键,因为 PHP 基本没有真正的并发,Go 遍地都是 goroutine 并发。
先一句话讲清楚本质
-
fmt.Print系列:并发不安全多个 goroutine 同时打印时,字符会穿插、混在一起、乱码、错位。
-
log/slog系列:并发安全内部加了锁(Mutex) ,同一时间只允许一个 goroutine 写日志,
输出永远完整、干净、不乱码。
我直接给你看对比(你复制就能跑)
1. 先用 fmt 并发打印(会乱)
go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// 10 个 goroutine 同时打印
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// 高并发同时输出
fmt.Printf("我是 goroutine %d,我在执行任务\n", n)
}(i)
}
wg.Wait()
}
你会看到这种乱套的输出(真实运行结果):
我是 goroutine 我是 goroutine 2,我在执行任务
1,我在执行任务
我是 goroutine 3,我在执行任务我是 goroutine 4,我在执行任务
我是 goroutine 5,我在执行任务
我是 goroutine 我是 goroutine 7,我在执行任务
6,我在执行任务
为什么乱?
fmt.Printf 不是原子操作,它分三步:
- 拼接字符串
- 写入缓冲区
- 刷到屏幕
多个 goroutine 会在这三步中间互相插队,导致文字撕裂、穿插、混行。
2. 换成 log / slog(完全不乱)
go
package main
import (
"log"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// log 自带锁
log.Printf("我是 goroutine %d,我在执行任务", n)
}(i)
}
wg.Wait()
}
输出永远是干净、整齐、完整的:
2025/04/05 15:30:00 我是 goroutine 0,我在执行任务
2025/04/05 15:30:00 我是 goroutine 1,我在执行任务
2025/04/05 15:30:00 我是 goroutine 2,我在执行任务
2025/04/05 15:30:00 我是 goroutine 3,我在执行任务
为什么不乱?
因为 log 内部有一把 互斥锁(sync.Mutex):
go
mu sync.Mutex // 锁
执行流程:
- goroutine A 来打印 → 上锁
- 完整写完一整行日志
- 解锁
- 下一个 goroutine 才能继续写
同一时间,只有一个人在写日志 → 绝对不乱。
3. 最核心的原理(你必须懂)
Go 里面的输出 = 写文件(控制台也是文件)
多个 goroutine 同时写同一个文件(stdout),如果不加锁,一定会出现:
- 字符穿插
- 半行输出
- 换行错位
- 信息乱码
fmt 没有锁
所以它快,但不安全,只适合单线程调试。
log/slog 有锁
所以安全、不乱、能上线,适合生产环境。
4. 用 PHP 思维类比一下
你在 PHP 里:
- 多个进程同时写同一个日志文件
- 必须加文件锁,否则日志会乱
- PHP 的
error_log()、Monolog都自动加锁
Go 也是一样:
fmt= 不加锁的echolog/slog= 加锁的专业日志库
Go 因为 goroutine 超轻量,并发量比 PHP 大得多,所以乱码问题更严重!
5. 最终结论(你记这个就够)
生产环境、并发、goroutine 必须用:
log / slog
- 自动加锁
- 并发不乱
- 日志完整
- 带时间、级别、可格式化
绝对不要用:
fmt
- 无锁
- 并发乱码
- 日志撕裂
- 不能上线
- 只适合本地临时调试
总结
fmt无锁 → 并发打印会乱、错位、穿插log/slog有锁 → 并发永远干净、完整、不乱- Go 高并发项目,日志只能用 slog/log
- fmt 只适合本地调试,绝不允许写进正式代码
如果你愿意,我可以把 slog 最终完整版(带东八区+JSON+并发安全+生产可用) 直接给你,你复制到项目里就能永久用。
六. 你最关心的:大家真的都这样吗?
100% 的 Go 项目,线上绝对不用 fmt 打日志。
- 调试用
fmt - 正式代码、服务、上线 → 必须用
log/slog
这就像:
- PHP 调试用
echo - 上线必须用
Log::info()或error_log()
终极总结(最直白版)
fmt.Println()
= 临时调试用
= 没有时间
= 没有级别
= 不能运维
= 不能上线
= 一次性草稿纸
log / slog
= 正式日志
= 自动带时间(你刚配置好+8)
= 自动带级别
= 可格式化
= 可存文件
= 可被监控识别
= 生产环境标准
最简单记忆法
想写日志 → 用 slog
想临时看一眼变量 → 用 fmt
头铁版劝离
所有表面功能 fmt 手写全都能复刻出来,唯独原子输出+内置互斥锁,并发场景下的串行互斥输出,是日志库不可替代的核心价值。
逐项对比
| 需求 | fmt 手动能否实现 | log/slog 自带 |
|---|---|---|
| 打印当前时间 | ✅ 能,自己拼 time.Now() |
✅ 自带 |
| 自定义时间格式/东八区 | ✅ 能自己格式化 | ✅ 可全局统一配置 |
| 拼接自定义字段 | ✅ 能拼接字符串 | ✅ 结构化参数更优雅 |
| 分级日志(Info/Error/Debug) | ✅ 自己写判断封装 | ✅ 原生支持 |
| 输出到文件/指定位置 | ✅ 改输出句柄就行 | ✅ 原生支持 |
| 多协程并发不串行、不乱码 | ❌ 原生不行,必须自己加锁封装 | ✅ 内部自带 sync.Mutex,天然安全 |
直白讲透
-
除了并发锁,剩下全是"体力活"
你嫌 slog/log 麻烦,完全可以封装一个全局函数:
- 手动取东八区时间
- 手动拼接前缀
- 手动拼接日志内容
这些fmt全能做到,语法层面没有任何做不到的点。
-
唯一绕不开的痛点:输出不是原子的
go// 就算你自己加了时间,依旧会乱 fmt.Printf("%s 业务日志:%s\n", NowCST().Format("2006-01-02 15:04:05"), msg)执行拆分:
- 取时间
- 格式化字符串
- 写入stdout缓冲区
- 刷出换行
多goroutine执行时,任意两步之间都能被插队,最终日志半截穿插、顺序错乱。
-
想用fmt做到和log一模一样?等于复刻锁逻辑
你要自己全局定义一把锁:
govar stdMu sync.Mutex func MyLog(msg string) { stdMu.Lock() defer stdMu.Unlock() t := time.Now().In(cstZone).Format("2006-01-02 15:04:05") fmt.Println(t, msg) }写到这一步,你手写的 MyLog 就和标准 log 原理完全一致了 。
既然官方已经封装好成熟稳定的锁机制、日志框架,没必要重复造轮子。
4、场景取舍
- 本地单协程调试
随便用fmt,省事无压力,乱不乱无所谓。 - 线上服务、常驻进程、大量goroutine
无脑用slog- 不用自己维护全局锁
- 不用自己统一时间时区
- 结构化日志方便后续收集检索
- 底层锁逻辑经过海量项目验证,零出错