Go 标准库的 fmt 包提供了类似于 C 风格 printf/scanf 的格式化 I/O 工具。这篇文章将学习 Go fmt 的基本使用方法和实现原理。
fmt 包的基本使用方法 格式化谓词 fmt 包对于打印功能提供了以下 格式化谓词
:
通用: :
%v:以默认格式打印值
%+v:类似于 %v
,但是在打印结构体时,%+v
会额外添加字段名
%#v:以 Go 语法格式打印值
%T:以 Go 语法格式打印值的类型
%%:打印 % 本身
如下是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" type Person struct { name string age uint } func main () { p := Person{ name: "Jack" , age: 20 , } fmt.Printf("%v\n" , p) fmt.Printf("%+v\n" , p) fmt.Printf("%#v\n" , p) fmt.Printf("%T\n" , p) }
布尔类型:
整数类型:
%b:二进制打印
%c:打印 Unicode 码点对应的字符
%d:十进制打印
%o:八进制打印
%O:八进制打印,使用 0o 作为前缀
%q:使用 Go 语法安全转义的单引号文本字符
%x:十六进制,小写形式
%X:十六进制,大写形式
%U:Unicode 格式:U+1234,等同于 “U+%04X”
如下是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package mainimport "fmt" func main () { bt, bf := true , false i := 65 c := '中' fmt.Printf("%t %t\n" , bt, bf) fmt.Printf("%d\n" , i) fmt.Printf("%b\n" , i) fmt.Printf("%o\n" , i) fmt.Printf("%O\n" , i) fmt.Printf("%x\n" , i) fmt.Printf("%X\n" , i) fmt.Printf("%c\n" , i) fmt.Printf("%U\n" , i) fmt.Printf("%q\n" , i) fmt.Printf("%d\n" , c) fmt.Printf("%b\n" , c) fmt.Printf("%o\n" , c) fmt.Printf("%O\n" , c) fmt.Printf("%x\n" , c) fmt.Printf("%X\n" , c) fmt.Printf("%c\n" , c) fmt.Printf("%U\n" , c) fmt.Printf("%q\n" , c) }
浮点类型和复数类型:
%b:指数为二次幂的无小数科学记数法,例如 -123456p-78
%e:科学计数法,例如e.g. -1.234456e+78
%E:科学计数法,例如e.g. -1.234456E+78
%f:小数形式,例如 123.456
%F:等效于 %f
%g:智能打印,如果指数部分很大,使用 %e,否则使用 %f
%G:智能打印,如果指数部分很大,使用 %E,否则使用 %F
%x:十六进制计数法,例如 -0x1.23abcp+20
%X:大写形式的十六进制计数法,例如 -0X1.23ABCP+20
字符串和字节切片:
%s:字符串形式
%q:使用 Go 语法安全转义的双引号文本字符串
%x:以十六进制小写形式打印每个字节(每个字节两个字符)
%X:以十六进制大写形式打印每个字节(每个字节两个字符)
如下是一个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package mainimport ( "fmt" ) func main () { s := "hello" buf := []byte {65 , 66 , 67 , 68 , 69 } buf2 := []byte ("中国" ) fmt.Printf("%s\n" , s) fmt.Printf("%q\n" , s) fmt.Printf("%x\n" , s) fmt.Printf("%X\n" , s) fmt.Printf("%s\n" , buf) fmt.Printf("%q\n" , buf) fmt.Printf("%x\n" , buf) fmt.Printf("%X\n" , buf) fmt.Printf("%s\n" , buf2) fmt.Printf("%q\n" , buf2) fmt.Printf("%x\n" , buf2) fmt.Printf("%X\n" , buf2) }
切片:
%p:以十六进制(存在前导 0x)打印字节切片中首个元素的地址
指针:
%p:以十六进制(存在前导 0x)打印地址
%b, %d, %o, %x and %X 也都是有效的,含义和整数打印相同
如下是一个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { t := []int {0 , 1 , 2 } fmt.Printf("%p\n" , t) fmt.Printf("%p\n" , &t[0 ]) fmt.Printf("%x\n" , &t[0 ]) }
对于 %v
格式谓词,其默认格式如下:
bool:%t
int, int8 等:%d
uint, uint8 等:%d,如果是 %#v 则是 %#x
float32, complex64 等:%g
string: %s
chan: %p
指针:%p
而当 %v
应用于复合类型时,会按照如下规则进行递归输出 :
结构体:{field0 field1 …}
数组、切片: [elem0 elem1 …]
map: map[key1:value1 key2:value2 …]
指向以上复合类型的指针: &{}, &[], &map[]
如下是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport "fmt" type Person struct { Name string Age int } func main () { p1 := Person{ Name: "jack" , Age: 20 , } p2 := Person{ Name: "Mick" , Age: 30 , } p3 := struct { id int p Person }{ 1 , Person{"Maria" , 18 }, } array := []Person{p1, p2} m := map [string ]Person{ "jack" : p1, "Mick" : p2, } fmt.Printf("%v\n" , p1) fmt.Printf("%v\n" , &p2) fmt.Printf("%v\n" , &p3) fmt.Printf("%v\n" , array) fmt.Printf("%v\n" , m) fmt.Printf("%v\n" , &m) }
宽度与精度 在格式化谓词之前,可以可选地添加十进制整数来指示宽度(Width)。如果没有显式指定宽度,那默认使用的宽度就是显式该值所必须的宽度。在可选的宽度之后,还可以通过添加小数点及十进制数的方式来指定精度。如果小数点后面没有指定数字,则精度为 0。
%f:默认宽度、精度
%9f:宽度为 9,默认精度
%.2f:默认宽度,精度为 2
%9.2f:宽度为 9,精度为 2
%9.f:宽度为 9,精度为 0
宽度和精度都是以 Unicode 码点(rune)为单位计算的。宽度和精度值都可以通过 *
来指定,这样具体的宽度、精度值将通过下一个整型参数来指定(即待格式化的参数之前的一个参数)。
对于大多数值来说,宽度就是所需要输出的最小字符(rune)个数,则必要时会进行字符填充
对于字符串、字节切片、字节数组来说,精度限制了要格式化的输入的长度。正常情况下,它也是以 rune 为单位进行计算,但是对于以 %x
或者 %X
进行格式化的类型,则是以字节为单位计算
对于浮点值,width 设置字段的最小宽度,precision 设置小数点后的位数。但是对于 %g/%G
来说,精度设置了最大有效位数(删除尾随零)
%e
、%f
和 %#g
的默认精度为6,对于 %g
,默认精度是唯一标识该值所需的最小位数
对于复数而言,宽度和精度独立地应用于两个分量,并且结果会用括号括起来
如下是一些示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package mainimport "fmt" func main () { f := 12.12345678 fmt.Printf("%f\n" , f) fmt.Printf("%.8f\n" , f) fmt.Printf("%.9f\n" , f) fmt.Printf("%12f\n" , f) fmt.Printf("%12.8f\n" , f) fmt.Printf("%12.f\n" , f) fmt.Printf("%e\n" , f) fmt.Printf("%20e\n" , f) fmt.Printf("%20.8e\n" , f) fmt.Printf("%g\n" , f) fmt.Printf("%12g\n" , f) fmt.Printf("%12.8g\n" , f) fmt.Printf("%12.4g\n" , f) fmt.Printf("%*.8f\n" , 12 , f) fmt.Printf("%12.*f\n" , 8 , f) fmt.Printf("%*.*f\n" , 12 , 8 , f) fmt.Printf("%*.*f\n" , 20 , 2 , f) fmt.Printf("%*f\n" , 20 , f) fmt.Printf("%*.f\n" , 20 , f) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" func main () { s := "hello" fmt.Printf("%s\n" , s) fmt.Printf("%10s\n" , s) fmt.Printf("%10.5s\n" , s) fmt.Printf("%10.2s\n" , s) t := 10 fmt.Printf("%.2d\n" , t) fmt.Printf("%10.2d\n" , t) }
其他支持的 flag 支持的其他 flag 包括:
+
:对于数值类型,总是打印符号;如果用于修饰 %q
(%+q
),则确保只会有 ASCII 字符输出
-
:实现左对齐(即在右边进行字符填充)。默认情况下都是右对齐(即在左边进行字符填充)
#
:替换格式
(空格):在数值中,为省略的符号预留一个空格,对于十六进制打印字符串或者 slices,在字节间预留空格
0
:使用前导字符 0
而不是空格填充。对于数字而言,该标志会将填充移动到符号之后。需要注意,对于左对齐,还是填充空格。
当某个 flag 对格式化谓词无意义时,该 flag 会被自动忽略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package mainimport "fmt" func main () { i := 10 s := []rune {'A' , '中' } fmt.Printf("%d\n" , i) fmt.Printf("%+d\n" , i) fmt.Printf("%q\n" , s) fmt.Printf("%+q\n" , s) fmt.Printf("%5d\n" , i) fmt.Printf("%-5d\n" , i) fmt.Printf("%d\n" , i) fmt.Printf("% d\n" , i) fmt.Printf("% d\n" , -i) fmt.Printf("% x\n" , "hello" ) fmt.Printf("%5d\n" , i) fmt.Printf("%05d\n" , i) fmt.Printf("%+05d\n" , i) fmt.Printf("%-05d$\n" , i) fmt.Printf("%5d\n" , -i) fmt.Printf("%05d\n" , -i) fmt.Printf("%+05d\n" , -i) fmt.Printf("%-05d$\n" , -i) fmt.Printf("%05s$\n" , "he" ) fmt.Printf("%-05s$\n" , "he" ) }
实现了特定接口的类型 当使用除 %T
、%p
之外的格式化谓词进行打印时,在对操作数进行格式化时也会考虑该操作数有没有实现特定的接口。规则如下:
如果操作数是 reflect.Value
类型,那么操作数会被替换为它所持有的具体值替代
如果操作数实现了 Formatter
接口,那么会调用该接口来进行格式化。
如果 %v
谓词和 #
一起使用,即使用 %#v
时,如果操作数实现了 GoStringer
接口,那么会调用该接口来进行格式化
如果指定的格式谓词对于字符串有效(%v
%s
%q
%x
%X
),或者使用的是 %v
而不是 %#v
,那么会应用如下如下两条规则:
如果操作数实现了 error
接口,那么会调用其 Error()
方法来生成字符串,并对结果继续应用格式化谓词
如果操作数提供了 String() string
方法,那么会调用该方法来生成字符串,并对结果继续应用格式化谓词
其他注意事项
不管格式化谓词是什么,如果操作数是一个 interface 类型的值,那么会使用该接口内部所保存的值,而不是接口本身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { var i interface {} = 10 fmt.Printf("%v\n" , i) fmt.Printf("%b\n" , i) fmt.Printf("%o\n" , i) fmt.Printf("%x\n" , i) }
当使用 %q
来格式化一个整数码点或者 []rune
时,对于无效 Unicode 码点,会替换为 U+FFFD
对于复合类型的操作数,格式化谓词是递归地应用于操作数中的每个元素,而不是整体应用于该操作数。但是对于字节切片,但使用字符串格式化谓词(%s
%q
%x
%X
),那么格式化谓词还是应用于整个字节切片,即将字节切片看成一个字符串
在格式化时,要避免可能出现的递归问题。例如:
1 2 type X string func (x X) String() string { return Sprintf("<%s>" , x) }
解决办法是,先进行类型转换:
1 func (x X) String() string { return Sprintf("<%s>" , string (x)) }
在格式化结构体时,fmt 不会尝试在未导出字段上触发 Error()
、String()
等方法
显示参数索引 对于 Printf 等函数,默认行为是 格式字符串
中的 格式化谓词
与后续的待打印的参数是一一对应的。但是也可以通过在格式化谓词前添加 [n]
指示符来指定该格式化谓词对应第 n
个参数(格式化字符串后的第一个参数索引为 1)。例如:
1 2 fmt.Sprintf("%[2]d %[1]d\n" , 11 , 22 )
如果宽度、精度使用 *
来指定,也可以通过在 *
前添加 [n]
来指示使用第 n 个参数作为宽度、精度的值
1 2 3 4 5 fmt.Sprintf("%[3]*.[2]*[1]f" , 12.0 , 2 , 6 ) 等效于 fmt.Sprintf("%6.2f" , 12.0 )
而且显的索引 [n]
会影响后续的 格式化谓词
所对应的参数。在没有显示指定的情况下,后续 格式化谓词
对应的参数为 n+1
、n+2
等。
1 2 # 16 17 0x10 0x11 fmt.Sprintf("%d %d %#[1]x %#x" , 16 , 17 )
如下是一个更复杂的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package mainimport "fmt" func main () { fmt.Printf("%[2]f,%[1]f\n" , 11.1111 , 22.2222 ) fmt.Printf("%10[2]f,%10[1]f\n" , 11.1111 , 22.2222 ) fmt.Printf("%[2]10f,%[1]10f\n" , 11.1111 , 22.2222 ) fmt.Printf("%8.2[2]f,%8.2[1]f\n" , 11.1111 , 22.2222 ) fmt.Printf("%[2]8.2f,%[1]8.2f\n" , 11.1111 , 22.2222 ) fmt.Printf("%.2[2]f,%.2[1]f\n" , 11.1111 , 22.2222 ) fmt.Printf("%[2].2f,%[1].2f\n" , 11.1111 , 22.2222 ) fmt.Printf("%.[2]*[3]f,%.[1]*[4]f\n" , 1 , 2 , 11.1111 , 22.2222 ) fmt.Printf("%0[5]*.[2]*[3]f,%-[6]*.[1]*[4]f$\n" , 1 , 2 , 11.1111 , 22.2222 , 6 , 8 ) fmt.Printf("%[4]*.*f\n" , 1 , 2 , 3 , 4 , 5 , 6.6 ) fmt.Printf("%[4]*.[1]*f\n" , 1 , 2.0 , 3 , 4 , 5 , 6.6 ) fmt.Printf("%[4]*.*f\n" , 1 , 2.0 , 3 , 4 ) fmt.Printf("%[4]*.*f\n" , 1 , 2.0 , 3 , 4 , 5 ) }
可以看到,当宽度、精度、显示参数索引一起使用时,需要将显示参数索引放在紧邻格式化谓词之前。同时参数索引也需要放在 *
字符之前,来指定宽度、精度对应的参数索引。
格式化错误 对于某个格式化谓词,如果该参数是无效的,则会在对应位置生成一个字符串来描述对应的问题。所有的错误都是以 %!
开头。
当 print 系列函数触发了某个参数的 Error()
或者 String()
方法且发生 panic 时,fmt 包会重新格式化错误消息,以表明该 panic 时来自 fmt 包。
打印系列函数 对于打印功能,fmt 包提供了 3 种风格的函数:
Print:不接受格式化字符串,对每个操作数使用 %v
进行打印。如果两个操作数都不是字符串,那么会在对应的格式化结果之间添加一个空格
Println 系列:和 Print 类似,但是总是会在每个操作数的格式化结果之间添加一个空格,同时最后会添加一个换行符
Printf:可以指定格式化字符串
如下是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { i, j, s := 10 , 20 , "test" fmt.Print(i, j, s) fmt.Println(i, j, s) fmt.Printf("%d %v %s\n" , i, j, s) }
PrintXXX
系列函数都是往标准输出进行打印,fmt 包还提供了一组类似的函数,可以向任意的可写对象进行输出:
1 2 3 func Fprint (w io.Writer, a ...any) (n int , err error )func Fprintln (w io.Writer, a ...any) (n int , err error )func Fprintf (w io.Writer, format string , a ...any) (n int , err error )
另外,fmt 包还提供了一组 Append
系列函数,用于向字节切片中添加格式化后的字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { i, j, s := 10 , 20 , "test" fmt.Printf("%s" , fmt.Append(nil , i, j, s)) fmt.Printf("%s" , fmt.Appendln(nil , i, j, s)) fmt.Printf("%s" , fmt.Appendf(nil , "%d %v %s\n" , i, j, s)) }
fmt.Errorf
函数可以允许根据指定的格式化字符串来生成错误信息。而且其提供了 %w
格式化谓词,用于 Wrap 一个或多个 error 对象。
1 func Errorf (format string , a ...any) error
如下是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "errors" "fmt" ) func main () { err := errors.New("basic error" ) errNew := fmt.Errorf("an errror from fmt error, %w" , err) fmt.Println(errNew) println (errors.Is(errNew, err)) }
Scan 系列函数 Scan
可以认为是 Print
的逆操作,Print
是将各种类型的值格式化为文本字符串,而 Scan
则是解析格式化的文本字符串,并生成对应类型的值。Scan
也存在一系列函数,根据解析方法的方式不同,可以分为以下 3 类:
Scan 系列函数(Scan、Fscan、Sscan)会将输入中的换行符当做空格处理
Scanln 系列函数(Scanln、Fscanln 和 Sscanln)在遇到换行符时就会停止扫描
Scanf 系列函数(Scanf、Fscanf 和 Sscanf)会严格根据格式字符串来解析输入
根据输入源的不同,Scan
系列函数又可以分为以下 3 类:
Scan,Scanf 和 Scanln:从标准输入中读取文本
Sscan,Sscanf 和 Sscanln:则从参数字符串中读取文本
Fscan,Fscanf 和 Fscanln:从一个 io.Reader 中读取文本
在 Scanf
的格式字符串中, 格式化谓词
(通过 %
符号来指定)会消耗并解析对应的输入,而格式字符串中的其他字符(除了 %、空格、换行符之外)需要与输入字符精确匹配。而对于空格、换行符的处理规则如下:
格式字符串中换行符前面的 0 个或多个空格,可以消耗输入源中换行符(或者输入结束)前的 0 或多个空格
格式字符串换行符之后的 1 个空格,可以消耗输入源中的 0 个或多个空格
格式字符串中的一个或多个空格可以消耗输入源中的尽可能多的空格
如下是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport "fmt" func main () { s := "a bb\n ccc" var n int var err error var a, b, c string n, err = fmt.Sscanf(s, "%s %s \n%s" , &a, &b, &c) fmt.Printf("%v, %v, %v, %v, %v\n" , n, err, a, b, c) n, err = fmt.Sscanf(s, "%s %s\n%s" , &a, &b, &c) fmt.Printf("%v, %v, %v, %v, %v\n" , n, err, a, b, c) n, err = fmt.Sscanf(s, "%s %s\n %s" , &a, &b, &c) fmt.Printf("%v, %v, %v, %v, %v\n" , n, err, a, b, c) n, err = fmt.Sscanf(s, "%s %s\n %s" , &a, &b, &c) fmt.Printf("%v, %v, %v, %v, %v\n" , n, err, a, b, c) }
1 2 3 4 5 3, <nil>, a, bb, ccc 3, <nil>, a, bb, ccc 3, <nil>, a, bb, ccc 3, <nil>, a, bb, ccc
Scan
中的格式化谓词行为类似于 Print
,这里就不再赘述。但是 Print
中的 %p
、%T
以及 #
、-
标志在 Scan
中没有实现。对于浮点数和复数,所有有效的格式化谓词都是等价的(%b %e %E %f %F %g %G %x %X and %v)。
Scan
中格式化谓词对输入的匹配默认是以空格分隔的:
每个格式谓词的实现(除了 %c)在开始匹配时都会忽略输入中的前导空格
%s(以及读取到字符串的 %v)都会在遇到第一个空白或换行符时停止
Scan 中的 格式化谓词
也支持设置 宽度
,如果指定了宽度,意味着对于该谓词最多读取多少个 rune(去除前导空格)。Scan 中的格式化谓词不支持 精度
。
以下是 Scan 系列函数的其他注意事项:
在所有的 Scan 函数中,\r\n
会被当成 \n
处理
在所有的额 Scan 函数中,如果操作数实现了 Scan
方法,则会使用该方法来解析该操作数对应的文本
从实现角度来讲,Scan
系列都是调用对应的 Fscan
函数来实现的,只需要将 io.Reader
参数设置为 os.Stdin
即可。
1 2 3 4 5 6 7 8 9 10 11 func Scan (a ...any) (n int , err error ) { return Fscan(os.Stdin, a...) } func Scanln (a ...any) (n int , err error ) { return Fscanln(os.Stdin, a...) } func Scanf (format string , a ...any) (n int , err error ) { return Fscanf(os.Stdin, format, a...) }
而 Sscan
系列函数但是它的输入源是字符串,而不是标准输入。它是通过 &string
转型为 *stringReader
并调用的 Fscan
函数来实现的。
1 2 3 4 5 6 7 8 9 10 11 func Sscan (str string , a ...any) (n int , err error ) { return Fscan((*stringReader)(&str), a...) } func Sscanln (str string , a ...any) (n int , err error ) { return Fscanln((*stringReader)(&str), a...) } func Sscanf (str string , format string , a ...any) (n int , err error ) { return Fscanf((*stringReader)(&str), format, a...) }
因此我们可以猜到 *stringReader
实现了 io.Reader
接口类型。如下展示了 stringReader
的相关实现
1 2 3 4 5 6 7 8 9 10 type stringReader string func (r *stringReader) Read(b []byte ) (n int , err error ) { n = copy (b, *r) *r = (*r)[n:] if n == 0 { err = io.EOF } return }
可以看到,stringReader
的底层类型就是 string
,而 *stringReader
的 Read 函数实现就是通过 copy 函数将字符串的内容拷贝到字节切片中,并重新更新原始字符串的内容。copy 函数支持从字符串中拷贝数据到字节切片中 。
从上面分析可以看到,Scan
特性的核心逻辑都是通过 Fscan
系列函数来实现的:
1 2 3 func Fscan (r io.Reader, a ...any) (n int , err error )func Fscanln (r io.Reader, a ...any) (n int , err error )func Fscanf (r io.Reader, format string , a ...any) (n int , err error )
小结 这篇文章学习了 go fmt
包的基本使用方法,学习如何里用 fmt
包来实现格式化 IO 操作。