Golang fmt.Scanner 接口
最后修改时间 2025 年 5 月 8 日
本教程将介绍如何在 Go 中使用 fmt.Scanner 接口。我们将通过自定义输入扫描的实际示例来涵盖接口基础知识。
fmt.Scanner 接口允许类型定义自己的扫描行为。它被 fmt.Scan 和 fmt.Fscan 等函数使用,以根据自定义规则解析输入。
在 Go 中,fmt.Scanner 需要实现一个 Scan 方法。此方法定义了类型应如何读取和解释输入数据。
基本 Scanner 接口定义
fmt.Scanner 接口简单而强大。它只包含一个必须实现的方法。
注意:为正确实现,方法签名必须完全匹配。
package main
import "fmt"
type Scanner interface {
Scan(state fmt.ScanState, verb rune) error
}
Scan 方法接收一个 fmt.ScanState 和一个动词 rune。如果扫描失败,它将返回一个错误。该方法定义了自定义扫描逻辑。
为自定义类型实现 Scanner
此示例演示了如何为自定义类型实现 fmt.Scanner。我们将创建一个 Coordinate 类型,用于扫描“(x,y)”格式的输入。
package main
import (
"fmt"
"io"
"strconv"
)
type Coordinate struct {
X, Y int
}
func (c *Coordinate) Scan(state fmt.ScanState, verb rune) error {
_, err := fmt.Fscanf(state, "(%d,%d)", &c.X, &c.Y)
return err
}
func main() {
var c Coordinate
_, err := fmt.Sscan("(10,20)", &c)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Scanned coordinate: %+v\n", c)
}
Coordinate 类型实现 Scan 来解析“(x,y)”格式。fmt.Fscanf 有助于从状态进行实际解析。
使用 Scanner 扫描 CSV 数据
我们可以使用 fmt.Scanner 来解析 CSV 格式的输入。此示例展示了一个 CSVRecord 类型,用于扫描逗号分隔的值。
package main
import (
"fmt"
"io"
"strings"
)
type CSVRecord struct {
Fields []string
}
func (r *CSVRecord) Scan(state fmt.ScanState, verb rune) error {
data, err := state.Token(true, func(r rune) bool {
return r != '\n' && r != '\r'
})
if err != nil {
return err
}
r.Fields = strings.Split(string(data), ",")
return nil
}
func main() {
var record CSVRecord
fmt.Sscan("John,Doe,42,New York", &record)
fmt.Printf("Scanned record: %v\n", record.Fields)
}
Scan 方法使用 state.Token 读取直到换行符。然后,它按逗号分割输入以创建记录字段。
扫描键值对
此示例演示了使用自定义格式扫描键值对。我们将实现一个 KeyValue 类型,用于解析“key=value”输入。
package main
import (
"fmt"
"strings"
)
type KeyValue struct {
Key string
Value string
}
func (kv *KeyValue) Scan(state fmt.ScanState, verb rune) error {
data, err := state.Token(false, func(r rune) bool {
return r != '='
})
if err != nil {
return err
}
kv.Key = string(data)
// Read the '=' separator
_, _, err = state.ReadRune()
if err != nil {
return err
}
// Read the rest as value
value, err := state.Token(false, nil)
kv.Value = string(value)
return err
}
func main() {
var kv KeyValue
fmt.Sscan("name=John", &kv)
fmt.Printf("Scanned: %+v\n", kv)
}
该方法首先读取到 '=' 用于键,然后读取剩余部分作为值。state.Token 和 ReadRune 提供了精确的控制。
扫描十六进制数
此示例展示了如何使用自定义类型扫描十六进制数。我们将创建一个 HexNumber,用于解析带有“0x”前缀的值。
package main
import (
"fmt"
"strconv"
)
type HexNumber int
func (h *HexNumber) Scan(state fmt.ScanState, verb rune) error {
// Check for 0x prefix
prefix := make([]rune, 2)
for i := 0; i < 2; i++ {
r, _, err := state.ReadRune()
if err != nil {
return err
}
prefix[i] = r
}
if prefix[0] != '0' || (prefix[1] != 'x' && prefix[1] != 'X') {
return fmt.Errorf("missing 0x prefix")
}
// Read the hex digits
digits, err := state.Token(false, nil)
if err != nil {
return err
}
value, err := strconv.ParseInt(string(digits), 16, 64)
if err != nil {
return err
}
*h = HexNumber(value)
return nil
}
func main() {
var num HexNumber
fmt.Sscan("0xFF", &num)
fmt.Printf("Scanned hex: %d (0x%X)\n", num, num)
}
Scan 方法在解析十六进制数字之前会验证“0x”前缀。strconv.ParseInt 将字符串转换为整数。
扫描自定义日期格式
此示例实现了一个 Date 类型,用于扫描“YYYY-MM-DD”格式的日期。它展示了更复杂的解析逻辑。
package main
import (
"fmt"
"strconv"
"time"
)
type Date struct {
time.Time
}
func (d *Date) Scan(state fmt.ScanState, verb rune) error {
// Read exactly 10 characters (YYYY-MM-DD)
data := make([]rune, 10)
for i := 0; i < 10; i++ {
r, _, err := state.ReadRune()
if err != nil {
return err
}
data[i] = r
}
// Parse the components
year, err := strconv.Atoi(string(data[0:4]))
if err != nil {
return err
}
month, err := strconv.Atoi(string(data[5:7]))
if err != nil {
return err
}
day, err := strconv.Atoi(string(data[8:10]))
if err != nil {
return err
}
// Validate separators
if data[4] != '-' || data[7] != '-' {
return fmt.Errorf("invalid date format")
}
d.Time = time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
return nil
}
func main() {
var date Date
fmt.Sscan("2025-05-08", &date)
fmt.Printf("Scanned date: %s\n", date.Format("January 02, 2006"))
}
该方法读取正好 10 个字符并验证格式。在构造最终日期之前,它会单独解析年、月和日组件。
扫描多个值
此示例展示了如何通过一次 Scan 调用扫描多个值。我们将创建一个 Person 类型,用于同时扫描姓名和年龄。
package main
import (
"fmt"
"strings"
)
type Person struct {
Name string
Age int
}
func (p *Person) Scan(state fmt.ScanState, verb rune) error {
// Read name (until whitespace)
name, err := state.Token(true, func(r rune) bool {
return !(r == ' ' || r == '\t' || r == '\n')
})
if err != nil {
return err
}
p.Name = string(name)
// Skip whitespace
_, _, err = state.ReadRune()
if err != nil {
return err
}
// Read age
ageStr, err := state.Token(false, nil)
if err != nil {
return err
}
// Simple age parsing (in real code, use strconv)
p.Age = len(strings.TrimSpace(string(ageStr)))
return nil
}
func main() {
var person Person
fmt.Sscan("John 42", &person)
fmt.Printf("Scanned person: %+v\n", person)
}
该方法首先读取姓名直到空格,然后跳过空格再读取年龄。这表明了如何在一次调用中处理多值扫描。
来源
本教程通过自定义输入扫描实现的实际示例,涵盖了 Go 中的 fmt.Scanner 接口。