携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
第一章的主角是 fmt 包,它包括 format print scan errors 这四个部分,我们将按照这个顺序来依次分析。
在上一篇文章中,我们学习了 ScanState 和 readRune 接口的方法,这些方法是扫描操作的底层函数,就像 fmt.go 对于 print.go 的关系一样,但是这里把它们都写在一个文件里了。在本篇文章中,我们可以开始愉快地分析具体的扫描操作了。
备注:本系列文章使用的是 go 1.19 源码:
结果注释中的 · 代表一个空格
scan+type
这些函数都不会区分字母的大小写。
scanBool
先跳过空白符并判断下一个字符不是EOF,然后判断格式动词是否是 tv 中的一个
if !s.okVerb(verb, "tv", "boolean") {
return false
}
OkVerb 会返回自定义错误信息,可以通过 err 接收到:
var b bool
_, err := Sscanf("true", "%f", &b)
Print(b, err) // false bad verb '%f' for boolean
获取下一个字符并判断:
switch s.getRune() {
case '0':
return false
case '1':
return true
case 't', 'T':
if s.accept("rR") && (!s.accept("uU") || !s.accept("eE")) {
s.error(boolError)
}
return true
case 'f', 'F':
if s.accept("aA") && (!s.accept("lL") || !s.accept("sS") || !s.accept("eE")) {
s.error(boolError)
}
return false
}
return false
1 t true 表示 true, 0 f false 以及 其他非上述字符 表示 false;如果 true 和 false 拼写不全会触发错误。
var b bool
n, err := Sscanf("t", "%v", &b)
Print(b, err, n) // true <nil> 1
var b bool
n, err := Sscanf("tr", "%v", &b)
Print(b, err, n) // false syntax error scanning boolean 0
如果触发了错误,会进入恐慌,然后由错误处理函数恢复恐慌并返回错误信息,只有触发错误之前的变量被赋值。示例如下:
var b, c, d bool = true, true, true
n, err := Sscanf("farffalse", "%v%v%v", &b, &c, &d)
Print(b, err, c, d, n) // true syntax error scanning boolean true true 0
var b, c bool
n, err := Sscanf("ttrd", "%v%v", &b, &c)
Print(b, err, c, n) // true syntax error scanning boolean false 1
需要注意一点是,调用 accept 时,读取的字符如果在 ok 字符串里,则将其写入缓存,如果不在,则会退回该字符。
scanComplex
scanComplex 会调用 complexTokens 获取复数令牌,而 complexTokens 又会调用 floatToken 获取浮点数令牌,所以我们先来看 floatToken 。
floatToken
依次检查 NaN, 正负号, Inf, 数字, (小数点, 数字), (指数符号, 正负号, 数字)。
整数部分可以为十进制或十六进制(以 0x 或 0X 开头),十进制的指数符号包括 eEpP ,而十六进制的指数符号只有 pP,小数部分只能用十进制表示。
数字部分可以用下划线分隔。
返回的值是字符串类型。
complexTokens
依次检查 (, 浮点数(实部), 正负号,浮点数(虚部),i,)
其中,如果没有中间的正负号、虚数单位、右括号,会报错。
最后返回实部和带符号的虚部,都是字符串。
convertFloat
该函数用于将字符串转换为 float64 值。
对于标准的十进制和十六进制计数法格式,可以直接用 strconv.ParseFloat 来转换。但是对于自定义的十进制加以2为底的阶码这种格式,就需要自己实现。
if p := indexRune(str, 'p'); p >= 0 && !hasX(str) {
f, err := strconv.ParseFloat(str[:p], n)
if err != nil {
if e, ok := err.(*strconv.NumError); ok {
e.Num = str
}
s.error(err)
}
m, err := strconv.Atoi(str[p+1:])
if err != nil {
if e, ok := err.(*strconv.NumError); ok {
e.Num = str
}
s.error(err)
}
return math.Ldexp(f, m)
}
实际上就是先把 p 符号之前的十进制数字符串转为浮点数,再把 p 符号之后的二进制数字符串转为整数,最后通过 Ldexp 函数计算 f x 2^m 的值。
scanComplex
func (s *ss) scanComplex(verb rune, n int) complex128 {
if !s.okVerb(verb, floatVerbs, "complex") {
return 0
}
s.SkipSpace()
s.notEOF()
sreal, simag := s.complexTokens()
real := s.convertFloat(sreal, n/2)
imag := s.convertFloat(simag, n/2)
return complex(real, imag)
}
看完上面这些函数后,scanComplex 函数自身就很简单了。实部和虚部分别占总位数的一半,然后可用的格式动词有 beEfFgGv。
scanInt
scanRune
对于格式动词 c,调用 scanRune 来获取一个字符,并返回它的 int64 值。
r := s.getRune()
n := uint(bitSize)
x := (int64(r) << (64 - n)) >> (64 - n) // 如果 r 的位数大于 n,左移操作会使它溢出并丢弃一部分前面的值
if x != int64(r) {
s.errorString("overflow on character value " + string(r))
}
它通过移位操作来判断值是否超过给定的位数。
getBase
在跳过空白符之后,调用 getBase 来返回格式动词对应的基数和包含的数字元素。
可用的格式动词包括 bdoUxXv。
如果动词是 U,必须要吃掉 'U+',否则会报错。
如果动词不是 U,先检查正负号,如果存在,会被保存在令牌缓存中。然后,如果动词是 v(即没有指定基数),扫描前缀并判断基数。
if verb == 'U' {
if !s.consume("U", false) || !s.consume("+", false) {
s.errorString("bad unicode format ")
}
} else {
s.accept(sign) // If there's a sign, it will be left in the token buffer.
if verb == 'v' {
base, digits, haveDigits = s.scanBasePrefix()
}
}
scanNumber
func (s *ss) scanNumber(digits string, haveDigits bool) string {
if !haveDigits {
s.notEOF()
if !s.accept(digits) {
s.errorString("expected integer")
}
}
for s.accept(digits) {
}
return string(s.buf)
}
如果 haveDigits 为 false,即前面的步骤不能确定是否存在与基数对应的数字,要先检查一下。然后循环获取直到停止。
i, err := strconv.ParseInt(tok, base, 64)
if err != nil {
s.error(err)
}
n := uint(bitSize)
x := (i << (64 - n)) >> (64 - n)
if x != i {
s.errorString("integer overflow on token " + tok)
}
return i
最后,通过 strconv.ParseInt 将字符串转换为整数,并用位移操作判断位数是否符合要求。
convertString
字符串的可用格式动词有 svqxX。
s 和 v 会直接返回下一个单词;q 会调用 quotedString;x X 会调用 hexString。
quotedString
先获取第一个字符并判断:
-
如果是反引号,会一直读取到 EOF 或 下一个反引号,结束
-
如果是双引号,读取到下一个双引号结束;其中,对于转义字符
\,会读取立即再读取后一个字符,用来保护反斜杠后的第一个字符;最后会通过strconv.Unquote来取消引号并转义。
hexString
返回空格分隔的十六进制编码字符串。
首先,循环调用 hexByte 来获取下一个十六进制编码的字节,如果出现错误就停止。如果此时缓存长度为 0,则报错;否则将缓存中的字节转换为字符串并返回。
hexByte
func (s *ss) hexByte() (b byte, ok bool) {
rune1 := s.getRune()
if rune1 == eof {
return
}
value1, ok := hexDigit(rune1)
if !ok {
s.UnreadRune()
return
}
value2, ok := hexDigit(s.mustReadRune())
if !ok {
s.errorString("illegal hex digit")
return
}
return byte(value1<<4 | value2), true
}
一个字节包含两个十六进制数, hexDigit 判断字符是否是十六进制数并返回对应的十进制数值:
-
如果遇到 EOF,直接返回
-
如果第一个字节不是十六进制数,会退回字符并返回
-
如果第一个是,但第二个不是,则报错
总结
本篇文章,我们将 scan.go 中所有的具体执行扫描操作的方法函数都介绍完了。下一篇文章,我们会分析完 scan 和 error,给整个 fmt 包做个收尾。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