深入解析 Go fmt 包
Jan 15, 2019 17:50 · 1080 words · 3 minute read
我们经常不假思索地使用 fmt
包,这里用下 fmt.Printf
那里用下 fmt.Sprintf
,用完即走。但是如果仔细琢磨一下,这里面还是有丶东西的。
Go 经常被用来编写服务,我们主要的调试工具是日志系统。log
包提供的 log.Printf
与 fmt.Printf
语义相同。良好且信息丰富日志对得起它们占用的空间,为你的数据结构添加一些格式化支持还会带来一些额外的信息价值。
格式化输出
Go fmt
方法们支持多种操作,最常用的是代表字符串的 %s
、代表整型数的 %d
和代表浮点数的 %f
。
%v 和 %T
%v
用于输出任意真值,%T
将打印变量的类型。
var e interface{} = 2.7182
fmt.Printf("e = %v (%T)\n", e, e) // e = 2.7182 (float64)
宽度
你可以调整被输出的数字的宽度,就像这样:
fmt.Printf("%10d\n", 353) // " 353"
以传参的形式指定宽度,使用 *
符号:
fmt.Printf("%*d\n", 10, 353) // " 353"
这在打印数列并希望以右对齐的格式进行比较时很有用:
func alignSize(nums []int) int {
size := 0
for _, n := range nums {
if s := int(math.Log10(float64(n))) + 1; s > size {
size = s
}
}
return size
}
func main() {
nums := []int{12, 237, 3878, 3}
size := alignSize(nums)
for i, n := range nums {
fmt.Printf("%02d %*d\n", i, size, n)
}
}
输出:
00 12
01 237
02 3878
03 3
这样比较起来更直观一些。
通过位置引用
如果要多次引用同一个变量,使用 %[n]
,n 代表参数的索引。从1开始!
fmt.Printf("The price of %[1]s was $%[2]d. $%[2]d! imagine that.\n", "carrot", 23)
输出:
The price of carrot was $23. $23! imagine that.
%v
%v
将输出真值,可以带上前缀 +
来打印结构体中的字段名,或者用 #
来同时打印结构体名和类型。
type Point struct {
X int
Y int
}
func main() {
p := &Point{1, 2}
fmt.Printf("%v %+v %#v \n", p, p, p)
}
&{1 2} &{X:1 Y:2} &main.Point{X:1, Y:2}
我个人更偏向于使用 %#v
。
fmt.Stringer & fmt.Formatter
有时候你想要更好地控制对象的打印方式。例如,日志中需要一个更详细的字符串来向用户展示错误信息。
为了控制对象的打印方式,需要自己实现 fmt.Formatter
或 fmt.Stringer
接口。
写个小例子,这里有个 AuthInfo
结构:
// AuthInfo is authentication information
type AuthInfo struct {
Login string // Login user
ACL uint // ACL bitmask
APIKey string // API key
}
const (
keyMask = "*****"
)
APIKey
不能随随便便让人看见,要用 *****
来替代,这是一个 fmt.Stringer
的简单实现:
// String implements Stringer interface
func (ai *AuthInfo) String() string {
key := ai.APIKey
if key != "" {
key = keyMask
}
return fmt.Sprintf("Login:%s, ACL:%08b, APIKey: %s", ai.Login, ai.ACL, key)
}
现在 fmt.Formatter
获取到了 fmt.State
和代表类型的字符。而 fmt.State
又是 io.Writer
的实现,让你可以直接往里面丢东西。
要想知道结构体中所有的字段,可以使用 reflect
包。这样确保了即使 AuthInfo
改变代码依旧可用。
var authInfoFields []string
func init() {
typ := reflect.TypeOf(AuthInfo{})
authInfoFields = make([]string, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
authInfoFields[i] = typ.Field(i).Name
}
sort.Strings(authInfoFields) // People are better with sorted data
}
现在万事俱备,就等实现 fmt.Formatter
:
// Format implements fmt.Formatter
func (ai *AuthInfo) Format(state fmt.State, verb rune) {
switch verb {
case 's', 'q':
val := ai.String()
if verb == 'q' {
val = fmt.Sprintf("%q", val)
}
fmt.Fprint(state, val)
case 'v':
if state.Flag('#') {
// Emit type before
fmt.Fprintf(state, "%T", ai)
}
fmt.Fprint(state, "{")
val := reflect.ValueOf(*ai)
for i, name := range authInfoFields {
if state.Flag('#') || state.Flag('+') {
fmt.Fprintf(state, "%s:", name)
}
fld := val.FieldByName(name)
if name == "APIKey" && fld.Len() > 0 {
fmt.Fprint(state, keyMask)
} else {
fmt.Fprint(state, fld)
}
if i < len(authInfoFields)-1 {
fmt.Fprint(state, " ")
}
}
fmt.Fprint(state, "}")
}
}
写点测试代码:
ai := &AuthInfo{
Login: "crazytaxii",
ACL: 1,
APIKey: "we_have_a_hulk",
}
fmt.Println(ai.String())
fmt.Printf("ai %%s: %s\n", ai)
fmt.Printf("ai %%q: %q\n", ai)
fmt.Printf("ai %%v: %v\n", ai)
fmt.Printf("ai %%+v: %+v\n", ai)
fmt.Printf("ai %%#v: %#v\n", ai)
运行一下:
Login:crazytaxii, ACL:00000001, APIKey: *****
ai %s: Login:crazytaxii, ACL:00000001, APIKey: *****
ai %q: "Login:crazytaxii, ACL:00000001, APIKey: *****"
ai %v: {1 ***** crazytaxii}
ai %+v: {ACL:1 APIKey:***** Login:crazytaxii}
ai %#v: *main.AuthInfo{ACL:1 APIKey:***** Login:crazytaxii}
结论
fmt
包有不少琐碎的小功能,如果你自己很熟悉这些细节的话,叩叮起来更得心应手。