rorycl
rorycl

Reputation: 1384

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.

Upvotes: 0

Views: 3160

Answers (1)

Thundercat
Thundercat

Reputation: 120941

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.

Upvotes: 3

Related Questions