欢迎加入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
}