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

1,287 阅读3分钟

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

前言

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

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

上一篇文章中,我们分析了 scan.go 中的常量与类型,初步了解了它们的内容和用途。在本篇文章中,我们将开始分析扫描函数的执行过程。

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

github.com/golang/go/t…

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

第一层函数

概览

github.com/golang/go/b…

scan 一共有 3x3=9 种导出扫描函数,它们分别是:

ScanScanlnScanf
SscanSscanlnSscanf
FscanFscanlnFscanf

按前缀分:

  • 无前缀:从标准输入 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
}

首先是创建一个扫描器状态,它同样使用了对象重用机制。

github.com/golang/go/b…

if rs, ok := r.(io.RuneScanner); ok {
        s.rs = rs
} else {
        s.rs = &readRune{reader: r, peekRune: -1}
}

获取到扫描器状态后,检查它是否实现了 io.RuneScanner 接口,它包括 ReadRuneUnreadRune 两个方法。如果没有实现,需要用 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)
}

如果是在递归时使用,只需将其恢复原来的状态。同时,不应缓存过大的结构体。


FscanlnFscan 代码的差别仅在于,是否设置了 nlIsSpace 这个参数

第二层函数

第二层函数只有两种 doScandoScanf。先来看较为简单的 doScan

doScan

github.com/golang/go/b…

首先要通过 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 结尾的函数)要检查 \nEOF,如果没有,并且也不是空白符,就会报错。

第三层函数

github.com/golang/go/b…

经过对 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 的文章中已经详细分析过了,所以这里相似的流程就不再过多叙述了。下一篇文章我们继续分析不同类型所对应的扫描函数,顺便看一下接口的方法们。

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