mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
265 lines
7.1 KiB
Markdown
265 lines
7.1 KiB
Markdown
# 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` |
|
||
|
||
### 宽度与精度
|
||
|
||
```go
|
||
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` 动词在双引号内打印字符串,使空字符串和控制字符可见:
|
||
|
||
```go
|
||
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
|
||
// 不好:变量格式字符串——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` 可以自动检查格式字符串:
|
||
|
||
```go
|
||
func Wrapf(err error, format string, args ...any) error
|
||
```
|
||
|
||
如果使用非标准名称,需要告知 `go vet`:
|
||
|
||
```bash
|
||
go vet -printfuncs=wrapf,statusf
|
||
```
|
||
|
||
---
|
||
|
||
## `fmt.Stringer` 接口
|
||
|
||
实现 `fmt.Stringer` 来控制类型在 `%v` 和 `%s` 下的显示方式:
|
||
|
||
```go
|
||
type fmt.Stringer interface {
|
||
String() string
|
||
}
|
||
```
|
||
|
||
```go
|
||
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 语法表示具有误导性或过于冗长的类型很有用:
|
||
|
||
```go
|
||
type fmt.GoStringer interface {
|
||
GoString() string
|
||
}
|
||
```
|
||
|
||
```go
|
||
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`:
|
||
|
||
```go
|
||
type fmt.Formatter interface {
|
||
Format(f fmt.State, verb rune)
|
||
}
|
||
```
|
||
|
||
```go
|
||
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` 会导致无限递归:**
|
||
|
||
```go
|
||
type MyString string
|
||
|
||
// BUG:无限递归——Sprintf 调用 String(),String() 又调用 Sprintf...
|
||
func (m MyString) String() string {
|
||
return fmt.Sprintf("MyString: %s", m) // 崩溃:栈溢出
|
||
}
|
||
```
|
||
|
||
修复方法——将接收者转换为其底层类型以打破方法集:
|
||
|
||
```go
|
||
func (m MyString) String() string {
|
||
return fmt.Sprintf("MyString: %s", string(m)) // 安全:string 没有 String()
|
||
}
|
||
```
|
||
|
||
此陷阱还适用于:
|
||
- 底层类型为 string、[]byte 或另一个 Stringer 的类型
|
||
- 任何使用 `%s` 或 `%v` 格式化 `self` 的 `String()` 方法
|
||
- 使用 `%#v` 格式化 `self` 的 `GoString()` 方法
|
||
|
||
```go
|
||
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)`;转换为底层类型 |
|