
当结构体实现了 String() string 方法,又在该方法内使用 fmt.Sprintf("%+v", s) 打印自身时,会触发无限递归——因为 fmt 包在格式化时会自动调用 String() 方法,从而形成循环调用,最终导致栈溢出。
当结构体实现了 `string() string` 方法,又在该方法内使用 `fmt.sprintf("%+v", s)` 打印自身时,会触发无限递归——因为 `fmt` 包在格式化时会自动调用 `string()` 方法,从而形成循环调用,最终导致栈溢出。
这个问题的本质在于 Go 的 fmt 包对 Stringer 接口(即实现 String() string 的类型)的隐式递归调用机制:只要你在 String() 方法中使用 %v、%+v、%#v 等任意 fmt 格式动词对当前类型实例进行格式化,fmt 就会再次调用该实例的 String() 方法——而非降级为默认结构体字段打印。这导致 ConfigOne.String() → fmt.Sprintf → 再次调用 ConfigOne.String() → 无限循环,直至 goroutine 栈空间耗尽(默认约 1GB),抛出 fatal error: stack overflow。
你观察到 c.Daemon 单独打印正常,正是因为 daemon 类型(注意首字母小写,未导出)未实现 String() 方法,fmt 会直接展开其字段;而 ConfigOne 实现了 String(),一旦 fmt 遇到它,就立即回调,陷入死循环。
✅ 正确做法是:在 String() 方法中,避免对自身类型使用 fmt 的通用格式动词。应显式访问字段,手动拼接字符串。例如:
func (c ConfigOne) String() string {
return fmt.Sprintf("ConfigOne{Daemon: daemon{Loglevel: %d, Logfile: %q}}",
c.Daemon.Loglevel,
c.Daemon.Logfile)
}
若结构体较复杂,可借助 fmt.Sprintf 对非自身类型的字段安全格式化(如基础类型、标准库类型、或未实现 Stringer 的自定义类型),但绝不可传入 c 或 &c。
⚠️ 注意事项:
- 不要试图用
fmt.Sprintf("%#v", c)或spew.Sdump(c)替代——它们同样会触发String(); - 若需调试输出,建议单独定义
DebugString()方法,不实现Stringer接口; - 导出类型名必须大写(如
Daemon而非daemon),否则跨包不可见(虽然本例中daemon是内部字段,不影响逻辑,但命名规范值得重视); -
String()应保持轻量、无副作用、线程安全,避免调用可能阻塞或 panic 的逻辑。
总结:String() 是 fmt 的“入口钩子”,不是普通工具函数。它的唯一职责是以字符串形式描述当前值,且必须绕过 fmt 对自身的再格式化。手动构造字符串虽稍显繁琐,却是规避栈溢出最可靠、最符合 Go 设计哲学的方式。










