zhangxiang958/Blog

解读 Golang 的 fmt 包

zhangxiang958 opened this issue · 0 comments

fmt 包应该是我们学习 golang 最开始接触的包,它是用于格式化我们的输入输出流的。但是一开始接触会对于代码中一会使用 fmt.Println 一会使用 fmt.Printf 有些困惑,而且各种占位符会让人非常迷惑。也许深入研究下 fmt 包可以有效地解开这个困惑。

Print

Print 函数不接受任何的格式化处理,直接将输入的参数值打印出来,但是注意的是,如果有多个参数值,那么如果该参数本身不是字符串并且前面不是字符串的情况下,会自动在前面加一个空格打印

fmt.Print("1", "2", "3", 4, 5); // 1234 5

Println

func Println(a ...interface{}) (n int, err error) {}

如上 Println 的函数签名所示,它接受任意类型的参数,然后将这个参数值打印出来。它的实现是:

func (p *pp) doPrintln(a []interface{}) {
	for argNum, arg := range a {
		if argNum > 0 {
			p.buf.writeByte(' ')
		}
		p.printArg(arg, 'v')
	}
	p.buf.writeByte('\n')
}

可以看到,这里如果有多个参数,那么会自动在每个参数之间打印一个空格。在打印完所有参数之后,会打印一个换行符。

Printf

Printf 函数应该是最为复杂的一个格式化函数,它接收一个含有格式化标识符的字符串,然后将传入的参数对应的值写到字符串对应位置。

如果你想要打印对应变量的值和类型,那么就需要使用 %v 和 %T 这两个格式化符号。

var a = "str"

fmt.Printf("what is a: %v, and its type is %T", a, a); // what is a: str, and its type is string

位置引用

上面的代码你或许可以看出一点问题,如果我们想要同时打印一个变量的值和类型,我们就需要传入多次该变量吗?这样会显得非常麻烦,我们可以通过位置引用来做到多次使用一个变量。

fmt.Printf("what is a: %[1]v, and its type is %[1]T", a);

位置的下标从 1 开始,通过方括号加上数字的形式来达到多次引用这个变量的值。

通用格式

我们可以看到 %v 可以打印出任意变量的值,在 fmt 底层的实现中,如果使用这个占位符,会使用类型推断来打印对应不同类型的值

switch f := arg.(type) {
case bool:
	...
case string:
	...
case int:
	...
...
}

而 %T 可以打印出变量对应的类型,它对应的底层实现其实就是利用反射 reflect.Typeof 这个函数获取类型:

var a = "1";

reflect.Typeof(a).String(); // string

宽度

在格式化输出的时候,fmt 还支持使用宽度设置:

fmt.Printf("%10v", 1)

如果像上面的这样,在输出 1 之前,会先输出 9 个空格。当然这个宽度是可以动态设置的:

fmt.Printf("%*v", 8, 1);

这个时候 * 号代表的就是宽度,那么第一个传入的参数就是指定的宽度,这就代表比如如果你想输出多个字符串值,输出宽度需要根据每个字符串的长度来定的话,就可以使用这种形式。

flag

fmt 包有几个特定的前置 flag 来设置打印的模式:

# go 语法表示,也就是打印出字面量
0 会让前置打印出来的不是空格而是 0
- 左对齐
+ 打印数值的正负号
[空格符]

具体可以看下下面的例子:

fmt.Printf("%#v", "1"); // "1", 如果不加 # 只会打印 1,不会打印出双引号
fmt.Printf("%02v", 1); // 01,这里 2 是表示宽度为 2,0 代表前置需要使用 0 来填补空白
fmt.Printf("%-v", 1);
fmt.Printf("% v", 1);
fmt.Printf("%+v", -1);

这些 flag 值在代码实现的时候都是布尔值的形式存在的,当需要打印的时候,对于某些类型的打印方法,内部会根据这些 flag 值来做 if...else 判断,从而打印出不同的形式。fmt 包里面会有很多对于这个 flag 值的检查,像 map,struct,point 或者 interface 之类的会打印出它们类型的名字,然后才打印每个字段对应的值。

