fmt.scanner不会被fmt.scan系列函数调用,仅fmt.fscanf等函数在传入指针且使用%v动词时才会触发;必须实现*type.scan方法,否则无效。

Go 的 fmt.Scanner 接口本身不能直接用于“自定义类型的扫描解析”——它只是个输入接口,真正起作用的是类型实现的 Scan 方法,且必须配合 fmt.Scanf、fmt.Fscanf 等函数使用;单独实现接口没用,也不会被自动调用。
为什么 fmt.Scanner 不会自动触发自定义解析
fmt.Scanner 是一个单方法接口:Scan(state ScanState, verb rune) error。但它**不会被 fmt.Scan 系列函数识别或调用**——这些函数只认 fmt.Stringer(输出)和内置类型逻辑,对输入端完全不检查该接口。
换句话说:哪怕你给结构体实现了 Scan 方法,写 fmt.Scan(&myVal) 也根本不会调用它,而是走默认的空格分隔字符串拆解逻辑(即把输入当普通 token 切分后尝试转基础类型)。
常见错误现象:fmt.Scan(&myStruct) 报 cannot scan type mypkg.MyType 或静默失败(比如只读入第一个字段),就是因为没走自定义逻辑。
- 只有
fmt.Fscan系列(Fscanf、Fscanln)在底层调用scanOne时,才会检查并调用Scan方法——但前提是传入的是*T(指针),且T显式实现了fmt.Scanner -
fmt.Scan和fmt.Scanf**完全忽略**fmt.Scanner接口,它们只处理基本类型、指针、切片、数组等预定义规则 - 所以“自定义扫描”唯一可靠路径是:用
fmt.Fscanf(os.Stdin, ...)或包装io.Reader,并确保传入的是实现了fmt.Scanner的指针
正确实现 Scanner 并让 Fscanf 调用它
以解析形如 "(123,456)" 的坐标为例:
type Point struct {
X, Y int
}
<p>func (p *Point) Scan(state fmt.ScanState, verb rune) error {
// 读左括号
if <em>, err := state.Token(true, func(r rune) bool { return r == '(' }); err != nil {
return err
}
// 读 X(跳过空白,读整数)
token, err := state.Token(true, func(r rune) bool { return r != ',' && r != ')' && !unicode.IsSpace(r) })
if err != nil {
return err
}
if n, </em> := strconv.Atoi(string(token)); err == nil {
p.X = n
} else {
return err
}
// 读逗号
if <em>, err := state.Token(true, func(r rune) bool { return r == ',' }); err != nil {
return err
}
// 读 Y
token, err = state.Token(true, func(r rune) bool { return r != ')' && !unicode.IsSpace(r) })
if err != nil {
return err
}
if n, </em> := strconv.Atoi(string(token)); err == nil {
p.Y = n
} else {
return err
}
// 读右括号
if _, err := state.Token(true, func(r rune) bool { return r == ')' }); err != nil {
return err
}
return nil
}
</p>
关键点:
- 方法接收者必须是
*Point(指针),否则Fscanf检查接口时无法匹配(值类型实现的接口 ≠ 指针类型实现的接口) - 必须用
fmt.Fscanf(os.Stdin, "%v", &p)或fmt.Fscan(os.Stdin, &p)—— 注意是&p,且动词建议用%v(%s或%d会绕过 Scanner) -
state.Token第二个参数是分隔符判断函数:返回true表示“保留该字符”,false表示“遇到就停”;上面例子中r != ','表示“只要不是逗号就继续收”,从而拿到完整数字字符串
替代方案:更可控、更常用的做法
绝大多数场景下,硬啃 fmt.Scanner 得不偿失——ScanState.Token 行为隐蔽、文档少、调试困难,且仅限 Fscanf 系列可用。
更自然的选择是:自己读字符串,再解析。
- 用
bufio.Scanner或fmt.Scanln读整行 → 得到string - 用正则(
regexp)、strings或strconv手动解析 → 控制力强、易测试、报错明确 - 如果要复用
fmt风格的格式化,可封装成方法,例如p.UnmarshalText([]byte(s)) error,再配合encoding.TextUnmarshaler - 性能敏感时,避免
state.Token多次拷贝;直接用state.ReadRune逐字符推进更高效(但代码更长)
真正容易被忽略的是:即使你实现了 fmt.Scanner,只要没用 Fscanf + 指针 + %v,它就等于没写。而 Fscanf 在交互式输入中不如先读行再解析来得稳定——比如用户多敲个空格或换行,ScanState 的内部缓冲行为就可能出人意料。











