fmt.Stringer 用于人眼可读的友好输出(如日志),由 %v/%s 触发;fmt.GoStringer 用于生成合法 Go 语法字面量,仅 %#v 触发;二者签名严格、互不干扰,须按语义区分实现。

fmt.Stringer 是给用户看的,默认用 %v/%s 时触发
当你想让结构体在日志、调试输出或普通 fmt.Println 中显示得更友好,就实现 String() 方法。它面向的是“人眼可读”,比如隐藏敏感字段、加前缀、格式化成一行描述。
常见错误现象:fmt.Print 或 fmt.Printf("%v", x) 没有按预期输出,往往是因为没导出方法(接收者是值但指针被传入,或反之),或者方法签名写成了 String() String 这类类型错误。
- 必须是
func (t T) String() string或func (t *T) String() string,不能带额外参数或返回多个值 - 如果结构体字段含未导出字段(小写开头),
String()是唯一能控制其输出的方式——否则%+v会显示为age:0这类零值,且无法跳过 -
fmt.Print、fmt.Println、fmt.Sprintf("%v", x)、fmt.Sprintf("%s", x)都会调用它;但fmt.Printf("%#v", x)不会
fmt.GoStringer 是给 Go 程序员/调试器看的,只在 %#v 时触发
GoString() 的目标不是可读性,而是“能直接粘贴进 Go 代码里跑通”。它返回的是该值的 Go 语法字面量表示,比如 &Person{Name:"alice", age:25},常用于生成测试数据、序列化调试快照、或配合 go-spew 类工具。
容易踩的坑:误以为 %v 或 %+v 也会走 GoString() —— 实际上只有 %#v 才触发它,其他动词完全无视这个方法。
- 签名必须是
func (t T) GoString() string或func (t *T) GoString() string - 返回值必须是合法 Go 语法:比如结构体要带类型名、字符串要加引号、
nil指针要显式写出(*T)(nil) - 如果同时实现了
Stringer和GoStringer,%#v只走后者,%v只走前者,互不干扰
两者共存时的调用优先级和冲突点
fmt 包内部用 handlerMethods 判断调用链,顺序固定:GoStringer → Stringer → 默认格式。但注意:这个“优先级”只在匹配动词时生效,不是覆盖关系。
-
%#v:只查GoStringer,查不到才 fallback 到默认(即{Field: value}形式) -
%v/%s:只查Stringer,查不到才 fallback 到默认 - 不要在一个类型里同时实现两个接口却返回相同内容——既浪费又误导,比如都返回 JSON 字符串,那
%#v就失去了“可粘贴进代码”的意义 - 如果
GoString()返回非法 Go 语法(如漏掉引号、类型名拼错),fmt.Printf("%#v", x)仍会输出,但结果不可用,且无编译期检查
为什么不能只靠 fmt.Printf 的动词切换来替代接口?
动词解决的是“怎么展开展示”,而接口解决的是“展开展示成什么内容”。比如 %+v 能打出字段名,但它无法隐藏密码字段、无法把时间转成 ISO8601、也无法把嵌套结构压缩成一行 ID。这些逻辑必须下沉到 String() 中。
-
%+v和%#v是通用机制,对所有类型一视同仁;Stringer是定制能力,每个类型自己决定“我看起来应该像什么” - 性能上,
String()是一次计算,%+v是运行时反射遍历字段——对高频日志场景,前者开销更低、更可控 - 跨包使用时,别人 import 你的类型后,
fmt.Println(yourType{})自动友好,不需要文档说明“请用 %+v”
User 类型,String() 返回 "User(123)" 很合理,但 GoString() 如果也返回这个,就彻底废掉了 %#v 的调试价值。区分“给人看”和“给代码看”,比记住函数签名重要得多。

















