ZetCode

Golang fmt.Scanner 接口

最后修改时间 2025 年 5 月 8 日

本教程将介绍如何在 Go 中使用 fmt.Scanner 接口。我们将通过自定义输入扫描的实际示例来涵盖接口基础知识。

fmt.Scanner 接口允许类型定义自己的扫描行为。它被 fmt.Scanfmt.Fscan 等函数使用,以根据自定义规则解析输入。

在 Go 中,fmt.Scanner 需要实现一个 Scan 方法。此方法定义了类型应如何读取和解释输入数据。

基本 Scanner 接口定义

fmt.Scanner 接口简单而强大。它只包含一个必须实现的方法。
注意:为正确实现,方法签名必须完全匹配。

scanner_interface.go
package main

import "fmt"

type Scanner interface {
    Scan(state fmt.ScanState, verb rune) error
}

Scan 方法接收一个 fmt.ScanState 和一个动词 rune。如果扫描失败,它将返回一个错误。该方法定义了自定义扫描逻辑。

为自定义类型实现 Scanner

此示例演示了如何为自定义类型实现 fmt.Scanner。我们将创建一个 Coordinate 类型,用于扫描“(x,y)”格式的输入。

custom_scanner.go
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 类型,用于扫描逗号分隔的值。

csv_scanner.go
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”输入。

keyvalue_scanner.go
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.TokenReadRune 提供了精确的控制。

扫描十六进制数

此示例展示了如何使用自定义类型扫描十六进制数。我们将创建一个 HexNumber,用于解析带有“0x”前缀的值。

hex_scanner.go
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”格式的日期。它展示了更复杂的解析逻辑。

date_scanner.go
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 类型,用于同时扫描姓名和年龄。

multi_value_scanner.go
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 文档

本教程通过自定义输入扫描实现的实际示例,涵盖了 Go 中的 fmt.Scanner 接口。

作者

我的名字是 Jan Bodnar,我是一名充满激情的程序员,拥有丰富的编程经验。我从 2007 年开始撰写编程文章。到目前为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有超过十年的经验。

列出所有 Golang 教程