text/scanner 包详解
概述
text/scanner 包为 UTF-8 编码的文本提供了扫描器和分词器功能。它接受一个提供源代码的 io.Reader,然后通过重复调用 Scan 函数来对其进行分词。
主要用途:
- 文本词法分析
- 源代码解析
- 配置文件解析
- 自定义语言解释器
- 文本处理和转换
核心特性:
- 支持 UTF-8 编码文本
- 自动跳过空白字符和 Go 风格注释
- 识别 Go 语言定义的所有字面量
- 可自定义识别的标识符和空白字符
- 支持错误处理回调
- 提供位置跟踪功能
重要说明:
- 不允许使用 NUL 字符(与现有工具兼容)
- 如果源中的第一个字符是 UTF-8 BOM,它会被丢弃
- 默认行为符合 Go 语言规范
包导入
import "text/scanner"
常量详解
扫描模式常量
const (
ScanIdents = 1 << -Ident // 识别标识符
ScanInts = 1 << -Int // 识别整数
ScanFloats = 1 << -Float // 识别浮点数
ScanChars = 1 << -Char // 识别字符字面量
ScanStrings = 1 << -String // 识别字符串字面量
ScanRawStrings = 1 << -RawString // 识别原始字符串字面量
ScanComments = 1 << -Comment // 识别注释
// 跳过注释(与 ScanComments 一起使用)
SkipComments = 1 << -SkipComment
// GoTokens:接受所有 Go 字面量标记,包括 Go 标识符
// 注释将被跳过
GoTokens = ScanIdents | ScanFloats | ScanChars |
ScanStrings | ScanRawStrings | ScanComments | SkipComments
)
说明:
- 预定义的模式位用于控制标记的识别
- 例如,要配置 Scanner 使其只识别(Go)标识符、整数,并跳过注释,将 Scanner 的 Mode 字段设置为:
ScanIdents | ScanInts | ScanComments | SkipComments - 除了注释(如果设置了 SkipComments 则会被跳过)之外,无法识别的标记不会被忽略
- 相反,扫描器简单地返回各个单独的字符(或可能是子标记)
- 例如,如果模式是 ScanIdents(不是 ScanStrings),字符串 “foo” 会被扫描为标记序列
'"' Ident '"'
示例:
var s scanner.Scanner
s.Init(reader)
// 只识别标识符和整数
s.Mode = scanner.ScanIdents | scanner.ScanInts
// 使用 GoTokens 识别所有 Go 标记
s.Mode = scanner.GoTokens
标记类型常量
const (
EOF = -(iota + 1) // 文件结束标记
Ident // 标识符
Int // 整数
Float // 浮点数
Char // 字符字面量
String // 字符串字面量
RawString // 原始字符串字面量
)
说明:
- Scan 的结果是这些标记之一或一个 Unicode 字符
- 负值用于避免与有效的 Unicode 码点冲突
示例:
tok := s.Scan()
switch tok {
case scanner.EOF:
fmt.Println("End of input")
case scanner.Ident:
fmt.Println("Identifier:", s.TokenText())
case scanner.Int:
fmt.Println("Integer:", s.TokenText())
case scanner.Float:
fmt.Println("Float:", s.TokenText())
case scanner.String:
fmt.Println("String:", s.TokenText())
}
空白字符常量
const GoWhitespace = 1<<'\t' | 1<<'\n' | 1<<'\r' | 1<<' '
作用:Scanner 的 Whitespace 字段的默认值
说明:
- 其值选择 Go 的空白字符
- 可以使用位掩码自定义空白字符
示例:
var s scanner.Scanner
s.Init(reader)
// 使用默认的 Go 空白字符
s.Whitespace = scanner.GoWhitespace
// 自定义空白字符(例如,将制表符视为标识符的一部分)
s.Whitespace = 1<<'\n' | 1<<'\r' | 1<<' ' // 排除制表符
函数详解
T
TokenString
func TokenString(tok rune) string
作用:返回标记或 Unicode 字符的可打印字符串表示
参数说明:
tok:标记或 Unicode 字符
返回值:
- 可读的字符串表示
示例:
var s scanner.Scanner
s.Init(strings.NewReader("42"))
tok := s.Scan()
fmt.Printf("Token: %s\n", scanner.TokenString(tok))
// 输出:Token: Int
tok = s.Scan()
fmt.Printf("Token: %s\n", scanner.TokenString(tok))
// 输出:Token: EOF
类型详解(按 A-Z 分层归类)
P
Position
type Position struct {
Filename string // 文件名
Offset int // 偏移量(从 0 开始)
Line int // 行号(从 1 开始)
Column int // 列号(从 1 开始)
}
作用:表示源代码位置的值
说明:
- 如果 Line > 0,则位置有效
- 用于跟踪扫描过程中的位置信息
示例:
var s scanner.Scanner
s.Init(strings.NewReader("hello"))
s.Filename = "test.txt"
tok := s.Scan()
pos := s.Pos()
fmt.Printf("At %s:%d:%d\n", pos.Filename, pos.Line, pos.Column)
// 输出:At test.txt:1:6
Position 方法
IsValid
func (pos *Position) IsValid() bool
作用:报告位置是否有效
返回值:
- 如果 Line > 0 返回 true,否则返回 false
示例:
var pos scanner.Position
if !pos.IsValid() {
fmt.Println("Invalid position")
}
// 扫描后获取有效位置
var s scanner.Scanner
s.Init(strings.NewReader("text"))
s.Scan()
pos = s.Pos()
if pos.IsValid() {
fmt.Printf("Valid position: %s\n", pos.String())
}
String
func (pos Position) String() string
作用:返回位置的可打印字符串表示
返回值:
- 格式为 “filename:line:column” 的字符串
示例:
var s scanner.Scanner
s.Init(strings.NewReader("hello world"))
s.Filename = "example.txt"
s.Scan()
pos := s.Pos()
fmt.Println(pos.String())
// 输出:example.txt:1:6
s.Scan()
pos = s.Pos()
fmt.Println(pos.String())
// 输出:example.txt:1:12
S
Scanner
type Scanner struct {
// 包含导出或未导出的字段
// 配置字段
Filename string // 文件名(用于错误消息和位置)
Mode uint // 扫描模式控制
Whitespace uint // 空白字符位掩码
IsIdentRune func(ch rune, i int) bool // 自定义标识符字符判断
// 状态字段
Error func(*Scanner, string) // 错误处理函数
ErrorCount int // 错误计数
}
作用:实现从 io.Reader 读取 Unicode 字符和标记的功能
字段说明:
Filename:文件名,用于错误消息和 PositionMode:控制识别哪些标记的模式位Whitespace:空白字符的位掩码IsIdentRune:自定义函数,用于判断字符是否是标识符的一部分Error:错误处理函数,如果为 nil 则打印到 os.StderrErrorCount:错误计数
示例:
var s scanner.Scanner
s.Init(reader)
s.Filename = "myfile.txt"
s.Mode = scanner.ScanIdents | scanner.ScanInts
s.Error = func(s *scanner.Scanner, msg string) {
log.Printf("Error at %s: %s", s.Position, msg)
}
Scanner 方法详解(按 A-Z 分层归类)
I
Init
func (s *Scanner) Init(src io.Reader) *Scanner
作用:用新源初始化 Scanner 并返回 s
参数说明:
src:输入源
返回值:
- 初始化后的 Scanner(支持链式调用)
初始化效果:
Scanner.Error设置为 nilScanner.ErrorCount设置为 0Scanner.Mode设置为GoTokensScanner.Whitespace设置为GoWhitespace
示例:
// 基本用法
var s scanner.Scanner
s.Init(strings.NewReader("hello world"))
// 链式调用
s := new(scanner.Scanner).Init(reader)
// 从文件读取
file, _ := os.Open("input.txt")
defer file.Close()
var s scanner.Scanner
s.Init(file)
N
Next
func (s *Scanner) Next() rune
作用:读取并返回下一个 Unicode 字符
返回值:
- 下一个 Unicode 字符
- 在源末尾返回 EOF
说明:
- 通过调用 s.Error 报告读取错误(如果 Error 不为 nil)
- 否则打印错误消息到 os.Stderr
- Next 不更新 Scanner.Position 字段
- 使用 Scanner.Pos() 获取当前位置
示例:
var s scanner.Scanner
s.Init(strings.NewReader("abc"))
for {
ch := s.Next()
if ch == scanner.EOF {
break
}
fmt.Printf("Character: %c\n", ch)
}
// 输出:
// Character: a
// Character: b
// Character: c
P
Peek
func (s *Scanner) Peek() rune
作用:返回源中的下一个 Unicode 字符而不推进扫描器
返回值:
- 下一个 Unicode 字符
- 如果扫描器位置在源的最后一个字符处,返回 EOF
说明:
- 用于前瞻而不消耗字符
- 常用于词法分析中的多字符标记识别
示例:
var s scanner.Scanner
s.Init(strings.NewReader("42"))
// 查看下一个字符
ch := s.Peek()
fmt.Printf("Next char: %c\n", ch) // 输出:4
// 再次查看(仍在同一位置)
ch = s.Peek()
fmt.Printf("Next char: %c\n", ch) // 输出:4
// 实际读取
ch = s.Next()
fmt.Printf("Read char: %c\n", ch) // 输出:4
Pos
func (s *Scanner) Pos() (pos Position)
作用:返回最后一个调用 Scanner.Next 或 Scanner.Scan 返回的字符或标记之后的字符位置
返回值:
- 当前位置
说明:
- 使用 Scanner.Position 字段获取最近扫描的标记的起始位置
示例:
var s scanner.Scanner
s.Init(strings.NewReader("hello world"))
s.Filename = "test.txt"
tok := s.Scan()
startPos := s.Position // 标记的起始位置
endPos := s.Pos() // 标记的结束位置
fmt.Printf("Token '%s' from %s to %s\n",
s.TokenText(), startPos.String(), endPos.String())
S
Scan
func (s *Scanner) Scan() rune
作用:从源读取下一个标记或 Unicode 字符并返回它
返回值:
- 标记或 Unicode 字符
- 在源末尾返回 EOF
说明:
- 只识别 Scanner.Mode 位(1<<-t)设置的标记 t
- 通过调用 s.Error 报告扫描器错误(读取和标记错误)
- 如果 Error 为 nil 则打印错误消息到 os.Stderr
示例:
var s scanner.Scanner
s.Init(strings.NewReader(`name = "John" age = 30`))
s.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanStrings
for {
tok := s.Scan()
if tok == scanner.EOF {
break
}
fmt.Printf("%s: %s\n", scanner.TokenString(tok), s.TokenText())
}
// 输出:
// Ident: name
// =: =
// Ident: age
// =: =
// Int: 30
TokenText
func (s *Scanner) TokenText() string
作用:返回与最近扫描的标记对应的字符串
返回值:
- 标记文本
说明:
- 在调用 Scanner.Scan 后有效
- 在 Scanner.Error 调用中也有效
示例:
var s scanner.Scanner
s.Init(strings.NewReader(`"hello" 42 3.14 'x'`))
s.Mode = scanner.GoTokens
for {
tok := s.Scan()
if tok == scanner.EOF {
break
}
text := s.TokenText()
fmt.Printf("%s: %q\n", scanner.TokenString(tok), text)
}
// 输出:
// String: "\"hello\""
// Int: "42"
// Float: "3.14"
// Char: "'x'"
典型示例
1. 基本扫描
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
var s scanner.Scanner
s.Init(strings.NewReader(`name = "John" age = 30`))
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
fmt.Printf("%s: %s\n", scanner.TokenString(tok), s.TokenText())
}
}
2. 只识别标识符
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
var s scanner.Scanner
s.Init(strings.NewReader("hello world 123"))
s.Mode = scanner.ScanIdents // 只识别标识符
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
if tok == scanner.Ident {
fmt.Printf("Identifier: %s\n", s.TokenText())
} else {
fmt.Printf("Other: %c\n", tok)
}
}
}
3. 位置跟踪
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
src := `line1
line2
line3`
var s scanner.Scanner
s.Init(strings.NewReader(src))
s.Filename = "example.txt"
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
pos := s.Position
fmt.Printf("%s:%d:%d: %s (%s)\n",
pos.Filename, pos.Line, pos.Column,
s.TokenText(), scanner.TokenString(tok))
}
}
4. 错误处理
package main
import (
"fmt"
"log"
"strings"
"text/scanner"
)
func main() {
var s scanner.Scanner
s.Init(strings.NewReader("valid invalid content"))
s.Mode = scanner.ScanIdents
// 自定义错误处理
s.Error = func(s *scanner.Scanner, msg string) {
log.Printf("Error at %s: %s", s.Position, msg)
s.ErrorCount++
}
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
fmt.Printf("Token: %s\n", s.TokenText())
}
fmt.Printf("Total errors: %d\n", s.ErrorCount)
}
5. 扫描注释
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
src := `// This is a comment
/* Multi-line
comment */
code`
var s scanner.Scanner
s.Init(strings.NewReader(src))
// 包含注释
s.Mode = scanner.GoTokens | scanner.ScanComments
s.Whitespace &^= scanner.SkipComments // 不跳过注释
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
if tok == scanner.Comment {
fmt.Printf("Comment: %s\n", s.TokenText())
}
}
}
6. 自定义标识符字符
package main
import (
"fmt"
"strings"
"text/scanner"
"unicode"
)
func main() {
var s scanner.Scanner
s.Init(strings.NewReader("hello-world test_case"))
// 自定义标识符判断:允许连字符
s.IsIdentRune = func(ch rune, i int) bool {
return ch == '-' || ch == '_' || unicode.IsLetter(ch) || unicode.IsDigit(ch)
}
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
fmt.Printf("%s: %s\n", scanner.TokenString(tok), s.TokenText())
}
// 输出:
// Ident: hello-world
// Ident: test_case
}
7. 扫描数字字面量
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
src := `42 3.14 1e10 0xFF 0b1010`
var s scanner.Scanner
s.Init(strings.NewReader(src))
s.Mode = scanner.ScanInts | scanner.ScanFloats
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
fmt.Printf("%s: %s\n", scanner.TokenString(tok), s.TokenText())
}
// 输出:
// Int: 42
// Float: 3.14
// Float: 1e10
// Int: 0xFF
// Int: 0b1010
}
8. 扫描字符串字面量
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
src := `"hello" 'x' \`raw string\``
var s scanner.Scanner
s.Init(strings.NewReader(src))
s.Mode = scanner.ScanStrings | scanner.ScanChars | scanner.ScanRawStrings
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
fmt.Printf("%s: %s\n", scanner.TokenString(tok), s.TokenText())
}
// 输出:
// String: "hello"
// Char: 'x'
// RawString: `raw string`
}
9. 使用 Peek 进行前瞻
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
var s scanner.Scanner
s.Init(strings.NewReader(":= : = ::"))
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
text := s.TokenText()
next := s.Peek()
if next != scanner.EOF {
fmt.Printf("Current: %q, Next: %c\n", text, next)
} else {
fmt.Printf("Current: %q, Next: EOF\n", text)
}
}
}
10. 扫描多行文本
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
src := `line 1
line 2
line 3`
var s scanner.Scanner
s.Init(strings.NewReader(src))
s.Filename = "multiline.txt"
lastLine := 0
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
pos := s.Position
if pos.Line != lastLine {
fmt.Printf("\nLine %d: ", pos.Line)
lastLine = pos.Line
}
fmt.Printf("%s ", s.TokenText())
}
fmt.Println()
}
11. 跳过特定空白
package main
import (
"fmt"
"strings"
"text/scanner"
)
func main() {
var s scanner.Scanner
s.Init(strings.NewReader("a b\tc\nd"))
// 只跳过空格和换行,保留制表符
s.Whitespace = 1<<' ' | 1<<'\n'
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
fmt.Printf("%q ", s.TokenText())
}
// 输出:"a" "b" "\t" "c" "d"
}
12. 解析简单表达式
package main
import (
"fmt"
"strconv"
"strings"
"text/scanner"
)
func main() {
src := "10 + 20 * 3"
var s scanner.Scanner
s.Init(strings.NewReader(src))
s.Mode = scanner.ScanInts
// 简单解析:读取第一个数字
tok := s.Scan()
if tok == scanner.Int {
value, _ := strconv.Atoi(s.TokenText())
fmt.Printf("First number: %d\n", value)
}
// 读取操作符
tok = s.Scan()
fmt.Printf("Operator: %s\n", s.TokenText())
// 读取第二个数字
tok = s.Scan()
if tok == scanner.Int {
value, _ := strconv.Atoi(s.TokenText())
fmt.Printf("Second number: %d\n", value)
}
}
最佳实践
1. 正确初始化
// 推荐的做法
var s scanner.Scanner
s.Init(reader)
// 或链式调用
s := new(scanner.Scanner).Init(reader)
2. 设置合适的模式
// 只扫描标识符
s.Mode = scanner.ScanIdents
// 扫描所有 Go 标记
s.Mode = scanner.GoTokens
// 包含注释
s.Mode = scanner.GoTokens | scanner.ScanComments
s.Whitespace &^= scanner.SkipComments
3. 使用位置信息
s.Filename = "input.txt"
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
pos := s.Position
fmt.Printf("%s:%d:%d: %s\n",
pos.Filename, pos.Line, pos.Column, s.TokenText())
}
4. 自定义错误处理
s.Error = func(s *scanner.Scanner, msg string) {
fmt.Fprintf(os.Stderr, "Error at %s: %s\n", s.Position, msg)
s.ErrorCount++
}
5. 使用 TokenText
// 在 Scan 后立即调用 TokenText
tok := s.Scan()
if tok != scanner.EOF {
text := s.TokenText()
// 处理 text
}
6. 使用 Peek 进行前瞻
tok := s.Scan()
next := s.Peek()
// 根据下一个字符决定如何处理
if next == '=' {
// 处理复合操作符
s.Next() // 消耗下一个字符
}
与其他包配合
io 包
import (
"io"
"text/scanner"
)
func ScanFromReader(r io.Reader) {
var s scanner.Scanner
s.Init(r)
// 扫描...
}
strings 包
import (
"strings"
"text/scanner"
)
func ScanString(src string) {
var s scanner.Scanner
s.Init(strings.NewReader(src))
// 扫描...
}
os 包
import (
"os"
"text/scanner"
)
func ScanFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
var s scanner.Scanner
s.Init(file)
s.Filename = filename
// 扫描...
return nil
}
bufio 包
import (
"bufio"
"text/scanner"
)
func ScanBuffered(r io.Reader) {
reader := bufio.NewReader(r)
var s scanner.Scanner
s.Init(reader)
// 扫描...
}
注意事项
1. NUL 字符限制
// NUL 字符不被允许
src := "text\x00more" // 会导致错误
2. BOM 处理
// UTF-8 BOM 会被自动丢弃
src := "\xEF\xBB\xBFhello" // BOM 被忽略,从 "hello" 开始扫描
3. 模式位设置
// 正确:使用位或运算
s.Mode = scanner.ScanIdents | scanner.ScanInts
// 错误:直接赋值会丢失其他位
s.Mode = scanner.ScanIdents // 只识别标识符
4. 注释处理
// 默认跳过注释
s.Mode = scanner.GoTokens // 注释被跳过
// 要识别注释
s.Mode = scanner.GoTokens | scanner.ScanComments
s.Whitespace &^= scanner.SkipComments // 不跳过注释
5. 错误处理
// 设置错误处理函数
s.Error = func(s *scanner.Scanner, msg string) {
// 处理错误
}
// 检查错误计数
if s.ErrorCount > 0 {
// 有错误发生
}
6. TokenText 的有效性
// TokenText 只在 Scan 后有效
tok := s.Scan()
text := s.TokenText() // 正确
// 在 Next 或 Peek 后可能无效
ch := s.Next()
text := s.TokenText() // 可能不是预期的结果
7. 位置跟踪
// Position 是标记的起始位置
tok := s.Scan()
startPos := s.Position // 标记起始
// Pos() 返回当前位置(标记后)
endPos := s.Pos() // 标记结束
快速参考
常量速查表
| 常量 | 说明 |
|---|---|
EOF | 文件结束标记 |
Ident | 标识符 |
Int | 整数 |
Float | 浮点数 |
Char | 字符字面量 |
String | 字符串字面量 |
RawString | 原始字符串 |
Comment | 注释 |
GoTokens | 所有 Go 标记 |
GoWhitespace | Go 空白字符 |
模式位速查表
| 模式位 | 说明 |
|---|---|
ScanIdents | 识别标识符 |
ScanInts | 识别整数 |
ScanFloats | 识别浮点数 |
ScanChars | 识别字符 |
ScanStrings | 识别字符串 |
ScanRawStrings | 识别原始字符串 |
ScanComments | 识别注释 |
SkipComments | 跳过注释 |
方法速查表
| 方法 | 说明 |
|---|---|
Init | 初始化扫描器 |
Scan | 扫描下一个标记 |
Next | 读取下一个字符 |
Peek | 查看下一个字符 |
Pos | 获取当前位置 |
TokenText | 获取标记文本 |
Position 字段速查表
| 字段 | 说明 |
|---|---|
Filename | 文件名 |
Offset | 偏移量 |
Line | 行号(从 1 开始) |
Column | 列号(从 1 开始) |
常见模式
// 基本扫描
var s scanner.Scanner
s.Init(reader)
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
// 处理标记
}
// 只扫描标识符
s.Mode = scanner.ScanIdents
// 扫描所有 Go 标记
s.Mode = scanner.GoTokens
// 包含注释
s.Mode = scanner.GoTokens | scanner.ScanComments
s.Whitespace &^= scanner.SkipComments
// 位置跟踪
s.Filename = "input.txt"
pos := s.Position
fmt.Printf("%s:%d:%d", pos.Filename, pos.Line, pos.Column)
// 错误处理
s.Error = func(s *scanner.Scanner, msg string) {
log.Printf("Error: %s", msg)
}
总结
text/scanner 包提供了强大的文本扫描和分词功能:
核心功能:
- UTF-8 文本扫描
- 标记识别和分词
- 位置跟踪
- 错误处理
- 可自定义行为
主要类型:
Scanner:扫描器主体Position:位置信息- 标记类型常量
配置选项:
Mode:控制识别哪些标记Whitespace:定义空白字符IsIdentRune:自定义标识符判断Error:错误处理函数
使用建议:
- 正确初始化 Scanner
- 设置合适的扫描模式
- 使用位置信息进行错误报告
- 实现自定义错误处理
- 使用 Peek 进行前瞻
- 注意 TokenText 的有效时机
典型用法:
var s scanner.Scanner
s.Init(reader)
s.Filename = "input.txt"
s.Mode = scanner.ScanIdents | scanner.ScanInts
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
pos := s.Position
fmt.Printf("%s:%d:%d: %s (%s)\n",
pos.Filename, pos.Line, pos.Column,
s.TokenText(), scanner.TokenString(tok))
}
通过 text/scanner 包,可以方便地实现词法分析器、解析器和各种文本处理工具。