欢迎加入GolangRoadmap,一个年轻的GO开发者社区https://www.golangroadmap.com/,目前是邀请制注册,注册码:Gopher-1035-0722,已开放GO内推,GO面试,GO宝典,GO返利等栏目

Go采用的格式化打印风格和C的 printf 族类似,但却更加丰富而通用。 这些函数位于 fmt 包中,且函数名首字母均为大写:如 fmt.Printf、fmt.Fprintf,fmt.Sprintf 等。 字符串函数(Sprintf 等)会返回一个字符串,而非填充给定的缓冲区。

你无需提供一个格式字符串。每个 Printf、Fprintf 和 Sprintf 都分别对应另外的函数,如 Print 与 Println。 这些函数并不接受格式字符串,而是为每个实参生成一种默认格式。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.Stdout 与 os.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

若你只想要默认的转换,如使用十进制的整数,你可以使用通用的格式 %v(对应“值”);其结果与 Print 和 Println 的输出完全相同。此外,这种格式还能打印任意值,甚至包括数组、结构体和映射。 以下是打印上一节中定义的时区映射的语句。

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

打印结果:

map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]

对于映射, Printf 会自动对映射值按照键的字典顺序排序。

当然,映射中的键可能按任意顺序输出。当打印结构体时,改进的格式 %+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", timeZone)

将打印

&{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]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}

(请注意其中的&符号)当遇到 string 或 []byte 值时, 可使用 %q 产生带引号的字符串;而格式 %#q 会尽可能使用反引号。 (%q 格式也可用于整数和符文,它会产生一个带单引号的符文常量。) 此外,%x 还可用于字符串、字节数组以及整数,并生成一个很长的十六进制字符串, 而带空格的格式(% x)还会在字节之间插入空格。

另一种实用的格式是 %T,它会打印某个值的类型。

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

会打印

map[string]int

若你想控制自定义类型的默认格式,只需为该类型定义一个具有 String() string 签名的方法。对于我们简单的类型 T,可进行如下操作。

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 的接收者就必须是值类型的;上面的例子中接收者是一个指针, 因为这对结构来说更高效而通用。更多详情见指针vs.值接收者一节)

我们的 String 方法也可调用 Sprintf, 因为打印例程可以完全重入并按这种方式封装。不过有一个重要的细节你需要知道: 请勿通过调用 Sprintf 来构造 String 方法,因为它会无限递归你的的 String 方法。如果 Sprintf 调用试图将接收器直接打印为字符串,而该字符串又将再次调用该方法,则会发生这种情况。这是一个常见的错误,如本例所示。

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // 错误:会无限递归
}

要解决这个问题也很简单:将该实参转换为基本的字符串类型,它没有这个方法。

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意转换
}

在 初始化 一节中,我们将看到避免这种递归的另一种技术。

另一种打印技术就是将打印例程的实参直接传入另一个这样的例程。Printf 的签名为其最后的实参使用了 …interface{} 类型,这样格式的后面就能出现任意数量,任意类型的形参了。

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

在 Printf 函数中,v 看起来更像是 []interface{} 类型的变量,但如果将它传递到另一个变参函数中,它就像是常规实参列表了。 以下是我们之前用过的 log.Println 的实现。它直接将其实参传递给 fmt.Sprintln 进行实际的格式化。

// Println 通过 fmt.Println 的方式将日志打印到标准记录器
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

在该 Sprintln 嵌套调用中,我们将 … 写在 v 之后来告诉编译器将 v 视作一个实参列表,否则它会将 v 当做单一的切片实参来传递。

还有很多关于打印知识点没有提及。详情请参阅 godoc 对 fmt 包的说明文档。

顺便一提,… 形参可指定具体的类型,例如从整数列表中选出最小值的函数 min,其形参可为 …int 类型。

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