Search code examples
gostructgo-reflectgo-interface

Programmatically fill golang struct


I have a file with many types of data record which I need to parse into structs.

I'd be grateful to learn of a idiomatic way -- if it exists -- of filling structs by record type. Something like python's namedtuple(*fields) constructor.

package main

import (
    "fmt"
    "strconv"
    "strings"
)

type X interface{}

type HDR struct {
    typer, a string
    b        int
}

type BDY struct {
    typer, c string
    d        int
    e        string
}

var lines string = `HDR~two~5
BDY~four~6~five`

func sn(s string) int {
    i, _ := strconv.Atoi(s)
    return i
}

func main() {
    sl := strings.Split(lines, "\n")
    for _, l := range sl {
        fields := strings.Split(l, "~")
        var r X
        switch fields[0] {
        case "HDR":
            r = HDR{fields[0], fields[1], sn(fields[2])} // 1
        case "BDY":
            r = BDY{fields[0], fields[1], sn(fields[2]), fields[3]} // 2
        }
        fmt.Printf("%T : %v\n", r, r)
    }
}

I'm specifically interested to learn if lines marked // 1 and // 2 can be conveniently replaced by code, perhaps some sort of generic decoder which allows the struct itself to handle type conversion.


Solution

  • Use the reflect package to programmatically set fields.

    A field must be exported to be set by the reflect package. Export the names by uppercasing the first rune in the name:

    type HDR struct {
        Typer, A string
        B        int
    }
    
    type BDY struct {
        Typer, C string
        D        int
        E        string
    }
    

    Create a map of names to the type associated with the name:

    var types = map[string]reflect.Type{
        "HDR": reflect.TypeOf((*HDR)(nil)).Elem(),
        "BDY": reflect.TypeOf((*BDY)(nil)).Elem(),
    }
    

    For each line, create a value of the type using the types map:

    for _, l := range strings.Split(lines, "\n") {
        fields := strings.Split(l, "~")
        t := types[fields[0]]
        v := reflect.New(t).Elem()
        ...
    }
    

    Loop over the fields in the line. Get the field value, convert the string to the kind of the field value and set the field value:

        for i, f := range fields {
            fv := v.Field(i)
            switch fv.Type().Kind() {
            case reflect.String:
                fv.SetString(f)
            case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                n, _ := strconv.ParseInt(f, 10, fv.Type().Bits())
                fv.SetInt(n)
            }
        }
    

    This is a basic outline of the approach. Error handling is notabling missing: the application will panic if the type name is not one of the types mentioned in types; the application ignores the error returned from parsing the integer; the application will panic if there are more fields in the data than the struct; the application does not report an error when it encounters an unsupported field kind; and more.

    Run it on the Go Playground.