从源码学习 Go 标准库(一):fmt - print(3)
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
第一章的主角是 fmt
包,它包括 format
print
scan
errors
这四个部分,我们将按照这个顺序来依次分析。
在上一篇文章中,我们梳理了不同打印函数的输出格式,复习了显式参数索引的相关知识,并且分析了函数调用过程中的前三层。本篇文章,我们会将 doPrintf
中宽度和精度的处理部分补充完整,并继续分析更深层的函数。
备注:本系列文章使用的是 go 1.19 源码:
结果注释中的 · 代表一个空格
打印函数
doPrintf
宽度处理
在处理参数索引之后,按照顺序先处理宽度。这里会出现几种情况:
-
格式符下一个字节是
*
,则用参数控制宽度,它分为两种-
带索引的
%[2]*[1]d
-
不带索引的
%*[2]d
-
-
没有
*
,-
但是同时有索引和宽度数字
%[3]2d
,错误 -
索引和数字只有一个,前者无宽度,后者有宽度
-
先看带星号的情况:通过参数索引调用 intFromArg
获取参数。
这个函数同时会检查参数是否是整型,以及是否太大,从而判断能不能做宽度。
回到 doPrintf
中,在接收到返回值后,先检查有没有出错,然后如果宽度值是负的,要把宽度值变成正的,然后设置左对齐标记。
if !p.fmt.widPresent {
p.buf.writeString(badWidthString)
}
if p.fmt.wid < 0 {
p.fmt.wid = -p.fmt.wid
p.fmt.minus = true
p.fmt.zero = false
}
afterIndex = false
再来看不带星号的情况,通过 parsenum
返回下一个整数,如果索引和数字同时存在,则索引错误。
精度处理
先判断是否带 .
,带 .
则分为:
-
索引后直接是点
%[3].2d
,错误 -
获取下一个参数和要处理的字节,如果有
*
,- 判断参数值:负精度、精度值不是整数或过大,报错
-
如果没有
*
,获取下一个整数- 没有整数则精度值为0
这里有一些可能的错误情况并没有处理,而是放到之后的 printArg
中处理。例如:
Printf("%3.[2][1]f\n", 12.256, 2, 6) // %![(int=··2)1]f
这里面第一次循环中处理的是 %3.[2][
, [
被当做格式动词传入 printArg
,然后在 badVerb
中打印错误 %![(int=··2)
,1]f
在第二次循环中被当做字符串打印。
doPrintf
结尾还会检查额外的参数,并报错(如果参数索引没有超出范围的话)。
if !p.reordered && argNum < len(a) {
p.fmt.clearflags()
p.buf.writeString(extraString)
for i, arg := range a[argNum:] {
// 按照如 type1=value1, <nil>, type3=value3 的格式打印错误
}
p.buf.writeByte(')')
}
第三层函数
现在,我们来到第三层函数 printArg
,它是一个分类器,通过判断参数和格式化动词的值或类型,来调用不同的处理函数。
在函数的开始,先对一些特殊的情况做处理。
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.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:
p.fmtFloat(f, 64, verb)
case complex64:
p.fmtComplex(complex128(f), 64, verb)
// ...
特别的是,对于非基本数据类型,它会先调用 handleMethods
查找接口是否有相应的格式化方法,即 String
Gostring
Format
Error
这些方法,如果没有再调用 printValue
去处理接口的反射值。
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)
}
}
总结
在本篇文章中,我们将 doPrintf
的宽度和精度处理部分补充完整,分析了第四层函数 printArg
的全部执行过程,并介绍了 fmt
中的导出接口方法的调用位置。下一篇文章,我们继续分析 handleMethods
printValue
以及 fmtxxx
函数。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿
转载自:https://juejin.cn/post/7133616969588146184