fmt — 格式化输入与输出
fmt 是 Go 中使用频率最高的包之一,提供了格式化 I/O 的完整能力。
1. 输出函数族
三类输出目标
| 族 | 目标 | 典型函数 |
|---|---|---|
os.Stdout |
Print, Printf, Println |
|
| Sprint | 内存字符串 | Sprint, Sprintf, Sprintln |
| Fprint | 任意 Writer | Fprint, Fprintf, Fprintln |
// Print — 直接写到标准输出
fmt.Print("hello")
fmt.Printf("%s %d", "world", 42)
fmt.Println("with newline")
// Sprint — 返回字符串(常用在测试和错误拼接中)
s := fmt.Sprintf("user_%d", 123)
// Fprint — 写到任意 io.Writer(文件、网络连接、bytes.Buffer 等)
fmt.Fprintf(w, "format: %v", data)需要拼接包含数字/类型的字符串时,fmt.Sprintf 比 + strconv 手动拼更清晰;但热路径下它更快,见第 5 节的取舍。
Print vs Println 的区别
Print:不添加空格,不添加换行(除非格式化字符串本身含\n)。相邻操作数之间,只有当两边都不是字符串时才自动加空格。Println:在相邻操作数间自动加空格,末尾自动加\n。
fmt.Println("a", "b", "c") // "a b c\n"
fmt.Print("a", "b", "c") // "abc" — 相邻字符串之间不插空格
fmt.Print("a", 1) // "a 1" — 字符串与非字符串之间插空格2. 格式化占位符速查
通用占位符
| 占位符 | 说明 | 示例 |
|---|---|---|
%v |
默认格式 | fmt.Sprintf("%v", user) → {Alice 30} |
%+v |
结构体带字段名(对实现了 Stringer 的类型仍走 String) | {Name:Alice Age:30} |
%#v |
Go 语法表示(调用 GoStringer,不调用 String) | main.User{Name:"Alice", Age:30} |
%T |
值的类型 | main.User |
%% |
字面百分号 | % |
关于 Stringer 的准确说法(很多资料会讲错):%v 和 %+v 都会调用类型的 String() string 方法;只有 %#v 不调用 String,它转而调用 GoString()。%+v 的"带字段名"效果,只体现在没有实现 Stringer 的结构体上——一旦类型有 String(),%v 和 %+v 输出完全相同。
type T struct{ X int }
func (t T) String() string { return "STRINGER" }
t := T{1}
fmt.Printf("%v %+v %#v\n", t, t, t) // STRINGER STRINGER main.T{X:1}
// 注意 %+v 仍是 STRINGER,并不会变成 {X:1}如果既想要 %+v 的展开字段视图、类型又恰好实现了 String(),标准库没有直接开关——常见做法是临时打印一个无方法的影子结构体,或用 %#v。
整数占位符
| 占位符 | 说明 | 示例 (42) |
|---|---|---|
%d |
十进制 | 42 |
%b |
二进制 | 101010 |
%o |
八进制 | 52 |
%O |
八进制(0o前缀) | 0o52 |
%x |
十六进制(小写) | 2a |
%X |
十六进制(大写) | 2A |
%c |
Unicode 字符 | * |
%q |
单引号转义 | '*' |
%U |
Unicode 格式 | U+002A |
浮点数占位符
| 占位符 | 说明 | 示例 (3.14159) |
|---|---|---|
%f |
小数 | 3.141590 |
%e |
科学计数法(小写 e) | 3.141590e+00 |
%E |
科学计数法(大写 E) | 3.141590E+00 |
%g |
自动选择 %e 或 %f | 3.14159 |
注意:%g 会自动选择最紧凑表示(位数少时 %f、位数多时 %e),并且会去掉尾随的零。这在极值下可能丢掉你期望的小数位展示,金融/定长展示建议用 %f 并显式控制精度。
字符串/字节切片
| 占位符 | 说明 | 示例 ("hi\n") |
|---|---|---|
%s |
原始字符串 | hi\n(含真实换行) |
%q |
双引号转义字符串 | "hi\n" |
%x |
十六进制(小写,每字节两字符) | 68690a |
%X |
十六进制(大写) | 68690A |
用 %x/%X 打印字节序列时,选含 a-f 十六进制位的数据才能看出大小写差异——"hi" 恰好全是数字位,看不出 %x 与 %X 的区别。
指针
| 占位符 | 说明 | 示例 |
|---|---|---|
%p |
带 0x 的地址 | 0xc00001c030 |
3. 宽度与精度控制
// 宽度:最小输出宽度
fmt.Printf("|%5d|\n", 42) // | 42| 右对齐
fmt.Printf("|%-5d|\n", 42) // |42 | 左对齐(- 表示)
fmt.Printf("|%05d|\n", 42) // |00042| 零填充
// 精度:对浮点数是小数位数,对字符串是最大字符数
fmt.Printf("%.2f\n", 3.14159) // 3.14
fmt.Printf("%.3s\n", "hello") // hel
// 宽度 + 精度
fmt.Printf("%8.2f\n", 3.14159) // " 3.14"宽度和精度可以写成 * 由参数动态传入:fmt.Printf("%*d", 5, 42)。这种动态宽度在格式化表格输出时很实用。
4. 输入函数族
三类输入来源
与输出对称,输入函数也分为三类:
| 族 | 来源 | 典型函数 |
|---|---|---|
| Scan | os.Stdin |
Scan, Scanf, Scanln |
| Sscan | 内存字符串 | Sscan, Sscanf, Sscanln |
| Fscan | 任意 Reader | Fscan, Fscanf, Fscanln |
// Scan — 从标准输入读取
var name string
var age int
fmt.Scan(&name, &age) // 空白分隔,读到指针里
fmt.Scanf("%s %d", &name, &age) // 格式化输入
fmt.Scanln(&name, &age) // 读到行尾
// Sscan — 从字符串读取
fmt.Sscanf("Alice 30", "%s %d", &name, &age)
// Fscan — 从任意 io.Reader 读取
r := strings.NewReader("hello 42")
fmt.Fscanf(r, "%s %d", &name, &age)陷阱:
Scan系列把换行符也当普通空白处理,行为对新手很不直观。- 强烈建议交互式/逐行输入一律改用
bufio.Scanner或bufio.Reader,别用fmt.Scan。生产代码里fmt.Scan几乎总是错误的选择。 Scanf必须与输入精确匹配,格式串里一个多余空格都可能让后续解析失败。- 所有 Scan 函数都返回
(n int, err error):n是成功扫描的项数,err可能为io.EOF。忽略返回值会导致部分失败被静默吞掉。
5. fmt 的效率问题
为什么 fmt.Sprintf 比手动拼接"贵"?
// 相对慢(装箱 + 可能的反射)
s := fmt.Sprintf("value: %d", n)
// 快(无反射、通常无额外分配)
s := "value: " + strconv.Itoa(n)fmt.Sprintf 的代价主要来自:
- 参数装箱为
any— 每个参数都要封成一个接口值,小对象可能触发分配。 - 格式串解析与反射 —
%v/%T/自定义类型需要反射。 - 内部
pp状态 — 虽从sync.Pool复用,但仍有一次解析开销。
热路径里避免 fmt.Sprintf,用 strconv + strings.Builder 组装。
fmt 也在被持续优化
- 内部用
sync.Pool复用格式化状态。 %s、%d、%f这类常见动词走快路径,不走反射。- Go 1.20 之后
fmt的常见场景性能已有明显改善;fmt.Appendf(1.19)/fmt.Append可把结果追加进复用的[]byte,减少一次字符串分配。
结论:日志、错误信息拼接这类"每秒几十次"的场景完全不用纠结 fmt;只有在明确的性能热点上才值得手写 strconv。
6. 自定义类型的格式化
实现 fmt.Stringer
type User struct {
Name string
Age int
}
func (u User) String() string {
return fmt.Sprintf("User(%s, %d)", u.Name, u.Age)
}
u := User{"Alice", 30}
fmt.Println(u) // User(Alice, 30)实现 fmt.GoStringer(影响 %#v)
func (u User) GoString() string {
return fmt.Sprintf("User{Name:%q, Age:%d}", u.Name, u.Age)
}注意:String() 里绝不要用 %v 打印接收者本身(return fmt.Sprintf("%v", u)),那会无限递归调用自己——fmt 有递归检测会在几层后抛 “labelled or keyed …” 或 panic,但这是未定义行为。打印字段时用具体动词(u.Name、u.Age)。
7. 常见陷阱
陷阱 1:nil 指针/接口的 %v
var p *int = nil
fmt.Printf("%v\n", p) // <nil>
// 接口里包了一个 typed-nil 指针,fmt 也能识别并打印 <nil>
var w io.Writer = (*os.File)(nil)
fmt.Printf("%v\n", w) // <nil>fmt 对指针有专门的 nil 检测:即便接口里装的是"有类型的 nil",%v 也会打印 <nil> 而不是 panic。但这不代表接口等于 nil——w == nil 仍是 false(见 builtin.md 的接口 nil 一节)。打印行为与相等判断是两套逻辑,别混为一谈。
陷阱 2:fmt.Println 的自动空格
fmt.Println("a", "b", "c") // "a b c\n" — 有空格
fmt.Print("a", "b", "c") // "abc" — 相邻字符串间无空格陷阱 3:格式串与参数不匹配
fmt.Printf("hello %s") // 输出 "hello %!s(MISSING)",且返回 errPrintf 返回 (n int, err error)。参数缺失/多余不会阻止编译通过(除非用 go vet),但会在输出里留下 %!s(MISSING) / %(EXTRA...) 之类的标记,并让 err != nil。go test 默认会跑 printf vet 检查,能在 CI 拦住大部分这类错误。写日志/错误信息时不要忽略返回的 err。
8. 实战建议
- 需要"带字段名调试视图"时,用
%+v,但前提是类型没实现String();否则先临时去掉方法或用%#v。 - 打印二进制/hex 用
%x,打印可打印转义用%q,两者职责不同别混用。 - 输入处理优先
bufio,把fmt.Scan留给"临时脚本读一行数字"这种一次性场景。 - 性能敏感处用
strconv+strings.Builder,其余场景信任 fmt——过早优化 fmt 往往得不偿失。