Go中的格式化打印使用类似C的printf系列的风格,但功能更为丰富和通用。这些函数位于fmt包中,并具有大写名称:fmt.Printffmt.Fprintffmt.Sprintf 等等。字符串函数(Sprintf等)返回一个字符串,而不是填充提供的缓冲区。

fmt包中打印函数如下:

func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)

func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string

一 格式字符串中的占位符

对于以f结尾的格式化打印函数FprintfPrintfSprintf可以接受一个格式字符串,其中格式字符串中有相应的占位符,不同的占位符格式效果不一样。

1.1 常规格式化

%v    默认格式。当打印结构类型时,带加号的标识(%+v)会添加字段名称
%#v   值的Go语法表示形式
%T    值类型的Go语法表示形式
%%    百分号标识字面量,无需提供值

示例:

package main

import (
    "fmt"
)

func main() {
    t := &T{7, -2.35, "abc\tdef"}

    fmt.Printf("%v\n", t)  // &{7 -2.35 abc    def}
    fmt.Printf("%+v\n", t) // &{a:7 b:-2.35 c:abc    def}
    fmt.Printf("%#v\n", t) // &main.T{a:7, b:-2.35, c:"abc\tdef"}
    fmt.Printf("%T\n", t)  // *main.T
    fmt.Printf("20%%\n")   // 20%
}

type T struct {
    a int
    b float64
    c string
}

1.2 布尔格式化

%t  输出布尔变量值 true 或 false

1.3 整数格式化

%b    二进制形式
%c    相应的Unicode码点表示的字符
%d    十进制形式
%o    八进制形式
%q    使用Go语法安全转义的单引号字符字面量
%x    十六进制形式,a-f为小写
%X    十六进制形式,A-F为大写
%U    Unicode格式: U+1234; 等价于 "U+%04X"

示例:

a := 255

fmt.Printf("%b\n", a) // 11111111
fmt.Printf("%c\n", a) // ÿ
fmt.Printf("%d\n", a) // 255
fmt.Printf("%o\n", a) // 377
fmt.Printf("%q\n", a) // 'ÿ'
fmt.Printf("%x\n", a) // ff
fmt.Printf("%X\n", a) // FF
fmt.Printf("%U\n", a) // U+00FF

1.4 浮点数和复数格式化

%b    指数为二的幂的无小数科学计数法,与strconv.FormatFloat函数的'b'格式一致。例如:6755399441055744p-52
%e    科学计数法(e小写), 如 -1.234456e+78
%E    科学计数法(E大写), 如 -1.234456E+78
%f    无指数小数,如 123.456
%F    同 %f
%g    用%e 表示大指数,否则用 %f 。 
%G    用%E 表示大指数,否则用 %F 。 
6755399441055744p-52 表示 6755399441055744 * 2-52,即:float64的1.5

关于精度的讨论,请参考1.8小节

示例:

b := 1234567.891

fmt.Printf("%b\n", b) // 5302428716536693p-32
fmt.Printf("%e\n", b) // 1.234568e+06
fmt.Printf("%E\n", b) // 1.234568E+06
fmt.Printf("%f\n", b) // 1234567.891000
fmt.Printf("%F\n", b) // 1234567.891000
fmt.Printf("%g\n", b) // 1.234567891e+06
fmt.Printf("%G\n", b) // 1.234567891E+06

1.5 字符串和字节切片格式化

%s    字符串或字节切片的未解释字节。
%q    Go语法安全转义的双引号字符串。
%x    十六进制形式,小写,每字节两个字符。
%X    十六进制形式,大写,每字节两个字符

示例:

s1 := `hello \nworld`
s2 := "hello \nworld"

fmt.Printf("%s\n", s1) // hello \nworld
fmt.Printf("%s\n", s2) // hello
                       // world // 注意这里换行了

fmt.Printf("%q\n", s1) // "hello \\nworld"
fmt.Printf("%q\n", s2) // "hello \nworld"

fmt.Printf("%x\n", s1) // 68656c6c6f205c6e776f726c64
fmt.Printf("%x\n", s2) // 68656c6c6f200a776f726c64

fmt.Printf("%X\n", s1) // 68656C6C6F205C6E776F726C64
fmt.Printf("%X\n", s2) // 68656C6C6F200A776F726C64

1.6 切片格式化

