ZetCode

Golang fmt.Scanner Interface

last modified May 8, 2025

This tutorial explains how to use the fmt.Scanner interface in Go. We'll cover interface basics with practical examples of custom input scanning.

The fmt.Scanner interface allows types to define their own scanning behavior. It's used by functions like fmt.Scan and fmt.Fscan to parse input according to custom rules.

In Go, fmt.Scanner requires implementing a single Scan method. This method defines how the type should read and interpret input data.

Basic Scanner Interface Definition

The fmt.Scanner interface is simple but powerful. It consists of just one method that must be implemented.
Note: The method signature must match exactly for proper implementation.

scanner_interface.go
package main

import "fmt"

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

The Scan method takes a fmt.ScanState and a verb rune. It returns an error if scanning fails. The method defines custom scanning logic.

Implementing Scanner for a Custom Type

This example shows how to implement fmt.Scanner for a custom type. We'll create a Coordinate type that scans "(x,y)" formatted input.

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)
}

The Coordinate type implements Scan to parse "(x,y)" format. fmt.Fscanf helps with the actual parsing from the state.

Scanning CSV Data with Scanner

We can use fmt.Scanner to parse CSV-formatted input. This example shows a CSVRecord type that scans comma-separated values.

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)
}

The Scan method uses state.Token to read until newline. It then splits the input by commas to create the record fields.

Scanning Key-Value Pairs

This example demonstrates scanning key-value pairs with custom formatting. We'll implement a KeyValue type that parses "key=value" input.

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)
}

The method first reads until '=' for the key, then reads the remainder as value. state.Token and ReadRune provide precise control.

Scanning Hexadecimal Numbers

This example shows how to scan hexadecimal numbers with a custom type. We'll create a HexNumber that parses "0x" prefixed values.

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)
}

The Scan method verifies the "0x" prefix before parsing the hex digits. strconv.ParseInt converts the string to an integer.

Scanning Custom Date Format

This example implements a Date type that scans dates in "YYYY-MM-DD" format. It demonstrates more complex parsing logic.

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"))
}

The method reads exactly 10 characters and validates the format. It parses year, month, and day components separately before constructing the final date.

Scanning with Multiple Values

This example shows how to scan multiple values with a single Scan call. We'll create a Person type that scans name and age together.

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)
}

The method first reads the name until whitespace, then skips whitespace before reading the age. This shows how to handle multi-value scanning in one call.

Source

Go fmt.Scanner documentation

This tutorial covered the fmt.Scanner interface in Go with practical examples of custom input scanning implementations.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Golang tutorials.