深入解析 Go fmt 包

Jan 15, 2019 17:50 · 1080 words · 3 minute read Golang

我们经常不假思索地使用 fmt 包,这里用下 fmt.Printf 那里用下 fmt.Sprintf,用完即走。但是如果仔细琢磨一下,这里面还是有丶东西的。

Go 经常被用来编写服务,我们主要的调试工具是日志系统。log 包提供的 log.Printffmt.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.Formatterfmt.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 包有不少琐碎的小功能,如果你自己很熟悉这些细节的话,叩叮起来更得心应手。