携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情
前言
本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。
第一章的主角是 fmt 包,它包括 format print scan errors 这四个部分,我们将按照这个顺序来依次分析。
在上一篇文章中,我们分析了 scan.go 中的常量与类型,初步了解了它们的内容和用途。在本篇文章中,我们将开始分析扫描函数的执行过程。
备注:本系列文章使用的是 go 1.19 源码:
结果注释中的 · 代表一个空格
第一层函数
概览
scan 一共有 3x3=9 种导出扫描函数,它们分别是:
| Scan | Scanln | Scanf |
|---|---|---|
| Sscan | Sscanln | Sscanf |
| Fscan | Fscanln | Fscanf |
按前缀分:
-
无前缀:从标准输入
os.Stdin中读取 -
'S':从字符串中读取
-
'F':从
io.Reader接口中读取
按后缀分:
-
无后缀:'\n' 会被当做空白符处理
-
'ln':遇到 '\n' 会终止,并且在输入的结尾必须要有 '\n' 或 EOF
-
'f':遵循格式参数扫描并存储值,格式参数中的 '\n' 必须与输入匹配,或者也可以用
%c接收掉
第一层函数实际上最终调用的都是 Fscan Fscanln Fscanf。
S 开头的函数会通过 StringReader 将字符串转换为 io.Reader 接口
(*stringReader)(&str)
Fscan
下面以 Fscan 为例,先来看看它的执行流程。
func Fscan(r io.Reader, a ...any) (n int, err error) {
s, old := newScanState(r, true, false)
n, err = s.doScan(a)
s.free(old)
return
}
首先是创建一个扫描器状态,它同样使用了对象重用机制。
if rs, ok := r.(io.RuneScanner); ok {
s.rs = rs
} else {
s.rs = &readRune{reader: r, peekRune: -1}
}
获取到扫描器状态后,检查它是否实现了 io.RuneScanner 接口,它包括 ReadRune 和 UnreadRune 两个方法。如果没有实现,需要用 readRune 结构体来实现。
然后将其他状态参数初始化,注意到默认输入的最大字符数和字符宽度都是 1<<30。
s.limit = hugeWid
s.argLimit = hugeWid
s.maxWid = hugeWid
顺便看一下 ss 的释放:
func (s *ss) free(old ssave) {
if old.validSave {
s.ssave = old
return
}
if cap(s.buf) > 1024 {
return
}
s.buf = s.buf[:0]
s.rs = nil
ssFree.Put(s)
}
如果是在递归时使用,只需将其恢复原来的状态。同时,不应缓存过大的结构体。
Fscanln 和 Fscan 代码的差别仅在于,是否设置了 nlIsSpace 这个参数
第二层函数
第二层函数只有两种 doScan 和 doScanf。先来看较为简单的 doScan。
doScan
首先要通过 defer 提前调用好错误处理函数。
func errorHandler(errp *error) {
if e := recover(); e != nil {
if se, ok := e.(scanError); ok {
*errp = se.err
} else if eof, ok := e.(error); ok && eof == io.EOF {
*errp = eof
} else {
panic(e)
}
}
}
它会处理文件内自定义的错误以及 io.EOF 错误,其他的错误会保持恐慌。
for _, arg := range a {
s.scanOne('v', arg)
numProcessed++
}
然后以格式动词 v 调用 scanOne 去扫描下一个值。
if s.nlIsEnd {
for {
r := s.getRune()
if r == '\n' || r == eof {
break
}
if !isSpace(r) {
s.errorString("expected newline")
break
}
}
}
在参数都已经存储了值后,(以 ln 结尾的函数)要检查 \n 或 EOF,如果没有,并且也不是空白符,就会报错。
第三层函数
经过对 print 的分析后,我们现在已经驾轻就熟了,scanOnce 的作用是分类调用。
首先,尝试调用参数自己的 Scan 方法。
如果没有,则对参数进行类型选择:
对于基本类型直接调用对应的处理函数。
Float 类型则略有些棘手,它需要在结果精度内扫描,而不是从高精度扫描然后转换,这样可以保持正确的错误条件。
case *float32:
if s.okVerb(verb, floatVerbs, "float32") {
s.SkipSpace()
s.notEOF()
*v = float32(s.convertFloat(s.floatToken(), 32))
}
其它的类型使用反射处理。
总结
本篇文章我们浏览了前三层函数中的一部分,因为在之前的 fmt & print 的文章中已经详细分析过了,所以这里相似的流程就不再过多叙述了。下一篇文章我们继续分析不同类型所对应的扫描函数,顺便看一下接口的方法们。
最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