Golang fmt 的那些高级用法

1,368 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

fmt 可以说是每个 Gopher 都用过的包了,每个开发者想必都写过 fmt.Println("hello world") 。但说来惭愧,自己开发 Go 时间也不算短了,对 fmt 包知道的也就是一些常用的函数:

  • Println
  • Printf
  • Sprintf
  • Errorf

真正用到的 format 也就是 %d, %f, %v, %s 这些,有时候需要结构体更多信息,会加上 + 或者 #。

最近在项目里看到一些同学 fmt 的用法感觉很不错。其实 fmt 包还是能做很多事情的,今天我们来看一看一些 fmt 不太常见的高级用法。相信总有一天能帮助到你。

(官方文档总是最好的参考,建议大家有时间还是完整看一下 fmt 的文档,能学到很多东西)

基础回顾

上来我们先简单回顾一下 fmt 的基础用法:

%v

默认格式的值,如果是打印结构体,用 %+v 可以加上属性名称。常见类型的默认格式如下:

image.png

%#v

Go语法下的值,将打印出结构体的字段名和类型。如果想定制,可以实现 GoStringer 接口

type GoStringer interface {
    GoString() string
}

示例:

type Ustr string

func (us Ustr) String() string {
    return strings.ToUpper(string(us))
}

func (us Ustr) GoString() string {
    return `"` + strings.ToUpper(string(us)) + `"`
}

%T

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

Go语法下值的类型

%%

代表一个 % 字符,不会匹配实际的值

指定宽度,精度

记住下面三句话即可:

  • Width is specified by an optional decimal number immediately preceding the verb. If absent, the width is whatever is necessary to represent the value.

  • Precision is specified after the (optional) width by a period followed by a decimal number. If no period is present, a default precision is used. A period with no following number specifies a precision of zero.

  • Either or both of the flags may be replaced with the character '*', causing their values to be obtained from the next operand (preceding the one to format), which must be of type int.

官方示例如下:

image.png

我们来看几个 case:

  1. 指定宽度为 10
fmt.Printf("%10d\n", 353)  // will print "       353"
  1. 动态指定宽度用 *
fmt.Printf("%*d\n", 10, 353)  // will print "       353"

上面也可以看到,默认的 padding 填充用的是【空格】,当然我们也可以换成别的:

fmt.Printf("%010d\n", 353)  // will print "0000000353"

这里就是用 0 来填充。

基于上面的能力,如果我们希望打印出数字列表而且希望它们能够靠右对齐时,就很方便了:

// alignSize return the required size for aligning all numbers in nums
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.

输出到 io.Writer

其实和 Printf, Println, Print 相对应,前面加上个 F 前缀,对应的三个函数就可以将 output 写入到指定的 io.Writer,而不是标准输入输出。

func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)

而换成 S 前缀的三个方法,则是将结果以字符串形式输出:

func Sprint(a ...interface{}) string
func Sprintln(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string

Formatter

Formatter 由自定义类型实现,用于实现该类型的自定义格式化过程。当格式化器需要格式化该类型的变量时,会调用其 Format 方法。

type Formatter interface {
    // f 用于获取占位符的旗标、宽度、精度等信息,也用于输出格式化的结果
    // c 是占位符中的动词
    Format(f State, c rune)
}

由格式化器(Print 之类的函数)实现,用于给自定义格式化过程提供信息:

type State interface {
    // Formatter 通过 Write 方法将格式化结果写入格式化器中,以便输出。
    Write(b []byte) (ret int, err error)
    // Formatter 通过 Width 方法获取占位符中的宽度信息及其是否被设置。
    Width() (wid int, ok bool)
    // Formatter 通过 Precision 方法获取占位符中的精度信息及其是否被设置。
    Precision() (prec int, ok bool)
    // Formatter 通过 Flag 方法获取占位符中的旗标[+- 0#]是否被设置。
    Flag(c int) bool
}

比如,我们有一个结构体,需要序列化之后打印,但是在一些场景下不希望每次都序列化,因为日志库底层会控制是否需要打印这个级别的日志,Debug日志不希望在线上打印,这时我们就可以用自定义 Formatter 的方法来延迟序列化:

// GetFormatter 延迟到打印时才序列化JSON字符串
func GetFormatter(v interface{}) fmt.Formatter {
	return &jsonMarshal{v: v}
}

// 通过json格式序列化数据,可以延迟序列化
func (v *jsonMarshal) Format(f fmt.State, c rune) {
	data, _ := Marshal(v.v)
	_, _ = f.Write(data)
}

传入一个结构体,拿到了个 fmt.Formatter 对象,此时是没有序列化的。

只有在真正要打印的时候,底层才会调用 Format 方法来序列化,然后调用 fmt.State 的 Write 方法写入。

参考资料