从源码学习 Go 标准库(一):fmt - format(2)
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
第一章的主角是 fmt
包,它包括 format
print
scan
errors
这四个部分,我们将按照这个顺序来依次分析。
本篇文章将继续带大家阅览 format.go
,并分析 fmt
的格式化器是如何通过不同的格式说明符来控制内容的打印格式。
备注:本系列文章使用的是 go 1.19 源码:
fmt + type
上一篇文章中,我们已经介绍完 fmt
格式化器的基本结构和部分方法,我们现在继续来看 fmt
是如何处理不同的格式化类型的。
所有的格式化方法都以 fmt
开头,我们可以先把它们梳理一下:
-
fmtBoolean
-
fmtUnicode
-
fmtInteger
-
fmtS & fmtBs : 前者是格式化字符串,后者是将字节切片格式化为字符串,实际上就是先截断再填充
-
fmtSbx :将字符串或字节切片格式化为其字节的十六进制编码,它是一个通用的处理
- fmtSx 和 fmtBx 是分不同情况对 fmtSbx 的调用
-
fmtQ :将字符串格式化为带双引号的转义 Go 字符串常量;如果格式说明符含
#
且字符串中不含除了\t
以外的控制字符,则返回带反引号的原始字符串 -
fmtC:将整数格式化为 Unicode 字符
-
fmtQc:将整数格式化为带单引号的转义 Go 字符常量
-
fmtFloat
fmtUnicode
源码较长,就不放完整的了,可以点上面的链接跳转到对应代码行,建议使用电脑阅读~
该函数的设计思想就是从右往左处理。它接收一个 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
首先判断符号,如果为负值,标记并把值转为正值。
然后检查缓存大小,它的最大长度不大于 3+wid+prec
,3
是为 符号(或省略符号位的空格) 和 进制前缀 保留的。
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
先得到字符串(切片)长度,它不大于精度(如果有精度格式符的话)。
然后,计算编码宽度:
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
转换的操作是由 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
}
先对返回的切片处理符号,如果切片带符号,我们直接舍弃保留的空间;如果没有符号,则在保留的空间填上加号。如果有空格格式符且没有加号格式符,则把前导加号改为空格。要特判 INF
或 NAN
。
下面是对 #
格式符的处理,它对于除 %b
以外其它浮点类格式符,要求强制打印小数点,并且不能删除后缀0。
然后是对 +
格式符的处理,如果有前导零,要先打印符号,再填充前导零和无符号数字。
最后,对于不需要打印符号的数字,直接填充无符号的数字。
总结
在本篇文章中,我们了解了格式化器针对不同格式化类型的具体操作方法。下一篇,我们将进入到 print.go
,看一看它是怎么将完整的格式化说明符分解成一个个操作的。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿
转载自:https://juejin.cn/post/7132131974067519502