package main
import (
“fmt”
)
func main() {
fmt.Println(“Hello World!”)
}
标准开场见多了,那内部标准库又是怎么输出这段英文的呢?今天一起来围观下源码吧 ?
原型
func Print(a …interface{}) (n int, err error) {
return Fprint(os.Stdout, a…)
}
func Println(a …interface{}) (n int, err error) {
return Fprintln(os.Stdout, a…)
}
func Printf(format string, a …interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a…)
}
Print:使用默认格式说明符打印格式并写入标准输出。当两者都不是字符串时,在操作数之间添加空格
Println:同上,不同的地方是始终在操作数之间添加空格,并附加换行符
Printf:根据格式说明符进行格式化并写入标准输出
以上三类就是最常见的格式化 I/O 的方法,我们将基于此去进行拆解描述
执行流程
案例一:Print
在这里我们使用 Print 方法做一个分析,便于后面的加深理解
func Print(a …interface{}) (n int, err error) {
return Fprint(os.Stdout, a…)
}
Print 使用默认格式说明符打印格式并写入标准输出。另外当两者都为非空字符串时将插入一个空格
原型
func Fprint(w io.Writer, a …interface{}) (n int, err error) {
p := newPrinter()
p.doPrint(a)
n, err = w.Write(p.buf)
p.free()
return
}
该函数一共有两个形参:
w:输出流,只要实现 io.Writer 就可以(抽象)为流的写入
a:任意类型的多个值
分析主干流程
1、 p := newPrinter(): 申请一个临时对象池(sync.Pool)
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
func newPrinter() pp {
p := ppFree.Get().(pp)
p.panicking = false
p.erroring = false
p.fmt.init(&p.buf)
return p
}
ppFree.Get():基于 sync.Pool 实现 *pp 的临时对象池,每次获取一定会返回一个新的 pp 对象用于接下来的处理
*pp.panicking:用于解决无限递归的 panic、recover 问题,会根据该参数在 catchPanic 及时掐断
*pp.erroring:用于表示正在处理错误无效的 verb 标识符,主要作用是防止调用 handleMethods 方法
*pp.fmt.init(&p.buf):初始化 fmt 配置,会设置 buf 并且清空 fmtFlags 标志位
2、 p.doPrint(a): 执行约定的格式化动作(参数间增加一个空格、最后一个参数增加换行符)
func (p *pp) doPrint(a []interface{}) {
prevString := false
for argNum, arg := range a {
true && false
isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String
// Add a space between two non-string arguments.
if argNum > 0 && !isString && !prevString {
p.buf.WriteByte(‘ ‘)
}
p.printArg(arg, ‘v’)
prevString = isString
}
}
可以看到底层通过判断该入参,同时满足以下条件就会添加分隔符(空格):
当前入参为多个参数(例如:Slice)
当前入参不为 nil 且不为字符串(通过反射确定)
当前入参不为首项或上一个入参不为字符串
而在 Print 方法中,不需要指定格式符。实际上在该方法内直接指定为 v。也就是默认格式的值
p.printArg(arg, ‘v’)
w.Write(p.buf): 写入标准输出(io.Writer)
*pp.free(): 释放已缓存的内容。在使用完临时对象后,会将 buf、arg、value 清空再重新存放到 ppFree 中。以便于后面再取出重用(利用 sync.Pool 的临时对象特性)
案例二:Printf
标识符
Verbs
%v the value in a default format
when printing structs, the plus flag (%+v) adds field names
%#v a Go-syntax representation of the value
%T a Go-syntax representation of the type of the value
%% a literal percent sign; consumes no value
%t the word true or false
Flags
0X for hex (%#X); suppress 0x for %p (%#p);
for %q, print a raw (backquoted) string if strconv.CanBackquote
returns true;
always print a decimal point for %e, %E, %f, %F, %g and %G;
do not remove trailing zeros for %g and %G;
write e.g. U+0078 ‘x’ if the character is printable for %U (%#U).
‘ ‘ (space) leave a space for elided sign in numbers (% d);
put spaces between bytes printing strings or slices in hex (% x, % X)
0 pad with leading zeros rather than spaces;
for numbers, this moves the padding after the sign
详细建议参见 Godoc
原型
func Fprintf(w io.Writer, format string, a …interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
与 Print 相比,最大的不同就是 doPrintf 方法了。在这里我们来详细看看其代码,如下:
func (p *pp) doPrintf(format string, a []interface{}) {
end := len(format)
argNum := 0 // we process one argument per non-trivial format
afterIndex := false // previous item in format was an index like [3].
p.reordered = false
formatLoop:
for i := 0; i < end; {
p.goodArgNum = true
lasti := i
for i < end && format[i] != ‘%’ {
i++
}
if i > lasti {
p.buf.WriteString(format[lasti:i])
}
if i >= end {
// done processing format string
break
}
// Process one verb
i++
// Do we have flags?
p.fmt.clearflags()
simpleFormat:
for ; i < end; i++ {
c := format[i]
switch c {
case '#': //'#'、'0'、'+'、'-'、' '
...
default:
if 'a' <= c && c <= 'z' && argNum < len(a) {
...
p.printArg(a[argNum], rune(c))
argNum++
i++
continue formatLoop
}
break simpleFormat
}
}
// Do we have an explicit argument index?
argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
// Do we have width?
if i < end && format[i] == '*' {
...
}
// Do we have precision?
if i+1 < end && format[i] == '.' {
...
}
if !afterIndex {
argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
}
if i >= end {
p.buf.WriteString(noVerbString)
break
}
...
switch {
case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.
p.buf.WriteByte('%')
case !p.goodArgNum:
p.badArgNum(verb)
case argNum >= len(a): // No argument left over to print for the current verb.
p.missingArg(verb)
case verb == 'v':
...
fallthrough
default:
p.printArg(a[argNum], verb)
argNum++
}
}
if !p.reordered && argNum < len(a) {
...
} } 分析主干流程 写入 % 之前的字符内容 如果所有标志位处理完毕(到达字符尾部),则跳出处理逻辑 (往后移)跳过 % ,开始处理其他 verb 标志位 清空(重新初始化) fmt 配置 处理一些基础的 verb 标识符(simpleFormat)。如:'#'、'0'、'+'、'-'、' ' 以及简单的 verbs 标识符(不包含精度、宽度和参数索引)。需要注意的是,若当前字符为简单 verb 标识符。则直接进行处理。完成后会直接后移到下一个字符。其余标志位则变更 fmt 配置项,便于后续处理 处理参数索引(argument index) 处理参数宽度(width) 处理参数精度(precision) % 之后若不存在 verbs 标识符则返回 noVerbString。值为 %!(NOVERB) 处理特殊 verbs 标识符(如:'%%'、'%#v'、'%+v')、错误情况(如:参数索引指定错误、参数集个数与 verbs 标识符数量不匹配)或进行格式化参数集 常规流程处理完毕 在特殊情况下,若提供的参数集比 verb 标识符多。fmt 将会贪婪检查下去,将多出的参数集以特定的格式输出,如下:
fmt.Printf(“%d”, 1, 2, 3)
// 1%!(EXTRA int=2, int=3)
约定前缀额外标志:%!(EXTRA
当前参数的类型
约定格式符:=
当前参数的值(默认以 %v 格式化)
约定格式符:)
值得注意的是,当指定了参数索引或实际处理的参数小于入参的参数集时,就不会进行贪婪匹配来展示
案例三:Println
原型
func Fprintln(w io.Writer, a …interface{}) (n int, err error) {
p := newPrinter()
p.doPrintln(a)
n, err = w.Write(p.buf)
p.free()
return
}
在这个方法中,最大的区别就是 doPrintln,我们一起来看看,如下:
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’)
}
分析主干流程
循环入参的参数集,并以空格分隔
格式化当前参数,默认以 %v 对参数进行格式化
在结尾添加 \n 字符
如何格式化参数
在上例的执行流程分析中,可以看到格式化参数这一步是在 p.printArg(arg, verb) 执行的,我们一起来看看它都做了些什么?
func (p *pp) printArg(arg interface{}, verb rune) {
p.arg = arg
p.value = reflect.Value{}
if arg == nil {
switch verb {
case 'T', 'v':
p.fmt.padString(nilAngleString)
default:
p.badVerb(verb)
}
return
}
switch verb {
case 'T':
p.fmt.fmt_s(reflect.TypeOf(arg).String())
return
case 'p':
p.fmtPointer(reflect.ValueOf(arg), 'p')
return
}
// Some types can be done without reflection.
switch f := arg.(type) {
case bool:
p.fmtBool(f, verb)
case float32:
p.fmtFloat(float64(f), 32, verb)
...
case reflect.Value:
if f.IsValid() && f.CanInterface() {
p.arg = f.Interface()
if p.handleMethods(verb) {
return
}
}
p.printValue(f, verb, 0)
default:
if !p.handleMethods(verb) {
p.printValue(reflect.ValueOf(f), verb, 0)
}
} } 在小节代码中可以看见,fmt 本身对不同的类型做了不同的处理。这样子就避免了通过反射确定。相对的提高了性能
其中有两个特殊的方法,分别是 handleMethods 和 badVerb,接下来分别来看看他们的作用是什么
1、badVerb
它主要用于格式化并处理错误的行为。我们可以一起来看看,代码如下:
func (p *pp) badVerb(verb rune) {
p.erroring = true
p.buf.WriteString(percentBangString)
p.buf.WriteRune(verb)
p.buf.WriteByte(‘(‘)
switch {
case p.arg != nil:
p.buf.WriteString(reflect.TypeOf(p.arg).String())
p.buf.WriteByte(‘=’)
p.printArg(p.arg, ‘v’)
…
default:
p.buf.WriteString(nilAngleString)
}
p.buf.WriteByte(‘)’)
p.erroring = false
}
在处理错误格式化时,我们可以对比以下例子:
fmt.Printf(“%s”, []int64{1, 2, 3})
// [%!s(int64=1) %!s(int64=2) %!s(int64=3)]%
在 badVerb 中可以看到错误字符串的处理主要分为以下部分:
约定前缀错误标志:%!
当前的格式化操作符
约定格式符:(
当前参数的类型
约定格式符:=
当前参数的值(默认以 %v 格式化)
约定格式符:)
2、handleMethods
func (p *pp) handleMethods(verb rune) (handled bool) {
if p.erroring {
return
}
// Is it a Formatter?
if formatter, ok := p.arg.(Formatter); ok {
handled = true
defer p.catchPanic(p.arg, verb)
formatter.Format(p, verb)
return
}
// If we're doing Go syntax and the argument knows how to supply it, take care of it now.
...
return false } 这个方法比较特殊,一般在自定义结构体和未知情况下进行调用。主要流程是:
若当前参数为错误 verb 标识符,则直接返回
判断是否实现了 Formatter
实现,则利用自定义 Formatter 格式化参数
未实现,则最大程度的利用 Go syntax 默认规则去格式化参数
拓展
在 fmt 标准库中可以通过自定义结构体来实现方法的自定义,大致如下几种
fmt.State
type State interface {
Write(b []byte) (n int, err error)
Width() (wid int, ok bool)
Precision() (prec int, ok bool)
Flag(c int) bool } State 用于获取标志位的状态值,涉及如下:
Write:将格式化完毕的字符写入缓冲区中,等待下一步处理
Width:返回宽度信息和是否被设置
Precision:返回精度信息和是否被设置
Flag:返回特殊标志符(’#’、’0’、’+’、’-‘、’ ‘)是否被设置
fmt.Formatter
type Formatter interface {
Format(f State, c rune)
}
Formatter 用于实现自定义格式化方法。可通过在自定义结构体中实现 Format 方法来实现这个目的
另外,可以通过 f 获取到当前标识符的宽度、精度等状态值。c 为 verb 标识符,可以得到其动作是什么
fmt.Stringer
type Stringer interface {
String() string
}
当该对象为 String、Array、Slice 等类型时,将会调用 String() 方法对类字符串进行格式化
fmt.GoStringer
type GoStringer interface {
GoString() string
}
当格式化特定 verb 标识符(%v)时,将调用 GoString() 方法对其进行格式化
总结
通过本文对 fmt 标准库的分析,可以发现它有以下特点:
在拓展性方面,可以自定义格式化方法等
在完整度方面,尽可能的贪婪匹配,输出参数集
在性能方面,每种不同的参数类型,都实现了不同的格式化处理操作
在性能方面,尽可能的最短匹配,格式化参数集
format
fmt包虽然不建议用来打印日志,但是格式化字符串确实是必不可少的,比如打印日志的时候。先详细介绍一下格式化的格式format。
format由百分号%开始,后面的部分可以分为四部分:
verb 占位符。
完整的格式可以参考Go 文档,下面我大概列几个:
%v 通过默认格式打印
%t 用于布尔类型,打印true或者false
%d 以10进制格式打印数字
%c 将数据转换成 Unicode 里面的字符打印
%x 以16进制格式打印数字
%e 科学计数法表示
%f 以10进制表示浮点数
%s 字符串
%p 指针,以0x开头的16进制地址
还有Go语言自己定义的类型:
%#v
宽度
比如%3c,c是占位符,表示把整数转成 Unicode 字符展示,而前面的3就是宽度了。
源码如下,看到num = num*10 + int(s[newi]-‘0’)很熟悉有没有,就是一个把字符转成整形的方法。那么%3c返回的num就是3了。
func parsenum(s string, start, end int) (num int, isnum bool, newi int) {
if start >= end {
return 0, false, end
}
for newi = start; newi < end && ‘0’ <= s[newi] && s[newi] <= ‘9’; newi++ {
if tooLarge(num) {
return 0, false, end // Overflow; crazy long number most likely.
}
num = num*10 + int(s[newi]-‘0’)
isnum = true
}
return
}
打印下面的语句:
fmt.Printf(“%3c\n”, ‘a’)
控制输出3位,a输出占用一位,前面需要补两个0。在源码中,是通过pad实现:
func (f *fmt) pad(b []byte) {
…
width := f.wid - utf8.RuneCount(b)
if !f.minus {
// left padding
f.writePadding(width)
f.buf.Write(b)
} else {
// right padding
f.buf.Write(b)
f.writePadding(width)
}
}
上面的代码width就是2,调用writePadding会打印对应宽度的空格。
如果打印的内容很长,比如有10位,而宽度只设置了3位,会展示完整的数字还是只显示3位呢?
答案是完整展示,当n小于等于0,直接返回,而打印内容方面,不受影响:
func (f *fmt) writePadding(n int) {
if n <= 0 { // No padding bytes needed.
return
}
…
精度
比如%3.2f,f是占位符,表示浮点数展示。3表示宽度,而小数点后面的2则是精度。精度在浮点数的格式化中会用到。精度的控制是通过strconv包的字符串转换函数来实现的:
num := strconv.AppendFloat(f.intbuf[:1], v, byte(verb), prec, size)
标记
除了宽度和精度,还有标记可以用来控制输出。
为十六进制添加前导 0x(%#x)或 0X(%#X),为 %p(%#p)去掉前导 0x;
如果可能的话,%q(%#q)会打印原始 (即反引号围绕的)字符串;
如果是可打印字符,%U(%#U)会写出该字符的
Unicode 编码形式(如字符 x 会被打印成 U+0078 ‘x’)。
‘ ‘ (空格)为数值中省略的正负号留出空白(% d);
以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开
0 填充前导的0而非空格;对于数字,这会将填充移到正负号之后
fmt.State 和 fmt.Formatter
上面提到的占位符、宽度、精度和标记,除了占位符,剩下的3个在解析后被保存到了接口fmt.State里面。这个接口还增加了一个函数Write用于写入数据。
type State interface {
// Write is the function to call to emit formatted output to be printed.
Write(b []byte) (n int, err error)
// Width returns the value of the width option and whether it has been set.
Width() (wid int, ok bool)
// Precision returns the value of the precision option and whether it has been set.
Precision() (prec int, ok bool)
// Flag reports whether the flag c, a character, has been set.
Flag(c int) bool }
它会在Formatter接口中被用到。参数c就是占位符,这些终于都凑齐了。这个接口用来自定义格式化方法,你可以在自己的结构体中实现Format函数来实现自动调用解析。
type Formatter interface {
Format(f State, c rune)
}
常见类型的格式化方法
func (p *pp) printArg(arg interface{}, verb rune)是底层真正进行转换的函数。
指针 %p,类型 %T
func (p *pp) printArg(arg interface{}, verb rune) {
…
// Special processing considerations.
// %T (the value’s type) and %p (its address) are special; we always do them first.
switch verb {
case ‘T’:
p.fmt.fmtS(reflect.TypeOf(arg).String())
return
case ‘p’:
p.fmtPointer(reflect.ValueOf(arg), ‘p’)
return
}
…
对于类型和指针的转换,有现成的方法调用,而这两个转换都是通过反射实现。
这里并没有判断是否调用用户自定义的 Format 函数,说明所有类型打印内存地址和类型都只能通过上面的代码实现,不能自定义。
数字
数字支持多种进制,16进制、8进制、4进制、2进制、10进制。在fmtInteger中通过求余法实现。
switch base {
case 10:
for u >= 10 {
i–
next := u / 10
buf[i] = byte(‘0’ + u - next*10)
u = next
}
…
万能通用格式,%v
万能格式其实也有映射关系:
int, int8 etc.: %d
uint, uint8 etc.: %d, %#x if printed with %#v
float32, complex64, etc: %g
string: %s
chan: %p
pointer: %p
一般结构体会用到这种打印方式。如果是结构体:
if p.fmt.sharpV {
p.buf.WriteString(f.Type().String())
}
p.buf.WriteByte(‘{‘)
for i := 0; i < f.NumField(); i++ {
if i > 0 {
if p.fmt.sharpV {
p.buf.WriteString(commaSpaceString)
} else {
p.buf.WriteByte(‘ ‘)
}
}
if p.fmt.plusV || p.fmt.sharpV {
if name := f.Type().Field(i).Name; name != “” {
p.buf.WriteString(name)
p.buf.WriteByte(‘:’)
}
}
p.printValue(getField(f, i), verb, depth+1)
}
p.buf.WriteByte(‘}’)
通过反射拿到字段 Field 和内容,如果格式是%+v,也就是p.fmt.plusV是true,这样会打印字段名称。
异常
转换的时候还会有异常捕获,这个在 Go 源码中不多见:
defer p.catchPanic(p.arg, verb)
p.fmtString(v.String(), verb)
如果在转换的时候发生异常panic,并不会发生异常,转换后的结果会是这个样子:
type data struct {
A string
B int
}
func (d *data) String() string {
panic(“implement me”)
}
func main() {
d := &data{“1”, 2}
fmt.Printf(“%s\n”, d) // prints: %!s(PANIC=implement me)
}
结果是%!s(PANIC=implement me),会有 PANIC 的字样。还有一个地方很有趣,String()方法并没有按要求返回字符串,只有一个panic,这样可以编译过。
fmt.Stringer
顺道介绍一下Stringer接口,上面的data对象就实现了这个方法。如果是通过%s打印,或者直接调用的Println,这时候会判断这个对象是否实现了Stringer接口,如果实现了,就调用对象的String方法,上一节的data就是这个例子。
type Stringer interface {
String() string
}
一个 fmt.Formatter 例子
还是针对上面的data类型,我实现了Formatter接口:
func (d *data) Format(f fmt.State, c rune) {
switch c {
case ‘v’: // &{1 2}
buf, err := json.Marshal(d)
if err != nil {
panic(err)
}
f.Write(buf)
case ‘s’:
f.Write([]byte(d.String()))
case ‘x’, ‘X’:
//case ‘p’:
v := reflect.ValueOf(d)
f.Write([]byte{‘(‘})
f.Write([]byte(v.Type().String()))
f.Write([]byte{‘)’, ‘(‘})
u := v.Pointer()
f.Write([]byte(strconv.FormatUint(uint64(u), 16)))
f.Write([]byte{‘)’})
default:
f.Write([]byte(“http://cyeam.com”))
}
}
d := &data{“1”, 2}
fmt.Printf(“v %v\n”, d)
fmt.Printf(“s %s\n”, d)
fmt.Printf(“p %p\n”, d)
fmt.Printf(“T %T\n”, d)
fmt.Printf(“b %b\n”, d)
fmt.Printf(“o %o\n”, d)
fmt.Printf(“x %x\n”, d)
fmt.Printf(“d %d\n”, d)
结果如下:
v {“A”:”1”,”B”:2} s {“A”:”1”,”B”:2} p 0xc00006c020 T main.data b http://cyeam.com o http://cyeam.com x (main.data)(c00006c020) d http://cyeam.com
b、o、d我没有实现,所以返回的是一个默认值;
v是返回的json编码;
p和T在前面也介绍了,它并不会调用Format,所以虽然我并没有实现这两个占位符,但是结果是对的;
x手写一个基于反射的实现,能返回变量名称和地址。
完整流程
格式解析,把fmt.State接口要用到的数据解析完成
func (p pp) doPrintf(format string, a []interface{}) {
…
// Do we have flags?
// 解析格式串中的标记
for ; i < end; i++ {
c := format[i]
switch c {
case ‘#’:
p.fmt.sharp = true
case ‘0’:
p.fmt.zero = !p.fmt.minus // Only allow zero padding to the left.
case ‘+’:
p.fmt.plus = true
case ‘-‘:
p.fmt.minus = true
p.fmt.zero = false // Do not pad with zeros to the right.
case ‘ ‘:
p.fmt.space = true
default:
}
…
// Do we have width?
if i < end && format[i] == ‘’ {
…
} else {
// 解析了格式串中的宽度内容
p.fmt.wid, p.fmt.widPresent, i = parsenum(format, i, end)
if afterIndex && p.fmt.widPresent { // “%[3]2d”
p.goodArgNum = false
}
}
…
// Do we have precision?
if i+1 < end && format[i] == ‘.’ {
…
// 解析了格式串中的精度内容
p.fmt.prec, p.fmt.precPresent, i = parsenum(format, i, end)
…
}
在func (p *pp) printArg(arg interface{}, verb rune)中进行格式化转换编码;
如果对象值是空,直接打印
if arg == nil {
switch verb {
case ‘T’, ‘v’:
p.fmt.padString(nilAngleString)
default:
p.badVerb(verb)
}
return
}
如果是指针或者类型格式化,调用反射实现
switch verb {
case ‘T’:
p.fmt.fmtS(reflect.TypeOf(arg).String())
return
case ‘p’:
p.fmtPointer(reflect.ValueOf(arg), ‘p’)
return
}
格式化数据
switch f := arg.(type) {
case bool:
p.fmtBool(f, verb)
case float32:
p.fmtFloat(float64(f), 32, verb)
case float64:
…
default:
if !p.handleMethods(verb) {
// Need to use reflection, since the type had no
// interface methods that could be used for formatting.
p.printValue(reflect.ValueOf(f), verb, 0)
}
}
每种内置类型都有自己的格式化实现,这样就避免了反射;
如果不是内置类型,判断是否实现了Formatter接口,如果实现了调用此接口;
如果需要转成字符串,而对象实现了Stringer接口,调用其String方法转换;
上面两个逻辑在函数func (p *pp) handleMethods(verb rune) (handled bool)中,如果能通过接口实现转换,返回true并格式化数据,否则返回false;(其实还有一些细节的逻辑,例如GoStringer,我就不展开细说了)
如果通过上面的转换失败,则需要使用默认转换策略。
默认转换策略 p.printValue(reflect.ValueOf(f), verb, 0)
默认转换就是通过反射实现,以结构体为例,如果反射出来是结构体,那就遍历所有字段打印,逻辑和上面提到的万能转换里提到的差不多。
总结
从格式化的完整流程中可以发现,底层格式化算法是有对性能优化的,那就是通过对每种内置对象单独编写格式化实现来规避反射来提高性能。
实际工作中经常需要对系统内复杂结构进行格式化,那么为这些对象实现Formatter接口也算是一种提升性能的有效方式。