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手动拼。
Print vs Println 的区别
Print:不添加空格,不添加换行(除非格式化字符串包含\n)。Println:在相邻非字符串参数间自动加空格,末尾自动加\n。
2. 格式化占位符速查
通用占位符
| 占位符 | 说明 | 示例 |
|---|---|---|
%v |
默认格式 | fmt.Sprintf("%v", user) → {Alice 30} |
%+v |
结构体带字段名 | {Name:Alice Age:30} |
%#v |
Go 语法表示 | main.User{Name:"Alice", Age:30} |
%T |
值的类型 | main.User |
%% |
字面百分号 | % |
🔬 深入原理:
%v会调用String() string方法(如果实现了fmt.Stringer);%+v和%#v则不会。如果你发现一个类型的%v输出和预期不同,检查它是否实现了String()。
整数占位符
| 占位符 | 说明 | 示例 (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并控制精度。
字符串/字节切片
| 占位符 | 说明 | 示例 ("hi") |
|---|---|---|
%s |
原始字符串 | hi |
%q |
双引号转义字符串 | "hi" |
%x |
十六进制(小写,每字节两字符) | 6869 |
%X |
十六进制(大写) | 6869 |
指针
| 占位符 | 说明 | 示例 |
|---|---|---|
%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"⚡ 性能提示:宽度/精度是编译时解析的,基本无运行时开销。
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) // 字符串解析
fmt.Sscan("42\nhello", &n, &s) // 空格分隔
// Fscan — 从任意 io.Reader 读取(文件、网络连接、bytes.Buffer 等)
r := strings.NewReader("hello 42")
fmt.Fscanf(r, "%s %d", &name, &age) // 从 Reader 解析
fmt.Fscan(r, &name) // 空格分隔
fmt.Fscanln(r, &name, &age) // 换行分隔🚨 陷阱:
Scan系列函数对空白字符的处理很不直观——换行符也当空白。- 强烈建议始终使用
bufio.Scanner或bufio.Reader代替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 的代价主要来自:
- 参数装箱为
interface{}— 每个参数都需要分配一个any值。 - 反射 —
%v和%T需要运行时反射。 - buffer 分配 — 内部的
pp结构每次都需要从 pool 获取。
⚡ 性能提示:热路径中避免
fmt.Sprintf,用strconv+strings.Builder替代。
但 fmt 也有优化
fmt.Printf的内部 buffer 使用了sync.Pool复用 pp 对象。- 常用的
%s、%d、%f不需要反射,速度还可以。 - Go 1.20+ 对
fmt.Sprintf做了大量优化,简单场景已经很快。
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()。String()不应该调用fmt.Sprint(self),会造成无限递归。
7. 🚨 常见陷阱
陷阱 1:nil 指针的 %v 行为
var p *int = nil
fmt.Printf("%v\n", p) // <nil> — 输出字符串 "<nil>"
// 但如果是接口的 nil:
var w io.Writer = (*os.File)(nil)
fmt.Printf("%v\n", w) // <nil> 或具体类型,取决于…
// 最佳实践:用 %p 打印指针,%v 打印值陷阱 2:fmt.Println 的自动空格
fmt.Println("a", "b", "c") // "a b c\n" — 有空格
fmt.Print("a", "b", "c") // "abc" — 无空格陷阱 3:忽略返回值
// 错误 — Printf 返回 (n int, err error)
fmt.Printf("hello %s") // 编译可能不报错,但运行时输出 "hello %!s(MISSING)"
// 规范:始终提供匹配的参数