【PHPer转Go】fmt vs log/slog

同样是把消息输出,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 并发。


先一句话讲清楚本质

  1. fmt.Print 系列:并发不安全

    多个 goroutine 同时打印时,字符会穿插、混在一起、乱码、错位

  2. 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 不是原子操作,它分三步:

  1. 拼接字符串
  2. 写入缓冲区
  3. 刷到屏幕

多个 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 // 锁

执行流程:

  1. goroutine A 来打印 → 上锁
  2. 完整写完一整行日志
  3. 解锁
  4. 下一个 goroutine 才能继续写

同一时间,只有一个人在写日志 → 绝对不乱。


3. 最核心的原理(你必须懂)

Go 里面的输出 = 写文件(控制台也是文件)

多个 goroutine 同时写同一个文件(stdout),如果不加锁,一定会出现:

  • 字符穿插
  • 半行输出
  • 换行错位
  • 信息乱码

fmt 没有锁

所以它快,但不安全,只适合单线程调试。

log/slog 有锁

所以安全、不乱、能上线,适合生产环境。


4. 用 PHP 思维类比一下

你在 PHP 里:

  • 多个进程同时写同一个日志文件
  • 必须加文件锁,否则日志会乱
  • PHP 的 error_log()Monolog 都自动加锁

Go 也是一样:

  • fmt = 不加锁的 echo
  • log/slog = 加锁的专业日志库

Go 因为 goroutine 超轻量,并发量比 PHP 大得多,所以乱码问题更严重!


5. 最终结论(你记这个就够)

生产环境、并发、goroutine 必须用:

log / slog

  • 自动加锁
  • 并发不乱
  • 日志完整
  • 带时间、级别、可格式化
绝对不要用:

fmt

  • 无锁
  • 并发乱码
  • 日志撕裂
  • 不能上线
  • 只适合本地临时调试

总结
  1. fmt 无锁 → 并发打印会乱、错位、穿插
  2. log/slog 有锁 → 并发永远干净、完整、不乱
  3. Go 高并发项目,日志只能用 slog/log
  4. 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,天然安全

直白讲透

  1. 除了并发锁,剩下全是"体力活"

    你嫌 slog/log 麻烦,完全可以封装一个全局函数:

    • 手动取东八区时间
    • 手动拼接前缀
    • 手动拼接日志内容
      这些 fmt 全能做到,语法层面没有任何做不到的点。
  2. 唯一绕不开的痛点:输出不是原子的

    go 复制代码
    // 就算你自己加了时间,依旧会乱
    fmt.Printf("%s 业务日志:%s\n", NowCST().Format("2006-01-02 15:04:05"), msg)

    执行拆分:

    1. 取时间
    2. 格式化字符串
    3. 写入stdout缓冲区
    4. 刷出换行
      多goroutine执行时,任意两步之间都能被插队,最终日志半截穿插、顺序错乱。
  3. 想用fmt做到和log一模一样?等于复刻锁逻辑

    你要自己全局定义一把锁:

    go 复制代码
    var 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、场景取舍

  1. 本地单协程调试
    随便用 fmt,省事无压力,乱不乱无所谓。
  2. 线上服务、常驻进程、大量goroutine
    无脑用 slog
    • 不用自己维护全局锁
    • 不用自己统一时间时区
    • 结构化日志方便后续收集检索
    • 底层锁逻辑经过海量项目验证,零出错

相关推荐
漓漾li4 小时前
每日面试题(2026-05-20)- GO AI agent全栈
后端·架构·go
HMS工业网络4 小时前
STP、RSTP到N-Ring的演进之路
服务器·开发语言·php
qq_543447824 小时前
Tcping测速是什么?Tcping测速核心概念解析
服务器·网络·php
Mr数据杨8 小时前
AIGC工具平台-StoryBoard故事板
人工智能·aigc·php
IronMurphy8 小时前
Redis拷打第七讲(最终章)
数据库·redis·php
.魚肉9 小时前
Raft 共识算法 · 演示系统(多终端)
算法·go·raft·分布式系统
marsh02069 小时前
49 openclaw故障排查:系统异常时的诊断方法
服务器·前端·青少年编程·ai·php·技术美术
审判长烧鸡11 小时前
【Go工具】go-playground除了validator还有哪些常用的库
go·web
审判长烧鸡11 小时前
Go 新版核心知识点合集(适配 Go1.18+ 含泛型 + 断言 + 接口 + 指针接收者全套)
go