likes
comments
collection
share

从源码学习 Go 标准库(一):fmt - print(3)

作者站长头像
站长
· 阅读数 44

前言

本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。

第一章的主角是 fmt 包,它包括 format print scan errors 这四个部分,我们将按照这个顺序来依次分析。

上一篇文章中,我们梳理了不同打印函数的输出格式,复习了显式参数索引的相关知识,并且分析了函数调用过程中的前三层。本篇文章,我们会将 doPrintf 中宽度和精度的处理部分补充完整,并继续分析更深层的函数。

备注:本系列文章使用的是 go 1.19 源码:

github.com/golang/go/t…

结果注释中的 · 代表一个空格

打印函数

doPrintf

宽度处理

github.com/golang/go/b…

在处理参数索引之后,按照顺序先处理宽度。这里会出现几种情况:

  • 格式符下一个字节是 *,则用参数控制宽度,它分为两种

    • 带索引的 %[2]*[1]d

    • 不带索引的 %*[2]d

  • 没有 *

    • 但是同时有索引和宽度数字 %[3]2d ,错误

    • 索引和数字只有一个,前者无宽度,后者有宽度

先看带星号的情况:通过参数索引调用 intFromArg 获取参数。

github.com/golang/go/b…

这个函数同时会检查参数是否是整型,以及是否太大,从而判断能不能做宽度。

回到 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 返回下一个整数,如果索引和数字同时存在,则索引错误。

精度处理

github.com/golang/go/b…

先判断是否带 .,带 . 则分为:

  • 索引后直接是点 %[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(')')
}

第三层函数

github.com/golang/go/b…

现在,我们来到第三层函数 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 函数。

最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