深入了解fmt包



英文原文:https://blog.gopheracademy.com/advent-2018/fmt/

fmt包是我们最常使用的package,我们使用它打印输出一些内容,也可以当作字符串formatter工具。在这篇内容中我们再更进一步了解fmt的使用技巧。

Formatting Output

Go fmt的方法支持多个“描述符”,一些常见的“描述符”例如:%s表示格式化输出为字符串,%d表示格式化输出为整数型,%f表示格式化输出为浮点类型。除了这些常见的,这里介绍一些其他的fmt包的“描述符”:

%v&%T

%v将会格式化输出任意的变量的值,%T则会格式化输出任意变量的类型(type)

1
2
var e interface{} = 2.7182
fmt.Printf("e = %v (%T)\n", e, e)

输出:

1
e = 2.7182 (float64)

宽度

可以在格式化输出的时候,指定输出内容的宽度,例如:

1
fmt.Println("[%10d]\n", 353)

输出:

1
[       353]

可以看到输出的内容在353前面多了7个空格(我真的数了),加上“353”正好是10个字符,这就是“宽度”的意思,把变量格式化为指定的宽度输出。当然指定的宽度如果小于要输出的内容,那么就不会生效了。

这个宽度可以用在其他的“描述符”中,%s%f%T等等,对于%T这种使用宽度时,会给类型描述设置宽度,这么描述可能不清楚,可以看看代码:

1
2
s := "123"
fmt.Printf("[%10T]\n", s)  // 输出:[    string]

也可以在参数中指定宽度的数值,使用*来指代宽度数值参数,例如:

1
fmt.Printf("%*d", 10, 353)

输出:

1
[       353]

例如在打印一组数字集合时,宽度可以用来使输出内容对其,这样更方便对比查看:

 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
package main

import (
    "fmt"
    "math"
)

func main() {
    nums := []int{12, 237, 3878, 3}
    size := alignSize(nums)
    for i, n := range nums {
        fmt.Printf("%02d %*d\n", i, size, n)
    }
}

// 计算数组中数字的最大宽度(返回的最大宽度最终+1)
func alignSize(nums []int) int {
    // 宽度的初始值为0
    size := 0
    // 循环数组,找出最大的宽度,并且赋值给size
    for _, n := range nums {
        // log10的对数,可以近似为数字的个数
        if s := int(math.Log10(float64(n))) + 1; s > size {
            size = s
        }
    }
    return size
}

输出:

1
2
3
4
00   12
01  237
02 3878
03    3

参数位置索引

如果你在一个格式化输出中,想要在多处““描述符””中引用一个变量,那么可以使用参数位置索引,使用%[n]d这种语法就能做到,其中n代表参数的位置,位置从1开始(如果写了参数索引为0,就会出现错误BADINDEX)。

1
fmt.Printf("您购买了%[1]s 消费 $%[2]d元. 合计$%[2]d元!\n", "机械键盘", 2300)

输出:

1
您购买了机械键盘 消费 $2300元. 合计$2300元!

%v

%v可以格式化输出一个值,在输出一个struct类型的值时,可以使用+前缀同时输出字段的名字,也可以使用#前缀同时输出字段的名字和struct的类型:

1
2
3
4
5
6
7
8
func main() {
    p := person{"hai", 26}
    s := "123"
    fmt.Printf("%v %+v %#v\n", p, p, p)
    fmt.Printf("%v %+v %#v\n", &p, &p, &p)
    fmt.Printf("%v %+v %#v\n", s, s, s)
    fmt.Printf("%v %+v %#v\n", &s, &s, &s)
}

输出:

1
2
3
4
{hai 26} {name:hai age:26} main.person{name:"hai", age:26}
&{hai 26} &{name:hai age:26} &main.person{name:"hai", age:26}
123 123 "123"
0xc0420461c0 0xc0420461c0 (*string)(0xc0420461c0)

fmt.Stringer & fmt.Formatter

有些情况下你也许需要精细的控制要格式化输出的内容,举个例子,你可能想把一些错误信息分成两种格式化输出:用户端显示简化的错误信息,日志内输出详细的错误内容。

要自定义控制对象的格式化输出,你需要实现fmt.Formatter接口和可选的fmt.Stringer接口。一个使用fmt.Formatter接口最优秀的例子是github.com/pkg/errors项目。

