likes
comments
collection
share

从源码学习 Go 标准库(一):fmt - format(2)

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

前言

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

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

本篇文章将继续带大家阅览 format.go,并分析 fmt 的格式化器是如何通过不同的格式说明符来控制内容的打印格式。

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

github.com/golang/go/t…

fmt + type

上一篇文章中,我们已经介绍完 fmt 格式化器的基本结构和部分方法,我们现在继续来看 fmt 是如何处理不同的格式化类型的。

所有的格式化方法都以 fmt 开头,我们可以先把它们梳理一下:

  • fmtBoolean

  • fmtUnicode

  • fmtInteger

  • fmtS & fmtBs : 前者是格式化字符串,后者是将字节切片格式化为字符串,实际上就是先截断再填充

  • fmtSbx :将字符串或字节切片格式化为其字节的十六进制编码,它是一个通用的处理

    • fmtSx 和 fmtBx 是分不同情况对 fmtSbx 的调用
  • fmtQ :将字符串格式化为带双引号的转义 Go 字符串常量;如果格式说明符含 # 且字符串中不含除了 \t 以外的控制字符,则返回带反引号的原始字符串

  • fmtC:将整数格式化为 Unicode 字符

  • fmtQc:将整数格式化为带单引号的转义 Go 字符常量

  • fmtFloat

fmtUnicode

github.com/golang/go/b…

源码较长,就不放完整的了,可以点上面的链接跳转到对应代码行,建议使用电脑阅读~

该函数的设计思想就是从右往左处理。它接收一个 uint64 类型的值,先检查缓存的长度,最长的长度是 9+prec (格式是 U+number 'x'x 是一个 utf-8 字符),默认精度(prec)是4。

如果是 %#U 且 该值在 rune 的表示范围内 且 该值转为字符后可打印,就从后往前加入'\'', rune(u), '\'' 和 ' '。

然后,当整数值大于15时,也就是用16进制表示大于1位时,循环处理:

for u >= 16 {
        i--
        buf[i] = udigits[u&0xF]
        prec--
        u >>= 4
}
i--
buf[i] = udigits[u]
prec--

u&0xF 通过位运算提取十六进制表示的末位,然后转为大写的十六进制表示,整数右移4位去掉处理过的位。

最后用0补齐剩余的精度位数,添加 '+', 'U',然后填充进缓存中。

后面的代码注释中均使用 · 来代表一个空格。

Printf("%U", '我') // U+6211
Printf("%#U", '我') // U+6211·'我'
Printf("%#U", 1) // U+0001  // 对于不可打印字符,即使带#也不会出现 'x'
Printf("%#U", -1) // U+FFFFFFFFFFFFFFFF

fmtInteger

github.com/golang/go/b…

首先判断符号,如果为负值,标记并把值转为正值。

然后检查缓存大小,它的最大长度不大于 3+wid+prec3 是为 符号(或省略符号位的空格) 和 进制前缀 保留的。

prec := 0
if f.precPresent {
        prec = f.prec
        if prec == 0 && u == 0 {
                oldZero := f.zero
                f.zero = false
                f.writePadding(f.wid)
                f.zero = oldZero
                return
        }
} else if f.zero && f.widPresent {
        prec = f.wid
        if negative || f.plus || f.space {
                prec--
        }
}

如果精度和值都为零,会用空格填充宽度(默认宽度为0,也就是什么都不打印)。

Printf("%5.0d", 0)  // ·····

如果有精度格式符,且精度不为0或值不为0,会用精度控制前导零个数,并且会忽略前导零格式符并用空格填充来代替它。

Printf("%05.2d", 2) // ···02

如果没有精度格式符,有前导零和宽度格式符,用宽度替代精度。

Printf("%05d", 2) // 00002

整数可以按不同进制表示进行输出,其中十进制使用常量进行除法和模运算,其余进制表示使用位运算,代码和 fmtUnicode 中类似。这里同样是从右往左处理。

接着,处理依次前导零,进制前缀和符号(或省略符号位的空格)

Printf("%b", 27) // 11011
Printf("%#b", 27) // 0b11011
Printf("%#x", 27) // 0x1b
Printf("%#X", 27) // 0X1B
Printf("%#5o", 27) // ··033
Printf("%#05o", 27) // 00033  // 当已经有前导零时,%o格式符不会再添加八进制前缀0
Printf("%#05O", 27) // 0o00033
Printf("%+#05O", 27) // +0o0033
Printf("% #05O", 27) // ·0o0033
Printf("% #05O", -27) // -0o0033

fmtSbx

github.com/golang/go/b…

先得到字符串(切片)长度,它不大于精度(如果有精度格式符的话)。

然后,计算编码宽度:

width := 2 * length
if width > 0 {
        if f.space {
                if f.sharp {
                        width *= 2
                }
                width += length - 1
        } else if f.sharp {
                width += 2
        }
} else {
        if f.widPresent {
                f.writePadding(f.wid)  // 如果是空字符串(切片),根据宽度控制符,填充空格
        }
        return
}

可以对照下面的示例来理解代码:

Printf("%x", "我爱Go") // e68891e788b1476f
Printf("% #x", "我爱Go") // 0xe6 0x88 0x91 0xe7 0x88 0xb1 0x47 0x6f
Printf("% x", "我爱Go") // e6 88 91 e7 88 b1 47 6f
Printf("%#x", "我爱Go") // 0xe68891e788b1476f

从左往右处理,先判断左边是否需要填充。

if f.widPresent && f.wid > width && !f.minus {
        f.writePadding(f.wid - width)
}

然后,通过循环加位运算,并根据格式符,来处理字符和十六进制的转换。

最后,判断右边是否需要填充。

fmtFloat

github.com/golang/go/b…

转换的操作是由 strconv 包处理的,返回给函数一个字节切片,注意到传入的缓存是从位置1开始的,我们把位置0保留下来以便需要前面的+号。

num := strconv.AppendFloat(f.intbuf[:1], v, byte(verb), prec, size)
if num[1] == '-' || num[1] == '+' {
        num = num[1:]
} else {
        num[0] = '+'
}
if f.space && num[0] == '+' && !f.plus {
        num[0] = ' '
}
if num[1] == 'I' || num[1] == 'N' {
        oldZero := f.zero
        f.zero = false
        if num[1] == 'N' && !f.space && !f.plus {
                num = num[1:]
        }
        f.pad(num)
        f.zero = oldZero
        return
}

先对返回的切片处理符号,如果切片带符号,我们直接舍弃保留的空间;如果没有符号,则在保留的空间填上加号。如果有空格格式符且没有加号格式符,则把前导加号改为空格。要特判 INFNAN

下面是对 # 格式符的处理,它对于除 %b 以外其它浮点类格式符,要求强制打印小数点,并且不能删除后缀0。

然后是对 + 格式符的处理,如果有前导零,要先打印符号,再填充前导零和无符号数字。

最后,对于不需要打印符号的数字,直接填充无符号的数字。

总结

在本篇文章中,我们了解了格式化器针对不同格式化类型的具体操作方法。下一篇,我们将进入到 print.go,看一看它是怎么将完整的格式化说明符分解成一个个操作的。

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

转载自:https://juejin.cn/post/7132131974067519502
评论
请登录