%p    第0个元素的地址的16进制表示形式,前导为0x。

示例:

s := make([]int, 2, 16)
fmt.Printf("%p\n", s) // 0x432080

1.7 指针格式化

%p    16进制表示形式,前导为0x。
%b, %d, %o, %x 和 %X 同样可以用来格式化指针,就像对待整数一样。

对于%v表示的默认格式,不同类型相当于:

bool:                    %t
int, int8 等:            %d
uint, uint8 等:          %d, 如果是%#v,则为%#x
float32, complex64, 等:  %g
string:                  %s
chan:                    %p
pointer:                 %p

对于复合对象,使用以下规则递归打印:

struct:             {field0 field1 ...}
array, slice:       [elem0 elem1 ...]
maps:               map[key1:value1 key2:value2 ...]
以上类型的指针:       &{}, &[], &map[]

1.8 宽度和精度

另外,还可以指定宽度和精度,格式为%w.p占位符,其中:w为指定的宽度,.p为指定的精度,注意p前面的点号,并不是所有的占位符都可以使用精度。如果实际宽度小于指定的宽度,则在前面补空格;如果实际宽度大于指定宽度,以实际宽度为准;如果缺省,则默认宽度是表示该值所必需的宽度,对于小数部分,默认宽度总是6。如果没有点号.,则使用默认精度;如果点号后没有数字p,则精度为0。精度在舍入时为四舍五入。宽度和精度均为十进制数字,且都是可选的。
示例:

b1 := 123456789.81234567
b2 := 12345678907.81
b3 := 12345678907.82

fmt.Printf("%f\n", b1)     // "123456789.812346"   - 默认宽度和精度
fmt.Printf("%f\n", b2)     // "12345678907.809999" - 默认宽度和精度
fmt.Printf("%f\n", b3)     // "12345678907.820000" - 默认宽度和精度
fmt.Println()              //
fmt.Printf("%20f\n", b1)   // "    123456789.812346" - 宽度20,默认精度
fmt.Printf("%.2f\n", b1)   // "123456789.81"         - 默认宽度,精度2
fmt.Printf("%16.2f\n", b1) // "    123456789.81"     - 宽度16,精度2
fmt.Printf("%9.f\n", b1)   // "123456790"            - 宽度9,精度0
注意第6行的结果并不能精确表示实际值
另外,上述示例中的注释为了体现差异增加了两端的双引号,实际不会输出,下同

1.9 其他标记

