2
votes

Am am attempting to create a file parser that can parse multiple types of data (users, addresses, etc) into a struct. To do this I have created an interface called Datatype:

package main

type Datatype interface {
  name() string
}

And several structs that implement the interface:

ex.

package main

type User struct {
    Username    string `validate:"nonzero"`
    FirstName   string `validate:"nonzero"`
    LastName    string `validate:"nonzero"`
    Email       string `validate:"regexp=^[0-9a-zA-Z]+@[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)+$"`
    Phone       string `validate:"min=10"`
    DateOfBirth string
}

type Users []User

func (u User) name() string {
    return "user"
}

I then read the filename, get the type of data it contains from the name of the file and create an instance of that struct to pass to the parser:

func Parsefile(file string, dtype Datatype) ([]Datatype, error) {
   // Do stuff in here to parse the file

I did this hoping I could create a parse method that took any one of the structs, detect the type and unmarshall from the csv record. However, what I am finding is that I can't do it this was as I can't seem to get the the underlying type from the interface. Or at least not with my Unmarshall function:

func Unmarshal(reader *csv.Reader, v *Datatype) error {
    record, err := reader.Read()
    fmt.Println("Record: ", record)
    if err != nil {
        return err
    }
    s := reflect.ValueOf(v).Elem()
    if s.NumField() != len(record) {
        return &FieldMismatch{s.NumField(), len(record)}
    }
    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        switch f.Type().String() {
        case "string":
            f.SetString(record[i])
        case "int":
            ival, err := strconv.ParseInt(record[i], 10, 0)
            if err != nil {
                return err
            }
            f.SetInt(ival)
        default:
            return &UnsupportedType{f.Type().String()}
        }
    }
    return nil
}

In the above function I get the following error when it hits the line trying to determine the number of fields in the Datatype:

panic: reflect: call of reflect.Value.NumField on interface Value

I know I am going about this poorly and I feel there must be a way to achieve this pattern without having to write logic specific to each data type. However, for the life of my I cannot figure out how to achieve this in go.

1
Does it work if you change the function signature to accept an interface{} instead of *Datatype?captncraig

1 Answers

5
votes

It appears you are trying to use the csv unmarshalling code from this question. That is designed to work when you have a pre-allocated struct of a specific type to pass into it. You are having issues because v is statically an interface type even though the particular value passed in is a struct.

I would recommend putting an Unmarshal method on your interface and on each subType:

func (u *User) populateFrom(reader *csv.Reader) string {
    Unmarshal(reader, u) 
}

Another cool thing in go is the type switch:

var i interface{}
switch t := v.(type) {
    case *User:
       i := t // i is now a *User.
    case *Address:
       i := t // i is now a *Address.
    default:
       panic("unknown type")
}
Unmarshal(reader,i)