fmt 包有三组打印函数,每组都有相同的三种变体:
| 函数 | 输出到 | 格式 |
|---|---|---|
Print、Println、Printf | 标准输出 | 默认格式、带空格和换行的默认格式、格式字符串 |
Sprint、Sprintln、Sprintf | 返回的 string | 同样三种风格 |
Fprint、Fprintln、Fprintf | 任意 io.Writer(文件、缓冲区、HTTP 响应) | 同样三种风格 |
Errorf | 返回的 error | 格式字符串,另外支持用于包装的 %w |
Println 会在操作数之间加空格,并在末尾加换行。Printf 什么都不加:\n 要你自己写。Print 只在两个相邻操作数都不是字符串时才加空格,这一点出人意料,所以大多数代码都用 Println 或 Printf。
格式化动词
通用
| 动词 | 打印内容 | 示例输出 |
|---|---|---|
%v | 默认格式的值 | {Ana 31 [admin]} |
%+v | 带字段名的结构体 | {Name:Ana Age:31 Tags:[admin]} |
%#v | 值的 Go 语法表示 | main.User{Name:"Ana", Age:31, Tags:[]string{"admin"}} |
%T | 类型 | main.User |
%% | 字面意义的百分号 | % |
调试时首选 %+v。指向结构体的指针会打印成 &{...},而不是一个地址。map 打印时按键排序,所以即使 map 的遍历顺序是随机的,输出也是稳定的。
整数
| 动词 | 含义 | fmt.Sprintf(verb, 255) |
|---|---|---|
%d | 十进制 | 255 |
%b | 二进制 | 11111111 |
%o | 八进制 | 377 |
%O | 带 0o 前缀的八进制 | 0o377 |
%x / %X | 十六进制,小写或大写 | ff / FF |
%#x | 带 0x 前缀的十六进制 | 0xff |
%c | 该码点对应的字符 | ÿ |
%q | 带单引号的字符字面量 | 'ÿ' |
%U | Unicode 格式 | U+00FF |
浮点数
| 动词 | 含义 | fmt.Sprintf(verb, 1234.5678) |
|---|---|---|
%f | 十进制小数,默认 6 位 | 1234.567800 |
%.2f | 十进制小数,2 位 | 1234.57 |
%e | 科学计数法 | 1.234568e+03 |
%g | %e 或 %f 中较短的一种,不带末尾的 0 | 1234.5678 |
%v | 与 %g 相同 | 1234.5678 |
%.2f 是对浮点数精确的二进制值做舍入,所以 fmt.Sprintf("%.2f", 2.675) 得到 2.67:最接近 2.675 的 float64 比它略小一点。永远不要用浮点数来格式化金额;用整数保存以分为单位的金额。
字符串与字节
| 动词 | 含义 | fmt.Sprintf(verb, "go\n") |
|---|---|---|
%s | 原样输出字符串 | go 加一个换行 |
%q | 带双引号,显示转义字符 | "go\n" |
%x | 每个字节的十六进制 | 676f0a |
% x | 带空格的十六进制 | 67 6f 0a |
对 []byte 使用 %s 会把它当作文本打印;%v 打印的是数字([104 105])。
其他类型
| 动词 | 类型 | 打印内容 |
|---|---|---|
%t | bool | true 或 false |
%p | 指针、切片、map、channel、函数 | 地址,比如 0xc000012345 |
%w | error(仅限 Errorf) | 错误消息,同时包装该错误 |
宽度、精度与填充
在 % 和动词之间,可以放标志、宽度和精度:
| 写法 | 效果 |
|---|---|
%5d | 宽度为 5,右对齐(左侧用空格填充) |
%-5d | 宽度为 5,左对齐 |
%05d | 用 0 填充 |
%.2f | 小数点后保留 2 位 |
%8.2f | 宽度为 8,保留 2 位小数 |
%.3s | 最多取字符串的 3 个字符 |
%+d | 始终显示正负号 |
%*d | 宽度取自下一个参数 |
注意,2.5 用 %.0f 打印出来是 2:这里 Go 采用四舍六入五成双。对字符串来说,宽度按 rune 计数,而不是按显示列数,所以中日韩字符和 emoji 仍可能让表格错位。要对齐长度不一的文本列,可以交给 text/tabwriter 去计算宽度。
参数索引
%[n] 按位置选取参数,这样同一个参数就可以重复使用:
fmt.Printf("%[2]s %[1]s\n", "world", "hello") // hello world
fmt.Printf("%d %[1]x %[1]b\n", 10) // 10 a 1010
Errorf 与 %w
fmt.Errorf 根据格式字符串构造一个 error。配合 %w,它还会包装另一个错误,让调用方仍能识别出原错误:
调用方可能需要检查原因时用 %w,有意隐藏原因时用 %v。从 Go 1.20 开始,一次 Errorf 调用可以包含多个 %w。错误处理一页会深入讲解错误包装。
用 String() 自定义格式
任何带有 String() string 方法的类型,都能控制 %v、%s 和 Println 如何显示它:
%d 会绕过 String(),打印底层的数字。对于错误类型,对应的方法是 Error() string,它的优先级高于 String()。
动词用错时
fmt 永远不会因为格式错误而 panic。它会把问题直接打印在输出里:
fmt.Printf("%d\n", "oops")
fmt.Printf("%d %d\n", 1)
fmt.Printf("%d\n", 1, 2)
%!d(string=oops)
1 %!d(MISSING)
1
%!(EXTRA int=2)
这样的输出往往会一路带到生产环境,因为它不会让任何东西崩溃。go vet 能在构建时发现这三种问题:
./main.go:8:2: fmt.Printf format %d has arg "oops" of wrong type string
./main.go:9:2: fmt.Printf format %d reads arg #2, but call has 1 arg
./main.go:10:2: fmt.Printf call needs 1 arg but has 2 args
性能说明
fmt 把每个参数都当作 any 接收,并在运行时检查其类型(遇到结构体、切片和 map 时会退回到反射),这用于日志和输出没有问题,但在紧凑的循环里就会产生可测量的开销。转换单个数字时,strconv.Itoa 和 strconv.FormatFloat 比 Sprintf 更快。在循环中构建长字符串时,用 fmt.Fprintf(&b, ...) 写入 strings.Builder,而不是把 Sprintf 的结果拼接起来。
常见问题
Go 中 Println、Printf 和 Sprintf 有什么区别?
fmt.Println 打印各个参数,参数之间用空格分隔,末尾加换行。fmt.Printf 按格式字符串打印,不加换行。fmt.Sprintf 的格式化方式和 Printf 相同,但把结果作为字符串返回,而不是打印出来。fmt.Errorf 也一样,只是返回一个 error。
在 Go 中怎样打印带字段名的结构体?
用 %+v:fmt.Printf("%+v\n", user) 打印 {Name:Ana Age:31}。%v 只打印值,即 {Ana 31};%#v 打印包含类型的 Go 语法,即 main.User{Name:"Ana", Age:31}。
在 Go 中怎样把浮点数格式化为两位小数?
用 %.2f:fmt.Sprintf("%.2f", 3.14159) 返回 "3.14"。加上宽度可以对齐列,如 %8.2f;加上负号则左对齐,如 %-8.2f。strconv.FormatFloat(f, 'f', 2, 64) 不用格式字符串也能得到同样的结果。
fmt.Errorf 中的 %w 有什么作用?
%w 像 %v 一样格式化错误,同时还会包装它,让新错误携带原错误。之后 errors.Is 和 errors.As 就能找到被包装的错误:err := fmt.Errorf("load config: %w", os.ErrNotExist) 会让 errors.Is(err, os.ErrNotExist) 为 true。%w 只在 fmt.Errorf 中有效。
为什么我的输出里出现了 %!d(string=...)?
动词和参数类型不匹配,比如给 %d 传了一个字符串。fmt 不会 panic,而是把问题直接打印在输出里:%!d(string=oops)。缺少参数时打印 %!d(MISSING),参数多余时打印 %!(EXTRA int=2)。go vet 能在你运行程序之前发现这三种问题。