结论:%v 默认格式(结构体只给字段值);%+v 结构体带字段名;%#v Go 语法表示(带类型名,可直接当源码读);%T 输出动态类型;%q 带双引号转义的字符串(或带引号的 rune)。排障打印结构体时 %+v 是默认选择,需要完整类型信息用 %#v

展开:示例对照:

type User struct{ Name string; Age int }
u := User{"张三", 30}
fmt.Printf("%v\n", u)   // {张三 30}
fmt.Printf("%+v\n", u)  // {Name:张三 Age:30}
fmt.Printf("%#v\n", u)  // main.User{Name:"张三", Age:30}
fmt.Printf("%T\n", u)   // main.User
fmt.Printf("%q\n", "a\tb") // "a\tb"

配套高频动词:%d 整数、%s 字符串、%t 布尔、%p 指针地址、%x 十六进制([]byte 打 hex 神器)、%w 仅在 fmt.Errorf 中包装错误(错误链可 errors.Unwrap/Is/As)。自定义输出:实现 String() string(fmt.Stringer)接管 %v/%sError() 同理;实现 Format(f State, c rune) 可区分动词。易错点:1)String() 里直接 %s 打印自己会无限递归,需转类型切断:fmt.Sprintf("%s", MyType(x));2)%v 打印 map 在 1.12+ 按键排序输出,老版本无序;3)%d 打印 string 得到一堆码点数字而非报错——vet 的 printf 检查能抓这类错。

追问方向:fmt 为何慢(反射 + interface{} 装箱)、zerolog/slog 如何避免格式化开销。