从源码学习 Go 标准库(一):fmt - scan(4)

897 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情

前言

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

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

上一篇文章中,我们学习了 ScanStatereadRune 接口的方法,这些方法是扫描操作的底层函数,就像 fmt.go 对于 print.go 的关系一样,但是这里把它们都写在一个文件里了。在本篇文章中,我们可以开始愉快地分析具体的扫描操作了。

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

github.com/golang/go/t…

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

scan+type

这些函数都不会区分字母的大小写。

scanBool

github.com/golang/go/b…

先跳过空白符并判断下一个字符不是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;如果 truefalse 拼写不全会触发错误。

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

github.com/golang/go/b…

依次检查 NaN, 正负号, Inf, 数字, (小数点, 数字), (指数符号, 正负号, 数字)。

整数部分可以为十进制或十六进制(以 0x0X 开头),十进制的指数符号包括 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

github.com/golang/go/b…

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)
}

如果 haveDigitsfalse,即前面的步骤不能确定是否存在与基数对应的数字,要先检查一下。然后循环获取直到停止。


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

github.com/golang/go/b…

字符串的可用格式动词有 svqxX

sv 会直接返回下一个单词;q 会调用 quotedStringx X 会调用 hexString

quotedString

github.com/golang/go/b…

先获取第一个字符并判断:

  • 如果是反引号,会一直读取到 EOF 或 下一个反引号,结束

  • 如果是双引号,读取到下一个双引号结束;其中,对于转义字符 \,会读取立即再读取后一个字符,用来保护反斜杠后的第一个字符;最后会通过 strconv.Unquote 来取消引号并转义。

hexString

github.com/golang/go/b…

返回空格分隔的十六进制编码字符串。

首先,循环调用 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 包做个收尾。

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