%v 和 %+v 的区别在于:对结构体,%v 仅输出字段值如 {123 "hello"},%+v 输出带字段名的格式如 {a:123 b:"hello"};对 map、slice 等其他类型二者行为完全相同。

fmt.Printf 里 %v 和 %+v 到底差在哪?
区别在于结构体字段的显示方式:%v 只输出值,%+v 会带上字段名。很多人以为 %+v 是“更详细”,其实它只对 struct 生效,其他类型(比如 map、slice)和 %v 行为完全一样。
- 用 %v 打印
struct{A int; B string}得到{123 "hello"} - 用 %+v 打印同一结构体得到
{A:123 B:"hello"},字段名清晰可见 - 对
map[string]int或[]int来说,%v 和 %+v 输出一模一样,别指望 %+v 给 map 加 key 名 - 调试时想快速确认字段赋值是否正确,优先用 %+v;日志里要控制体积或兼容旧解析逻辑,就别用 %+v
为什么 %s 有时 panic:interface{} 不能直接用 %s 打印?
因为 %s 要求参数是 string 类型,传入 interface{}(比如 fmt.Printf("%s", anyVar) 中的 anyVar 是 interface{})会导致运行时报错:panic: runtime error: invalid memory address or nil pointer dereference —— 实际是底层尝试调 .String() 方法失败。
- 安全做法:显式转成
string,比如fmt.Printf("%s", stringVal),确保stringVal类型就是string - 如果变量是
interface{}且可能为字符串,先做类型断言:if s, ok := v.(string); ok { fmt.Printf("%s", s) } - 想“兜底”输出任意值,用
%v更稳妥,它专为interface{}设计 - 注意:
%q也要求string,同样不接受裸interface{}
fmt.Sprintf 性能比字符串拼接慢很多?真相比你想象的简单
不是“慢很多”,而是有明确代价:每次调用 Sprintf 都会分配新字符串,并触发格式解析逻辑。在 hot path(比如循环内、高频日志)中,这确实会成为瓶颈。
- 简单拼接如
"id:" + strconv.Itoa(id) + ",name:" + name在已知类型时几乎零分配,更快 -
Sprintf的优势在于可读性和类型安全——不用手动转类型,也不怕类型错位 - 如果必须用格式化且性能敏感,考虑预分配
strings.Builder+fmt.Fprint系列函数 - Go 1.22+ 对
Sprintf做了小优化,但没改变本质:它仍是通用方案,不是高性能原语
自定义类型怎么让 %v 输出更友好?实现 String() 方法就行
只要实现了 String() string 方法,任何类型在用 %v(或 %s)打印时都会自动调这个方法,而不是输出默认的 {...} 结构。
立即学习“go语言免费学习笔记(深入)”;
- 方法签名必须严格是
func (t T) String() string,接收者不能是指针(除非你明确想只对指针生效) - 避免在
String()里再调用fmt.Sprint自身,容易无限递归 - 如果类型同时实现了
error接口,%v会优先走Error()而非String() - 调试时想临时改输出,可以加个带标记的字段,比如
DebugMode bool,在String()里按需返回不同格式
Sprintf,什么时候该绕开它;还有,别在 String() 方法里打日志或者调网络——它可能被任何一次 fmt 调用悄悄触发。










