Files
OpenFlare/.agents/skills/go-functions/references/PRINTF-STRINGER.md
T
2026-08-04 11:40:32 +08:00

7.1 KiB
Raw Blame History

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);转换为底层类型