GO标准库巡礼-fmt

fmt

模板

fmt中的转义词

  • %v

    通用占位符,该占位符会自动转换变量为string(以默认选项)。通常用于转换基础类型

  • %#v

    会将变量转换为符合go syntax的输出,也就是说我们可以直接复制输出结果然后粘贴到代码中而不会报错。我们可以看一个例子来加深理解

    func main() {
    	c := struct {
    		hello string
    		world string
    	}{"1", "2"}
    	fmt.Printf("%#v", c)
    }
    // 输出结果
    // struct { hello string; world string }{hello:"1", world:"2"}
    
  • %T

    打印变量类型。通常用于DEBUG检查实际类型

  • %d

    打印十进制下的整数,也可以使用%v但是%d会意图更加明确

  • %x或者%X

    打印十六进制下的整数。一个有用的做法是传入字节切片,会输出两位数的十六进制数字

  • %f

    打印没有e的浮点数。也可以使用%v但是%f允许我们设置宽度以及精确度等

  • %q

    打印quoted字符串。常常用于当数据中存在不可见字符

  • %p

    打印变量的指针地址。常常用于debug的时候检查是否不同的指针变量指向相同数据

宽度和精度

我们可以通过增加很多flags来控制转义词的输出效果。尤其是对于浮点数来说,通常我们需要对其精度和宽度做限制

  • 设置精度

    我们可以通过在%后面添加一个.以及数字来设置精度,比方说%.2f会将传入的100.567输出为100.57(注意这里使用四舍五入)

  • 设置宽度

    我们可以通过一个紧跟在%后面的数字来指定宽度,如果变量长度小于指定宽度,那么会用空格填充。因此这个功能可以用于打印类似表格的效果。比方说%8.2f会将传入的100.567打印为••100.57(•表示空格)

左对齐

输出默认的是右对齐,意味着空格(如果需要的话)会填充在左边。如果我们需要左对齐,可以在%后面添加-,比方说%-8.2f会将传入的100.567打印为100.57••(•表示空格)

用零填充

有些时候我们可能希望用0来填充而不是空格,比方说我们希望生成定长整数字符串。我们可以通过在%后紧跟0来实现这点。比方说%08d会将传入的123打印为00000123

其他转义词和flag

可以参考fmt官方文档中的Printing部分

输出

fmt主要用于格式化字符串,这些格式化函数基于输出类型来分组:STDOUT, io.Writer以及string

每一组都有三个函数:默认格式化,用户定义格式化以及默认格式化+换行

输出至STDOUT

最常用到的就是通过输出到STDOUT在终端上显示,可以使用Print函数组来实现这个目的

func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

期中,Print函数仅仅会基于默认格式打印一系列变量至STDOUT;而Printf函数允许我们通过上面提到的模板进行格式化;Println函数类似于Print但是会在变量之间插入空格以及在最后加上换行符

通常我们会使用Printf来格式化输出,如果需要默认格式会使用Println因为一般我们需要换行。一个例外是当我们试图与用户交互的时候,如果我们希望输出What is your name?后光标在输出字符串后面而不是换行,我们可以使用Printf

输出至io.Writer

如果我们希望打印至非STDOUT的io.Writer(比方说STDERR或者buffer),那么我们可以使用Fprint函数组。其中F表示FILE,这是继承自C语言的fprintf函数

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

这些函数类似于Print函数组只是需要我们定义writer,事实上Print函数组仅仅是对Fprint函数组的简单封装

我们可以用Fprintf来抽象出STDOUT从而方便我们实现单元测试(我们可以传入buffer然后验证输出结果)

输出至字符串

有些时候我们就希望得到字符串,一个办法是使用buffer+Fprintf函数,但是可能较为繁琐。我们可以直接使用Sprintf函数组

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

值得注意的是,Sprintf虽然较为方便,但是如果常常生成字符串,往往会变成性能瓶颈。如果在性能剖析之后发现需要优化,可以重用bytes.Buffer+Fprintf来提高速度

