携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
第一章的主角是 fmt 包,它包括 format print scan errors 这四个部分,我们将按照这个顺序来依次分析。
在上一篇文章中,我们学习了 print.go 中的类型和常量以及它们的用途,并介绍了打印机的对象重用机制。本篇文章,我们将梳理打印函数的执行流程,并进行更详细地分析。
备注:本系列文章使用的是 go 1.19 源码:
结果注释中的 · 代表一个空格
打印函数
第一层函数
在 Go 1.19 中一共有 3x4=12 种导出的打印函数,它们分别是:
| Printf | Println | |
|---|---|---|
| Fprintf | Fprint | Fprintln |
| Sprintf | Sprint | Sprintln |
| Appendf | Append | Appendln |
其中 Printf 系列实际上对 Fprintf 系列在 os.Stdout 上输出的封装。
Printf 系列和 Fprintf 系列向 io.Writer 接口输出,通常是写入到文件中,它们会返回写入的字节数和遇到的错误。
Sprintf 系列将输出转换为字符串并将其返回;Appendf 系列则是将输出追加到一个字节切片中并将新的切片返回。
以 f 结尾的函数可以根据格式说明符进行格式化,以 ln 结尾的函数会在多个输出间(始终)添加空格并在结尾添加空行,结尾什么都不带的只会在多个输出间添加空格,并且只有当相邻两个操作数都不是字符串时,才会在它们的输出中间添加空格。
s := []byte{'a', 'b', 'c', 'd', 'e'}
Print(s, "Print", 20.2, 2)
Fprintln(os.Stdout, s, "Fprintln", 21.24)
Println(Sprintf("%#v", s))
s = Appendln(s, "2333", "hahaha")
Printf("%q", s)
result:
[97 98 99 100 101]Print20.2 2[97 98 99 100 101] Fprintln 21.24
[]byte{0x61, 0x62, 0x63, 0x64, 0x65}
"abcde2333 hahaha\n"
第二层函数
上面这些导出的函数,又分别调用了下面这些函数:
| doPrintf | doPrint | doPrintln |
|---|
这些函数对应处理每种打印函数的特性。
doPrint & doPrintln
doPrint 和 doPrintln 都比较简单,doPrint 稍微多一些操作,我们先来看它。
prevString := false
for argNum, arg := range a {
isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String
if argNum > 0 && !isString && !prevString {
p.buf.writeByte(' ')
}
p.printArg(arg, 'v')
prevString = isString
}
prevString 用来记录前一个操作数是不是字符串。从第二个操作数开始,会判断自己是不是字符串以及前一个是不是字符串,如果都不是就先添加一个空格。
相比而言,doPrintln 没有检查类型是不是字符串的操作,然后在结尾多了一个添加空行的操作。
p.buf.writeByte('\n')
另外,我们也可以注意到它们在继续调用下一层函数时,使用的格式符是 v。
doPrintf
在分析 doPrintf 之前,我们先来看一下显示参数索引(以f结尾的打印函数可用)。
在格式符前显示地添加 [n] 标记可以告知程序在此处使用第 n 个参数。如果该标记后加上一个 * 可以用对应的参数来表示宽度或精度。使用了 [n] 标记的格式符后面的格式符默认按顺序继续调用第 n+1 、 n+2 ··· 个参数(这个特性可以用来打印相同的值多次)。
Printf("%[2]d %[1]d", 11, 22) // 22 11
Printf("%[3]*.[2]*[1]f", 12.0, 2, 6) // ·12.00
Printf("%d %d %#[1]x %#x", 16, 17) // 16 17 0x10 0x11
下面我们来看看 doPrintf 是怎么处理格式符的。
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 {
break
}
i++
p.fmt.clearflags()
整个格式符的处理过程在 formatLoop 循环中,它先寻找格式串中的 %,把它前面到上一个格式符之间的部分写入打印机缓存。
接下来是 simpleFormat 循环,它会处理简单的格式标志,包括
# 0 + - 空格 a-z
这个循环是一个快速处理,它不能处理精度、宽度或者索引,如果发现处理不了,就会跳出 simpleFormat 循环。
default:
if 'a' <= c && c <= 'z' && argNum < len(a) {
if c == 'v' {
p.fmt.sharpV = p.fmt.sharp
p.fmt.sharp = false
p.fmt.plusV = p.fmt.plus
p.fmt.plus = false
}
p.printArg(a[argNum], rune(c))
argNum++
i++
continue formatLoop
}
break simpleFormat
接下来,通过 argNumber 获取要计算的下一个参数、要处理的格式的下一个字节的索引以及要处理的格式是否有参数索引。
func (p *pp) argNumber(argNum int, format string, i int, numArgs int) (newArgNum, newi int, found bool) {
if len(format) <= i || format[i] != '[' {
return argNum, i, false
}
p.reordered = true
index, wid, ok := parseArgNumber(format[i:])
if ok && 0 <= index && index < numArgs {
return index, i + wid, true
}
p.goodArgNum = false
return argNum, i + wid, ok
}
一开始先排除两个简单的情况,然后通过 parseArgNumber 去找闭合方括号,如果存在会返回索引值(减去一)、到 ] 处理过的字节,以及索引是不是整数;如果 ] 不存在,处理的字节会被设为 1 。
然后是计算宽度和精度,这两个部分我们放到下一篇文章讲。
处理完宽度和精度后,如果当前格式无参数索引,还要再重新获取一遍要计算的参数,因为有可能在前面处理宽度和精度时被覆盖了。
if !afterIndex {
argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
}
if i >= end { // 如果后面没有格式化动词,需要填充错误字段
p.buf.writeString(noVerbString)
break
}
verb, size := rune(format[i]), 1
if verb >= utf8.RuneSelf {
verb, size = utf8.DecodeRuneInString(format[i:])
}
i += size
最后对不同的格式化动词,以及处理参数过程中出现的错误进行统一处理:
switch {
case verb == '%':
p.buf.writeByte('%')
case !p.goodArgNum:
p.badArgNum(verb) // 参数索引不是整数、大于参数个数以及其他可能的错误
case argNum >= len(a):
p.missingArg(verb) // 所需参数大于参数个数
case verb == 'v':
p.fmt.sharpV = p.fmt.sharp
p.fmt.sharp = false
p.fmt.plusV = p.fmt.plus
p.fmt.plus = false
fallthrough
default:
p.printArg(a[argNum], verb)
argNum++
}
总结
在本篇文章中,我们梳理了不同打印函数的输出格式,复习了显式参数索引的相关知识,并且分析了函数调用过程中的前两层。下一篇文章,我们会将 doPrintf 中宽度和精度的处理部分补充完整,并继续更深层的函数。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