mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
7.1 KiB
7.1 KiB
Printf、Stringer 与自定义格式化
Go 的 fmt 打印动词、Stringer 和 GoStringer 接口、自定义 Format() 方法以及常见陷阱的深度参考。
Printf 动词
通用动词
| 动词 | 用途 |
|---|---|
%v |
默认格式(结构体字段、切片元素) |
%+v |
带字段名的结构体:{Name:alice Age:30} |
%#v |
Go 语法表示:main.User{Name:"alice", Age:30} |
%T |
值的类型:main.User |
%% |
字面百分号 |
字符串与字节动词
| 动词 | 用途 |
|---|---|
%s |
纯字符串或字节切片 |
%q |
带 Go 语法转义的引号字符串:"hello\n" |
%x |
十六进制编码,小写:68656c6c6f |
%X |
十六进制编码,大写:68656C6C6F |
整数动词
| 动词 | 用途 |
|---|---|
%d |
十进制整数 |
%b |
二进制 |
%o |
八进制 |
%O |
带 0o 前缀的八进制 |
%x |
十六进制,小写 |
%X |
十六进制,大写 |
浮点数动词
| 动词 | 用途 |
|---|---|
%f |
小数点,无指数:123.456 |
%e |
科学计数法:1.23456e+02 |
%g |
紧凑格式:大指数用 %e,否则用 %f |
宽度与精度
fmt.Sprintf("%10d", 42) // " 42" (宽度 10,右对齐)
fmt.Sprintf("%-10d", 42) // "42 " (宽度 10,左对齐)
fmt.Sprintf("%.2f", 3.14159) // "3.14" (2 位小数)
fmt.Sprintf("%010d", 42) // "0000000042" (零填充)
使用 %q 输出字符串
%q 动词在双引号内打印字符串,使空字符串和控制字符可见:
fmt.Printf("value %q looks like English text", someText)
// 不好:手动添加引号
fmt.Printf("value \"%s\" looks like English text", someText)
在面向人类的输出中,如果值可能为空或包含控制字符,优先使用 %q。
Printf 之外的格式字符串
在 Printf 风格调用之外声明格式字符串时,使用 const。这样 go vet 可以进行静态分析:
// 不好:变量格式字符串——go vet 无法检查
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
// 好:常量格式字符串——go vet 可以验证
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
Printf 风格函数的命名
接受格式字符串的函数应以 f 结尾。这样 go vet 可以自动检查格式字符串:
func Wrapf(err error, format string, args ...any) error
如果使用非标准名称,需要告知 go vet:
go vet -printfuncs=wrapf,statusf
fmt.Stringer 接口
实现 fmt.Stringer 来控制类型在 %v 和 %s 下的显示方式:
type fmt.Stringer interface {
String() string
}
type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
// fmt.Println(Point{1, 2}) → "(1, 2)"
// fmt.Sprintf("point: %v", p) → "point: (1, 2)"
// fmt.Sprintf("point: %s", p) → "point: (1, 2)"
何时实现 Stringer
- 类型将出现在日志消息或面向用户的输出中
- 默认的
%v输出(仅字段值)不够有意义 - 需要一种区别于序列化的、对人类友好的表示
fmt.GoStringer 接口
实现 fmt.GoStringer 来控制 %#v 输出。这对于默认 Go 语法表示具有误导性或过于冗长的类型很有用:
type fmt.GoStringer interface {
GoString() string
}
type Color struct{ R, G, B uint8 }
func (c Color) GoString() string {
return fmt.Sprintf("Color(%#02x, %#02x, %#02x)", c.R, c.G, c.B)
}
// fmt.Sprintf("%#v", Color{255, 128, 0})
// → "Color(0xff, 0x80, 0x00)" 而非 "main.Color{R:0xff, G:0x80, B:0x00}"
GoString() 的输出应该是有效的 Go 语法或接近有效语法——它用于调试,而非面向用户的显示。
使用 fmt.Formatter 自定义格式化
要完全控制所有格式动词,实现 fmt.Formatter:
type fmt.Formatter interface {
Format(f fmt.State, verb rune)
}
type Point struct{ X, Y int }
func (p Point) Format(f fmt.State, verb rune) {
switch verb {
case 'v':
if f.Flag('#') {
// %#v——Go 语法表示
fmt.Fprintf(f, "Point{X: %d, Y: %d}", p.X, p.Y)
return
}
if f.Flag('+') {
// %+v——带字段名的详细格式
fmt.Fprintf(f, "X:%d Y:%d", p.X, p.Y)
return
}
// %v——默认
fmt.Fprintf(f, "(%d, %d)", p.X, p.Y)
case 's':
fmt.Fprintf(f, "(%d, %d)", p.X, p.Y)
case 'q':
fmt.Fprintf(f, "%q", p.String())
default:
fmt.Fprintf(f, "%%!%c(Point=%d,%d)", verb, p.X, p.Y)
}
}
fmt.State 方法
| 方法 | 返回值 |
|---|---|
Flag(c int) bool |
标志(+、-、#、0、 )是否设置 |
Width() (int, bool) |
宽度值以及是否指定了宽度 |
Precision() (int, bool) |
精度值以及是否指定了精度 |
Write(b []byte) (int, error) |
写入输出字节 |
仅在 String() 不够用时才实现 fmt.Formatter——很少需要这样做。常见原因:需要为 %v、%+v、%#v 提供不同输出,或者需要遵循宽度/精度标志。
无限递归陷阱
在 String() 方法内部对接收者使用 %s 或 %v 调用 fmt.Sprintf 会导致无限递归:
type MyString string
// BUG:无限递归——Sprintf 调用 String(),String() 又调用 Sprintf...
func (m MyString) String() string {
return fmt.Sprintf("MyString: %s", m) // 崩溃:栈溢出
}
修复方法——将接收者转换为其底层类型以打破方法集:
func (m MyString) String() string {
return fmt.Sprintf("MyString: %s", string(m)) // 安全:string 没有 String()
}
此陷阱还适用于:
- 底层类型为 string、[]byte 或另一个 Stringer 的类型
- 任何使用
%s或%v格式化self的String()方法 - 使用
%#v格式化self的GoString()方法
type IPAddr [4]byte
// BUG:%v 调用 String(),无限递归
func (ip IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
// 这里安全——ip[0] 是 byte(uint8),没有 String() 方法。
// 但如果 ip 是一个包装了 Stringer 的命名类型,就会递归。
}
经验法则:在 String() 内部,永远不要将接收者(或重新转换为自身类型的接收者)传递给 %s 或 %v 动词。先转换为底层原始类型。
快速参考
| 主题 | 规则 |
|---|---|
%q |
用于人类可读的字符串输出 |
%+v |
带字段名的结构体 |
%#v |
Go 语法表示;通过 GoStringer 自定义 |
| 格式字符串存储 | 在 Printf 调用之外声明为 const |
| Printf 函数名 | 以 f 结尾以支持 go vet |
Stringer |
实现 String() string 用于 %v/%s 输出 |
GoStringer |
实现 GoString() string 用于 %#v 输出 |
Formatter |
实现 Format(fmt.State, rune) 以完全控制动词 |
| 递归陷阱 | 永远不要在 String() 内部使用 Sprintf("%s", receiver);转换为底层类型 |