错误格式化

我们可以使用Errorf来格式化错误,函数定义如下

func Errorf(format string, a ...interface{}) error

该函数其实是对errors.New以及Sprintf的简单封装

func Errorf(format string, a ...interface{}) error {
	return errors.New(Sprintf(format, a...))
}

扫描(scanning)

fmt也提供了对应的函数从我们格式化的输入中提取数据写入变量,这个过程称之为扫描(scanning)。和输出类似,对应函数可以基于 从STDIN读取、从io.Reader读取以及从字符串中读取分组

不过注意的是,通常来说,我们通过CLI flags、环境变量或者API调用来获取输入值而不是通过扫描这种方式

从STDIN中读取

和Print写入STDOUT相似,我们可以使用Scan函数组来读取STDIN

func Scan(a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
func Scanln(a ...interface{}) (n int, err error)

Scan函数会基于空格来划分输入然后分别写入变量中(换行符被视为空格)

Scanf函数允许我们使用格式化字符串来识别格式化选项

Scanln函数类似于Scan函数但是不会将换行符视为空格

下面是一个相关的例子

var name string
var age int
if _, err := fmt.Scan(&name, &age); err != nil {
        fmt.Println(err)
        os.Exit(1)
}
fmt.Printf("Your name is: %s\n", name)
fmt.Printf("Your age is: %d\n", age)
//输出结果
//Jane 25               --这是输入
//Your name is: Jane
//Your age is: 25

从io.Reader中读取

同样可以用Fscan函数组来读取reader

func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)

从字符串中读取

func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)

Stringer接口

对于%s以及调用了Print函数的变量来说,go会检查是否实现了Stringer接口,如果是则调用String函数

type Stringer interface {
        String() string
}

格式化至go中

我们也可以实现GoStringer接口来修改%#v的显示结果,不过一般%#v已经能够输出满意的结果所以比较少用

type GoStringer interface {
        GoString() string
}

用户定义类型

fmt中最让人疑惑部分就是用户自定义输出和写入。它们可以让你完全控制对象如何被输出(在使用Printf和Scanf的时候)

格式化器(Formatters)

可以通过实现Formatter接口来为自己的类型提供格式化器,接口定义如下

type Formatter interface {
        Format(f State, c rune)
}

其中State对象列举了指定的所有标记(flag)包括宽度、精度等,c rune识别了转义词的字符(即%s中的s或者%v中的v)

  • 一个简单的使用例子

    下面的结构体会基于宽度在最前面打印若干个#,基于精度在后面打印若干个☃

    // Header represents formattable header text.
    type Header string
    // Format decorates the header with pounds and snowmen.
    func (hdr Header) Format(f fmt.State, c rune) {
            wid, _ := f.Width()
            prec, _ := f.Precision()
            f.Write([]byte(strings.Repeat("#", wid)))
            f.Write([]byte(hdr))
            f.Write([]byte(strings.Repeat("☃", prec)))
    }
    
    hdr := Header(“GO WALKTHROUGH”)
    fmt.Printf(%2.3s\n”, hdr)
    //输出结果
    //##GO WALKTHROUGH☃☃☃
    

在go标准库中,格式化器常常用于特别的数字类型如big.Float或者big.Int

读取器(Scanners)

对应Fomartter我们也有相应的Scanner接口

type Scanner interface {
        Scan(state ScanState, verb rune) error
}

总结

  • 如果不需要任何格式化选项,使用%v

  • 尽量使用宽度和精度,特别是对于浮点数

  • 尽量不要使用Scan函数用于读取,通常有更好的选择

  • 如果默认实现不可行,尽量定义String函数

  • 不要使用自定义格式化器和读取器,通常没有太好的用例

    一个比较有意义的用例适用于实现字节数向MB的格式转换,例子

  • 如果发现fmt是性能瓶颈,可以转为使用strconv,但是要先做性能剖析

发布了31 篇原创文章 · 获赞 32 · 访问量 728

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/105075677