+    总是为数字值打印符号;对于%q (%+q),只保证ASCII字符的输出。
-    右侧填充空格,而不是左侧(左对齐)
#    可选格式: 对于八进制(%#o)添加前导0;十六进制(%#x)添加前导0x,十六进制(%#X)添加前导0X;
    对于%p (%#p)则抑制前导0x;
    对于%q,如果strconv.CanBackquote返回true,则打印原始字符串(反引号字符串);
    对于%e, %E, %f, %F, %g 和 %G总是打印一个小数点;
    对于%g 和 %G,不会移除结尾的0;
    如果字符'x'可用%U (%#U)打印,则会输出例如 U+0078 'x'
' '    (空格) 为数字中省略的符号保留一个空格(% d);
    以十六进制打印字符串或切片时(% x, % X),在字节之间插入空格
0    使用0而不是空格填充宽度;对于数字,则是在符号后填充。

示例:

n1 := 123
b1 := 123456789.81234567
s1 := 'x'
s2 := "ABC"

fmt.Printf("%+d\n", n1)    // "+123"
fmt.Printf("%+d\n", -n1)   // "-123"
fmt.Println()              //
fmt.Printf("%d\n", n1)     // "123"
fmt.Printf("%d\n", -n1)    // "-123"
fmt.Printf("% d\n", n1)    // " 123"
fmt.Printf("% d\n", -n1)   // "-123"
fmt.Println()              //
fmt.Printf("%020f\n", -b1) // "-000123456789.812346"
fmt.Printf("%020f\n", b1)  // "0000123456789.812346"
fmt.Printf("%#9.f\n", b1)  // "123456790."
fmt.Println()              //
fmt.Printf("%U\n", s1)     // "U+0078"
fmt.Printf("%#U\n", s1)    // "U+0078 'x'"
fmt.Printf("%5s\n", s2)    // "  ABC"
fmt.Printf("%-5s\n", s2)   // "ABC  "
fmt.Printf("%05s\n", s2)   // "00ABC"

二 格式化打印

你不需要提供格式字符串。对于PrintfFprintfSprintf中的每一个,还有另外的函数与之对应,比如PrintPrintln,这些函数不使用格式字符串,而是为每个参数生成默认格式。Println版本还在参数之间插入一个空格,并在输出中附加一个换行符,而Print版本只在两边的操作数都不是字符串时才添加空格。下面的例子输出的结果一样:

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

格式化打印函数如fmt.Fprint等,将任何实现了io.Writer接口的对象作为第一个参数,像变量os.Stdoutos.Stderr

接下来的事情与C不同,首先,像%d等数字格式不接受符号或大小标识,相反,打印例程会使用参数的类型来决定这些属性。

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

会打印出:

18446744073709551615 ffffffffffffffff; -1 -1

如果只是想要默认转换,例如decimalinteger,可以使用%v格式,结果正是PrintPrintln将产生的结果。而且,该格式可以打印任何值,甚至是数组、切片,结构和映射。
例如打印映射的语句:

var color = map[string]string{
    "red":    "红",
    "orange": "橙",
    "yellow": "黄",
    "green":  "绿", // 注意每行结尾的逗号不能少
}

fmt.Printf("%v\n", color)
fmt.Println(color)

会打印出:

map[green:绿 orange:橙 red:红 yellow:黄]
map[green:绿 orange:橙 red:红 yellow:黄]

对于映射,输出的key的顺序是不固定的。对于结构类型,改版的%+v格式使用它们的名称注解其中的字段,对于任何值,%#v替换格式以完整的Go语法打印。

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", color)

会打印出:

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]string{"green":"绿", "orange":"橙", "red":"红", "yellow":"黄"}

注意&符号。当应用于string[]byte类型的值时,带引号的字符串格式也可以通过%q获得。如果可能,备用格式%#q将使用反引号。(%q也适用于整数和符文,会输出单引号的符号常量)。此外,%x适用于字符串,字节数组和字节切片以及整数,生成长十六进制字符串,中间有个空格的% x格式会在字节之间放置空格。

另一个方便的格式是%T,它打印一个值的类型:

fmt.Printf("%T\n", color)

会打印出:

map[string]string

如果要控制自定义类型的默认格式,只需要在类型中定义一个String() string签名的方法即可,例如:

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

输出:

7/-2.35/"abc\tdef"

如果你需要像打印类型T的指针那样打印T的值,String的接收器必须是一个值类型,这个例子使用了一个指针,因为对于结构类型更有效和惯用。

我们的String方法能够调用Sprintf,是因为打印例程完全是可重入的,并且以这种方式封装。但是,要理解这种方法有一个重要的细节:请勿通过调用Sprintf来构造String方法,因为它会无限递归String方法。如果Sprintf调用尝试直接以字符串形式打印接收器,而字符串又会再次调用该方法,就会发生这种情况,这是一个常见且容易犯的错误。例如以下的错误示例:

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

很容易修复这个问题:将参数转换为基本字符串类型,该类型没有这个方法。

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

另一种解决办法是使用非字符串格式标记,例如:

type ByteSize float64

const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

在这里用Sprintf实现ByteSizeString方法很安全(不会无限递归),这倒不是因为类型转换,而是它以%f调用了Sprintf,它并不是一种字符串格式:Sprintf只会在它需要字符串时才调用String方法,而%f需要一个浮点数值。

另一种打印技术是将打印例程的参数直接传递给另一个这样的例程。Printf的签名使用...interface{}类型作为最终参数,以指定在格式化之后可以出现任意数量的参数(任意类型):

func Printf(format string, v ...interface{}) (n int, err error) {

在函数Printf中,v相当于[]interface类型的变量,但是如果它被传递给另一个可变参数函数,它就像一个常规的参数列表。这是我们上面使用的函数的log.Println的实现。它将其参数直接传递给fmt.Sprintln以进行实际格式化。

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

我们在对嵌套的Sprintln调用中的v后边加上...告诉编译器将v视作参数列表,否则它只会将v作为单个切片参数传递。

顺便说一下,...参数可以是特定类型,例如用于选择最小整数列表min函数的... int

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

参考:
https://golang.org/doc/effective_go.html#printing
https://golang.org/pkg/fmt

文章目录