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

129 阅读4分钟

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

前言

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

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

上一篇文章中,我们浏览了 scan 的前三层函数中的一部分。本篇文章,我们先来看一下 ScanStatereadRune 接口的方法们。

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

github.com/golang/go/t…

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

接口的方法们

在对接口的方法们进行更加详细的学习之前,您可以先复习一下scan 中的常量与类型

在下面的分析中,我们会略过一些简易的函数。

*ss

github.com/golang/go/b…

ReadRune

func (s *ss) ReadRune() (r rune, size int, err error) {
        if s.atEOF || s.count >= s.argLimit {  // 达到 EOF 了,或者字符数超过范围了,返回错误
                err = io.EOF
                return
        }
        r, size, err = s.rs.ReadRune()
        if err == nil {
                s.count++  // 已扫描的字符数加一
                if s.nlIsEnd && r == '\n' {  // 对于 -ln 函数,扫描到 \n 就结束
                        s.atEOF = true
                }
        } else if err == io.EOF {
                s.atEOF = true
        }
        return
}

本函数是对 io.RuneScanner 接口的 ReadRune 方法的封装,并提供了对 EOF 和扫描结束的检查与处理。

本函数还有一些进一步封装的方法,它们只返回扫描到的字符值,并具有一些特性:

  • getRune: EOF 作为值 -1 返回

  • mustReadRune:一般用于在读取固定长度的数据过程中,遇到 EOF 时报错

Token

func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
        defer func() {
                if e := recover(); e != nil {
                        if se, ok := e.(scanError); ok {
                                err = se.err
                        } else {
                                panic(e)
                        }
                }
        }()
        if f == nil {
                f = notSpace
        }
        s.buf = s.buf[:0]
        tok = s.token(skipSpace, f)
        return
}

首先通过 defer 处理错误,如果是自定义处理的错误则恢复,不能处理的错误保持恐慌。

然后,返回下一个令牌,函数 f 决定何时停止,默认为被空白符分隔的下一个字符串。

token

github.com/golang/go/b…

如果 skipSpace 被设为 true,会先跳过空白符。然后,不断获取字符并写入到缓存中,直到:

  • 遇到 EOF,停止

  • 函数 f 返回 false,退回读取的字符并停止

skipSpace

不断获取字符,并依次做以下判断:

  • 如果是 EOF,则返回

  • 如果是 \r,并且下一个字符是 \n(通过 peek 判断,该字符此时没有被获取),则 continue

  • 如果是 \n,并且它被当做空白符,则 continue;否则,报错 s.errorString("unexpected newline")

  • 如果不是空白符,退回读取的字符并返回

\r\n\n 的意义是一样的,但由于 \r 本身是空白符,所以对于 \r\n 要特殊处理(实际上,这种情况下是跳过 \r 处理 \n

peek & indexRune

github.com/golang/go/b…

peek 这个函数在获取字符后,会再把字符退回去,然后判断传入的字符串是否包含该字符

indexRune 遍历字符串并匹配字符,返回字符索引或者-1

consume

func (s *ss) consume(ok string, accept bool) bool

读取下一个字符并判断是否在字符串 ok 中,当 acceptfalse 时,读取的字符不会退回。

accepttrue 时,如果读取的字符在 ok 中,就会把它写入到缓存里;否则,会退回字符。

*readRune

ReadRune

github.com/golang/go/b…

if r.peekRune >= 0 {
        rr = r.peekRune
        r.peekRune = ^r.peekRune
        size = utf8.RuneLen(rr)
        return
}

peekRune 大于等于 0,表示的是下一个字符;小于 0 ,则是上一个字符的取反。

如果下一个字符已经读取了,直接返回相应的参数。

r.buf[0], err = r.readByte()

否则,通过 readByte 去读取。

if r.buf[0] < utf8.RuneSelf {
        rr = rune(r.buf[0])
        size = 1
        r.peekRune = ^rr
        return
}

如果小于 128,说明是 ASCII 字符,直接转换并返回。

var n int
for n = 1; !utf8.FullRune(r.buf[:n]); n++ {
        r.buf[n], err = r.readByte()
        if err != nil {
                if err == io.EOF {
                        err = nil
                        break
                }
                return
        }
}
rr, size = utf8.DecodeRune(r.buf[:n])
if size < n {
        copy(r.pendBuf[r.pending:], r.buf[size:n])
        r.pending += n - size
}
r.peekRune = ^rr
return

如果字符大于一字节,则不断获取字节直至字符完整。如果解码出来的字符宽度小于遍历获取的字节数,说明出现了错误,把多出的部分放入到 pendBuf 中存储。

readByte

github.com/golang/go/b…

如果 pendBuf 中有遗留的字节,先从其中获取。

n, err := io.ReadFull(r.reader, r.pendBuf[:1])

否则,通过 io.ReadFull 来从 reader 中获取一个字节。

UnreadRune

func (r *readRune) UnreadRune() error {
	if r.peekRune >= 0 {
		return errors.New("fmt: scanning called UnreadRune with no rune available")
	}
	r.peekRune = ^r.peekRune
	return nil
}

退回字符操作,实际上直接对 peekRune 取反就可以了。

总结

本篇文章中,我们学习了 ScanStatereadRune 接口的方法,这些方法是扫描操作的底层函数,就像 fmt.go 对于 print.go 的关系一样,但是这里把它们都写在一个文件里了。弄明白字符流是如何形成之后,我们就可以愉快地分析具体的扫描操作了。

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