Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

解读 Golang 的 fmt 包 #53

Open
zhangxiang958 opened this issue Mar 29, 2020 · 0 comments
Open

解读 Golang 的 fmt 包 #53

zhangxiang958 opened this issue Mar 29, 2020 · 0 comments
Labels

Comments

@zhangxiang958
Copy link
Owner

zhangxiang958 commented Mar 29, 2020

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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant