当前位置:首页 > 文章列表 > Golang > Go教程 > Go fmt.Scanner 自定义扫描规则的实现要点

Go fmt.Scanner 自定义扫描规则的实现要点

来源:17golang原创 2026-09-29 03:41:50 0浏览 收藏

当 fmt.Sscan、Fscan 或 Sscanf 遇到实现了 fmt.Scanner 的参数时,会把扫描状态和格式动词交给这个类型的 Scan 方法。实现自定义规则的关键不是“从字符串转一次类型”,而是先界定词元、完整校验,再一次性写入指针接收者,避免错误输入留下半更新对象。

官方文档:https://pkg.go.dev/fmt#Scanner

实现要点
  • Scan(state fmt.ScanState, verb rune) error 通常使用指针接收者。
  • 简单的空白分隔值优先用 state.Token,复杂边界才逐个 ReadRune。
  • Token 返回的是共享数据,只能在本次 Scan 调用内使用。
  • 动词、字段数量、字符范围和数值转换都应显式报错。
  • 解析到临时值,全部成功后再覆盖接收者。

触发场景:让 fmt 把文本写入领域类型

一个实用场景是读取版本号。输入可能是 v1.24.3 或 1.24.3,业务希望直接得到三个整数字段,而不是每个调用点都重复执行切分和转换。只要 *SemVer 实现 fmt.Scanner,它就能作为 fmt 扫描函数的目标参数。

type SemVer struct {
	Major int
	Minor int
	Patch int
}

// 编译期断言确保指针类型实现 fmt.Scanner。
var _ fmt.Scanner = (*SemVer)(nil)

必须注意接收者。Scan 要把解析结果保存到对象中,值接收者只会修改副本,所以应写成 func (v *SemVer) Scan(...)。调用时也要传 &version;官方文档说明,被扫描参数必须是基本类型指针,或者实现 Scanner 的值。

Go fmt 扫描入口、Scanner 接口、ScanState 和 SemVer 接收者的浅色工程蓝图静态调用结构
图1:fmt 扫描入口、Scanner 接口与自定义类型接收者的静态调用关系;这是结构图,不是运行截图。

权限配置:只接受约定的扫描动词

verb 来自格式字符串。无格式的 Sscan、Fscan 通常以 %v 语义调用自定义扫描器;Sscanf 则会传入格式中使用的动词。自定义类型不需要假装支持所有动词,明确接受 v 和 s,其余返回带上下文的错误,接口行为更可预测。

func (v *SemVer) Scan(state fmt.ScanState, verb rune) error {
	// 只开放能表达文本版本号的动词,其他格式直接拒绝。
	if verb != 'v' && verb != 's' {
		return fmt.Errorf("SemVer: 不支持扫描动词 %%%c", verb)
	}

	// 后续阶段负责读取词元、校验三段版本并提交结果。
	return v.scanToken(state)
}

这种动词检查相当于扫描规则的入口权限。它能防止调用方写出 %d 之类看似有效、实际语义不清的代码。错误应该返回给 fmt 扫描函数,由上层决定记录、重试还是拒绝输入,不要在 Scanner 内部打印或吞掉。

流水线阶段:Token、解析与提交

ScanState.Token 可以先跳过空白,再读取连续满足谓词的 Unicode 字符。版本号只允许数字、点和可选的 v 前缀,因此谓词保持很小。Token 遇到第一个不满足条件的字符就停止,适合这种单个、空白分隔的领域值。

func (v *SemVer) scanToken(state fmt.ScanState) error {
	// 词元只允许数字、点和可选前缀 v,避免把标点吞进版本号。
	token, err := state.Token(true, func(r rune) bool {
		return unicode.IsDigit(r) || r == '.' || r == 'v'
	})
	if err != nil {
		return fmt.Errorf("SemVer: 读取词元失败: %w", err)
	}

	// Token 的字节切片是共享数据;这里立刻转成字符串并在返回前完成解析。
	text := strings.TrimPrefix(string(token), "v")
	parts := strings.Split(text, ".")
	if len(parts) != 3 {
		return fmt.Errorf("SemVer: 需要 major.minor.patch,收到 %q", text)
	}

	var values [3]int
	for i, part := range parts {
		if part == "" {
			return fmt.Errorf("SemVer: 第 %d 段为空", i+1)
		}
		n, err := strconv.Atoi(part)
		if err != nil || n 

官方对 Token 的生命周期有特别说明:返回切片指向共享数据,下一次 Token 调用、使用同一 ScanState 的扫描调用,或者当前 Scan 方法返回,都可能覆盖它。因此不要把 token 保存到结构体;需要长期持有原文时,应在方法返回前复制。

门禁规则:先完整校验,再写入接收者

我更推荐“解析到临时对象”的做法。假设原对象是 1.2.3,新输入 2.x.0 在第二段失败;如果边解析边写字段,接收者可能变成 2.2.3 这种输入里从未存在过的混合状态。临时数组和 next 把提交点推迟到所有检查通过之后。

Go fmt.Scanner 中 Token、字符谓词、字段转换、error 和 SemVer 接收者的浅色工程蓝图依赖关系
图2:自定义扫描器的解析门禁关系,错误返回与最终赋值分离;这是静态说明图。

门禁不必过度扩张。SemVer 示例只保证三段非负整数,并没有实现完整语义化版本规范中的预发布标识和构建元数据。如果业务需要完整规范,应该扩展明确的领域解析器,再让 Scan 调用它,而不是在一个越来越长的谓词里塞进所有语法。

调用阶段:Sscan、Sscanf 和 Fscan 共用规则

实现接口后,同一规则可以接入字符串或 Reader。Sscan 适合空白分隔输入;Sscanf 可以匹配固定字面量;Fscan 则从指定 io.Reader 读取。三者最终都把领域值交给同一个 Scan 方法。

func parseExamples() error {
	var a, b, c SemVer

	// 无格式扫描使用默认动词语义。
	if _, err := fmt.Sscan("v1.24.3", &a); err != nil {
		return err
	}

	// 固定前缀由格式字符串匹配,%s 交给 SemVer.Scan。
	if _, err := fmt.Sscanf("release=v2.1.0", "release=%s", &b); err != nil {
		return err
	}

	// Reader 场景复用同一自定义扫描规则。
	reader := strings.NewReader("3.7.9")
	if _, err := fmt.Fscan(reader, &c); err != nil {
		return err
	}
	return nil
}

如果输入值之间没有空白,连续调用 Fscan 还要留意读取器能力。fmt 文档提醒,扫描函数可能多读一个 rune;Reader 同时实现 ReadRune 和 UnreadRune 时可以保存这个字符,bufio.NewReader 能为普通 Reader 补上这组能力。对有明确分隔符的复杂协议,专用解析器通常更稳。

失败处理:让错误保留字段上下文

自定义 Scanner 的错误会沿着 Sscan、Fscan 或 Sscanf 返回。错误文本至少应说明目标类型、失败阶段和原始片段,例如“SemVer 第 2 段不是非负整数”。这样上层不必猜是词元为空、动词不支持还是转换失败。

func TestSemVerScan(t *testing.T) {
	tests := []struct {
		input   string
		want    SemVer
		wantErr bool
	}{
		// 正常值覆盖有前缀和无前缀两种入口。
		{input: "v1.2.3", want: SemVer{1, 2, 3}},
		{input: "10.20.30", want: SemVer{10, 20, 30}},
		// 异常值覆盖缺段、空段和非数字字段。
		{input: "1.2", wantErr: true},
		{input: "1..3", wantErr: true},
		{input: "1.x.3", wantErr: true},
	}

	for _, tt := range tests {
		var got SemVer
		_, err := fmt.Sscan(tt.input, &got)
		if (err != nil) != tt.wantErr {
			t.Fatalf("input=%q err=%v", tt.input, err)
		}
		if err == nil && got != tt.want {
			t.Fatalf("input=%q got=%+v want=%+v", tt.input, got, tt.want)
		}
	}
}

什么时候不要用 fmt.Scanner

fmt.Scanner 最适合已有 fmt 扫描链路、输入是短小文本词元、并且领域类型需要统一解析规则的场景。如果要处理长记录、转义、嵌套语法、可恢复错误或精确位置,独立解析函数、bufio.Scanner 配合 SplitFunc,甚至专用解析器会更清楚。不要因为接口方便,就把一套协议语法全部压进单个 Scan 方法。

相关问题

为什么 Scanner.Scan 要用指针接收者?

扫描结果需要写回调用方对象。值接收者只修改副本,方法即使返回 nil,外部值也不会得到预期更新。

ScanState.Token 返回的 []byte 可以保存吗?

不能直接长期保存。它指向共享数据,后续扫描或 Scan 返回后可能被覆盖;需要保存时应复制。

Token 和 ReadRune 应该选哪个?

空白分隔、字符集合明确的词元优先 Token;需要逐字符处理转义、定界符或回退时再使用 ReadRune 与 UnreadRune。

Sscanf 会保证消耗完整输入吗?

不会。官方文档说明 Sscanf 不一定消耗完整输入,也无法恢复它具体用了多少字符;要求整串严格匹配时应增加外层校验或使用专用解析器。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python tomllib 读取配置并保留类型信息Python tomllib 读取配置并保留类型信息
上一篇
Python tomllib 读取配置并保留类型信息
爱玩机工具箱 WebUI 怎么看?模块界面、目录结构与授权边界说明
下一篇
爱玩机工具箱 WebUI 怎么看?模块界面、目录结构与授权边界说明
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    258次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    302次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    281次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    259次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    68次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码