假如你的项目中有一个加载配置文件的错误发生,你可以给用户展示简洁的错误,在日志中输出详细的错误信息。

1
2
3
4
5
cfg, err := loadConfig("/no/such/config.toml")
if err != nil {
    fmt.Printf("error: %s\n", err)
    log.Printf("can't load config\n%+v", err)
}

展示给用户的错误信息:

1
error: can't open config file: open /no/such/file.toml: no such file or directory

输出到日志文件的错误信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
2018/11/28 10:43:00 can't load config
open /no/such/file.toml: no such file or directory
can't open config file
main.loadConfig
    /home/miki/Projects/gopheracademy-web/content/advent-2018/fmt.go:101
main.main
    /home/miki/Projects/gopheracademy-web/content/advent-2018/fmt.go:135
runtime.main
    /usr/lib/go/src/runtime/proc.go:201
runtime.goexit
    /usr/lib/go/src/runtime/asm_amd64.s:1333

接下来说明一下怎么使用fmt.Formatterfmt.Stringer来精细化控制格式化信息。

假如有一个存储用户认证信息的struct AthInfo

1
2
3
4
5
type AuthInfo struct {
    Login  string
    ACL    uint
    APIKey string
}

在格式化输出(error或者information ..)的时候,应该不能直接把APIKey的值展示出来,最好的做法是使用掩码******替代。所以可以定义一个常量:

1
2
3
const (
    keyMask = "******"
)

首先实现fmt.Stringer接口,以用来使用掩码隐藏APIKey

1
2
3
4
5
6
7
8
9
// 实现https://golang.org/pkg/fmt/#Stringer接口
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接口,该接口只有一个方法Format(f State, c rune),这个方法可以实现对格式化输出的自定义操作,其中第一个参数是提供给自定义Formatter的一个“printer”的状态数据(struct),其中包含了当前格式化输出的一些数据,例如当前正在格式化的“描述符”。

当我们的struct实现了Format方法,在使用Printf格式化方法时,会自动调用自定义的Format方法,从而实现对格式化输出的精细化控制。

Format函数会在fmt\print.go文件中的handleMethods方法中被调用,我们可以看看这个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func (p *pp) handleMethods(verb rune) (handled bool) {
    if p.erroring {
        return
    }
    // p是当前操作的printer,这是一个使用sync.pool生成的对象,因为所有的格式化输出都需要用到它,
    // 所以借助池,减少创建对象时的开销。
    // p.arg 就是当前正在格式化的参数(因为一个格式化输出操作,
    // 可能包含多个“描述符”和参数,所以必不可少的使用循环,来一对一[verb <=> p.arg]操作)
    // 因为我们的参数(对象)有可能实现了Format方法,所以如果断言它是Formatter接口类型,
    // 那么就可以调用自定义的Format方法了
    if formatter, ok := p.arg.(Formatter); ok {
        handled = true
        defer p.catchPanic(p.arg, verb)
        // 调用自定义的Format函数,传入printer和描述符
        formatter.Format(p, verb)
        return
    }

    ...
    return false
}

解释完了Formatter接口,为了接下来的例子,先实现一个存储需要格式化输出struct的所有字段名字的数组,可以在init函数中获取,获取也很简单,直接使用reflect反射操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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) 
}

最后一步,实现我们的自定义Format方法:

 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
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':
        // 如果描述符标志为“#”号,就先将格式化参数的类型写入“state”
        if state.Flag('#') {
            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)
            // 字段名称为`APIKey`并且值的长度大于0,就需要使用掩码替代
            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, "}")
    }
}

最后测试一个格式化输出结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
    ai := &AuthInfo{
        Login:  "daffy",
        ACL:    1,
        APIKey: "dck season",
    }
    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)
}

输出结果:

1
2
3
4
5
6
Login:daffy, ACL:00000001, APIKey:******
ai %s 描述符输出: Login:daffy, ACL:00000001, APIKey:******
ai %q 描述符输出: "Login:daffy, ACL:00000001, APIKey:******"
ai %v 描述符输出: {1 ****** daffy}
ai %+v 描述符输出: {ACL:1 APIKey:****** Login:daffy}
ai %#v 描述符输出: *main.AuthInfo{ACL:1 APIKey:****** Login:daffy}

参考资料

本文参照翻译https://blog.gopheracademy.com/advent-2018/fmt/ 并且修改增加了部分内容。