Skip to content
Go
fmt — 格式化输入与输出

fmt — 格式化输入与输出

fmt 是 Go 中使用频率最高的包之一,提供了格式化 I/O 的完整能力。


1. 输出函数族

三类输出目标

目标 典型函数
Print 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.Scannerbufio.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 的代价主要来自:

  1. 参数装箱为 interface{} — 每个参数都需要分配一个 any 值。
  2. 反射%v%T 需要运行时反射。
  3. 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)"
// 规范:始终提供匹配的参数