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
接口。