比如 “#” 就是设置 fmt 模块上的 sharp 属性为 true,例如对于 map 类型,如果 sharp 值为 true,那么会打印出 map 类型的字面量类型,以下是具体实现代码:

if p.fmt.sharpV {
    p.buf.writeString(f.Type().String())
    if f.IsNil() {
    	p.buf.writeString(nilParenString)
    	return
	}
	p.buf.writeByte('{')
} else {
	p.buf.writeString(mapString)
}

精细化控制

如果你想精细化地控制你的输出,制定一些自己的规则,那么就可以实现 Formatter 接口,就好像在 Node.js 里面,自行定义 toJSON 方法,那么在变量值在 JSON.stringify 方法调用的时候,会按照你想要的方式进行打印。

Formatter & toJSON

类似 toJSON 方法,在 node 里面,你如果实现了对象的 toJSON 方法,那么在 JSON.stringify 方法调用的时候就会按照这个 toJSON 方法来打印,而在 golang 里面,也有类似的方法,在 fmt 包中提供了 Formatter 这一个接口,只要你的结构体实现了 Formatter 接口的 format 方法,那么在 print 等方法打印时使用它来对值的打印进行格式化。

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

比如需要转化一个结构体的某个属性值:

type SomeValue struct {
	Name string
	Sex  int
}

func (s *SomeValue) Format(state fmt.State, verb rune) {
	switch verb {
	case 'v':
		if state.Flag('#') {
			fmt.Fprintf(state, "%T", s)
		}
		fmt.Fprint(state, "{ ")
		value := reflect.ValueOf(*s)
		for _, name := range someValFields {
			field := value.FieldByName(name)
			fmt.Fprintf(state, "%v:", name)
			if name == "Sex" {
				var _sex string
				if s.Sex == 1 {
					_sex = "man"
				} else {
					_sex = "woman"
				}
				fmt.Fprintf(state, "%s; ", _sex)
			} else {
				fmt.Fprint(state, field, "; ")
			}
		}
		fmt.Fprint(state, "}")
	}
}

someValType := reflect.TypeOf(SomeValue{})
someValFields := make([]string, someValType.NumField())
for i := 0; i < someValType.NumField(); i++ {
	someValFields[i] = someValType.Field(i).Name
}

v := &SomeValue{
	Name: "name",
	Sex:  1,
}

fmt.Printf("v: %v", v); 

Stringer & toString

类似 toJSON,node 里面对于值的打印,如果对象实现了 toString 方法,那么在转化为字符串的时候会调用 toString 方法取方法的值进行返回。fmt 包对此也有类似功能,也就是 Stringer 接口,结构体需要实现 String 方法达到个性化地转化字符串时的值。

type Stringer interface {
	String() string
}

请看下面例子代码:结构体里面的 Sex 属性原本是数字类型,但是为了打印出来更语义化,所以想要对于值做一些替换

type SomeValue struct {
	Name string
	Sex  int
}

func (s *SomeValue) String() string {
	sex := s.Sex
	var _sex string
	if sex == 1 {
		_sex = "man"
	} else {
		_sex = "woman"
	}

	return fmt.Sprintf("Name:%v, Sex:%v", s.Name, _sex)
}

v := &SomeValue{
	Name: "name",
	Sex:  1,
}

fmt.Println(v.String());
fmt.Printf("v: %v", v);

而对于 Formatter 和 Stringer 这两个接口,在 fmt 里面是 Formatter 优先级更高的,因为在实现的时候是先判断值是否符合 Formatter 接口即有没有实现 format 方法。

Sprint & Sprintln & Sprintf

这几个函数都和上面说的三个函数功能类似,只是这三个函数并不是将结果通过 io.stdout 输出,而是返回一个字符串。

Fprint & Fprintln & Fprintf

这三个函数的功能也和上面所说的函数类似,只是他们第一个接收的参数是一个输出流,如果知道 Node.js 的 stream 或者 shell 的重定向的同学应该知道,这个是 fmt 包提供用于重定向文件输出流的。

附录

go 居然也有 ...interface{}

如果函数声明了返回值,就直接用

func test(name string, sex string) (isMan bool, age int) {
    isMan = true
}