解读 Golang 的 fmt 包
zhangxiang958 opened this issue · 0 comments
fmt 包应该是我们学习 golang 最开始接触的包,它是用于格式化我们的输入输出流的。但是一开始接触会对于代码中一会使用 fmt.Println 一会使用 fmt.Printf 有些困惑,而且各种占位符会让人非常迷惑。也许深入研究下 fmt 包可以有效地解开这个困惑。
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
}