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

270 阅读4分钟

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

前言

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

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

上一篇文章中,我们已经将 print.go 的所有执行逻辑都分析完毕,fmt 的半壁江山已经拿下。本篇文章,我们会开始 scan.go 的分析。

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

github.com/golang/go/t…

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

scan

github.com/golang/go/b…

按照流程,我们还是先来看看 scan.go 中都定义了哪些常量与类型,它们的用途都是什么。

常量与类型

type ScanState interface {
	ReadRune() (r rune, size int, err error)
	UnreadRune() error
	SkipSpace()
	Token(skipSpace bool, f func(rune) bool) (token []byte, err error)
	Width() (wid int, ok bool)
	Read(buf []byte) (n int, err error)
}

在分析 scan.go 的类型时,可以对照着 print.go 理解。首先是 ScanState 接口,它代表传递给自定义扫描器的扫描器状态,扫描器在扫描时可能会询问它去寻找下一个空格分隔的令牌。它包括六个方法。

  • ReadRune:用于从输入中读取下一个字符;如果在 -ln 系列函数中调用,函数会在第一个 \n 或读取宽度超过指定宽度时返回 EOF

  • UnreadRune:退回上一个读取的字符,下一次调用 ReadRune 会返回同一个字符

  • SkipSpace:从输入中读取时跳过空白符

  • Token:跳过空白符,然后返回符合 f() 的字节切片;切片所指向的共享数据可能会在下一次调用时被覆盖

  • Width:返回宽度值以及是否设置了宽度

  • Read:仅用来实现 io.Reader 接口,因为有 ReadRune 了,所以 Read 一般永远都不会被使用,在被调用时会返回错误

如果你学过编译原理的话,可能分析到这里时你会感到非常的熟悉,ScanState 非常像一个小型的编译器前端,不过它不需要生成 AST。

type Scanner interface {
	Scan(state ScanState, verb rune) error
}

Scanner 接口可以对应 Formatter 接口理解,用于自定义某个类型的扫描方法。

type stringReader string

该类型有 Read 方法,用于实现 io.Reader 接口。它只在 Sscan 系列函数中使用,为了调用 Fscan 系列函数,所以要将传入的字符串转换为实现 io.Reader 接口的类型。

type scanError struct {
	err error
}

scanError 用作恢复恐慌时区分错误的唯一签名。

type ss struct {
	rs    io.RuneScanner // 读取输入的地方
	buf   buffer         // 令牌累加器
	count int            // 已读取的字符个数
	atEOF bool           // 已经到达 EOF
	ssave
}

ss 是 fmt 包对于 ScanState 的实现。

type ssave struct {
	validSave bool // 是或者曾是 ss 的一部分
	nlIsEnd   bool // 换行符是否终止扫描
	nlIsSpace bool // 换行符是否算作空白符
	argLimit  int  // 参数的 ss.count 的最大值;argLimit <= limit
	limit     int  // ss.count 的最大值
	maxWid    int  // 参数的宽度
}

ssave 存储 ss 中在递归扫描时需要保存和恢复的部分。

type readRune struct {
	reader   io.Reader
	buf      [utf8.UTFMax]byte // 4字节大小的缓存,存储一个字符
	pending  int               // pendBuf 中字节的个数,大于0说明上一次读取时遇到了不正确的字符
	pendBuf  [utf8.UTFMax]byte // 上一次读取时剩余的字节
	peekRune rune              // 大于等于0代表下一个字符,小于0代表上一个字符的取反
}

readRune 从 io.Reader 中读取字符,它会在传递给扫描器的 Reader 没有实现 io.RuneScanner 时被使用。

io.RuneScanner 需要 ReadRune() (r rune, size int, err error)UnreadRune() error 方法。

const eof = -1
var space = [][2]uint16{  // 范围内的字符为空白符,避免依赖 unicode 包
	{0x0009, 0x000d},
	{0x0020, 0x0020},
	{0x0085, 0x0085},
	{0x00a0, 0x00a0},
	{0x1680, 0x1680},
	{0x2000, 0x200a},
	{0x2028, 0x2029},
	{0x202f, 0x202f},
	{0x205f, 0x205f},
	{0x3000, 0x3000},
}
const ( // 定义不同类型的数值,用于读取下一个字符时的检查
	binaryDigits      = "01"
	octalDigits       = "01234567"
	decimalDigits     = "0123456789"
	hexadecimalDigits = "0123456789aAbBcCdDeEfF"
	sign              = "+-"
	period            = "."
	exponent          = "eEpP"
)
const (
	floatVerbs = "beEfFgGv"
	hugeWid = 1 << 30
	intBits     = 32 << (^uint(0) >> 63)
	uintptrBits = 32 << (^uintptr(0) >> 63)
)

后两行的写法,在这篇文章中讲过原理,像 int uint 这样的类型,它的表示位数与系统有关,所以这里需要通过取反运算得到。

总结

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

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