-1

I would like to print CSV-data to the output with martini. Currently, I have always used r.JSON(200, somestruct) where r is a render.Render from github.com/martini-contrib.

Now I have an slice of structs and I would like to print them as CSV (stringify each field of a single struct and print one struct at one line).

Currently, I do it like this:

r.Data(200, []byte("id,Latitude,Longitude\n"))
for _, packet := range tour.Packets {
    r.Data(200, []byte(strconv.FormatInt(packet.Id, 10)+","+strconv.FormatFloat(packet.Latitude, 'f', 6, 64)+","+strconv.FormatFloat(packet.Longitude, 'f', 6, 64)+"\n"))
}

But I don't like the way I do it for the following reasons:

  • It is downloaded directly and not printed to the screen.
  • I get http: multiple response.WriteHeader calls
  • I would prefer not to make this manually (the struct has much more fields, but all fields are either ìnt64, float64 or time.Time.

How can I implement the CSV export option in a simpler way?

Martin Thoma
  • 124,992
  • 159
  • 614
  • 958

1 Answers1

-1

Use the standard library. There is no general solution without reflection, but you can simplify it.

func handler(rw http.ResponseWriter) {
    rw.Header().Add("Content-Type", "text/csv")
    wr := csv.NewWriter(rw)
    err := wr.Write([]string{"id", "Latitude", "Longitude"})
    if err != nil {
        ...
    }
    for _, packet := range tour.Packets {
        err := wr.Write([]string{
            strconv.FormatInt(packet.Id, 10),
            strconv.FormatFloat(packet.Latitude, 'f', 6, 64),
            strconv.FormatFloat(packet.Longitude, 'f', 6, 64),
        })
        if err != nil {
            ...
        }
    }
}

If you need a general solution for any struct, it will require reflect. See here.

// structToStringSlice takes a struct value and
// creates a string slice of all the values in that struct
func structToStringSlice(i interface{}) []string {
    v := reflect.ValueOf(i)
    n := v.NumField()
    out := make([]string, n)
    for i := 0; i < n; i++ {
        field := v.Field(i)
        switch field.Kind() {
        case reflect.String:
            out[i] = field.String()
        case reflect.Int:
            out[i] = strconv.FormatInt(field.Int(), 10)
        // add cases here to support more field types.
        }
    }
    return out
}

// writeToCSV prints a slice of structs as csv to a writer
func writeToCSV(w io.Writer, i interface{}) {
    wr := csv.NewWriter(w)
    v := reflect.ValueOf(i)

    // Get slice's element type (some unknown struct type)
    typ := v.Type().Elem()
    numFields := typ.NumField()
    fieldSet := make([]string, numFields)
    for i := 0; i < numFields; i++ {
        fieldSet[i] = typ.Field(i).Name
    }
    // Write header row
    wr.Write(fieldSet)

    // Write data rows
    sliceLen := v.Len()
    for i := 0; i < sliceLen; i++ {
        wr.Write(structToStringSlice(v.Index(i).Interface()))
    }
    wr.Flush()
}

so then your example is just:

func handler(rw http.ResponseWriter) {
    ....
    writeToCSV(rw, tour.Packets)
}

The function I've written will only work for int or string fields. You can easily extend this to more types by adding cases to the switch in structToStringSlice. See here for reflect docs on the other Kinds.

Logiraptor
  • 1,496
  • 10
  • 14
  • You should add `wr.Flush()`. But even then, the file is automatically downloaded and the approach is very manual (-> I have to manually add each field of the struct). – Martin Thoma Mar 22 '15 at 10:13
  • Yep. If you want a general solution with no extra work for new fields, you have to use reflect. – Logiraptor Mar 22 '15 at 19:00