I assumed you āgot data as a mapā and wrote the example assuming userMap to be a map[int]string. Adjust the example accordingly for your actual types.
You can work with data directly. Loop over the elements of data via for ... := range data {. Each element is a map[string]interface{}. From your printout you know the two keys in that map are āuser_idā and āuser_nameā. Print the values for those keys, separated by a tab ā\tā.
for _, entryMap := range data {
fmt.Fprintf(f, "%d\t%v\n", entryMap["user_id"], entryMap["user_name"])
}
OK so stubbing out your data I can get it working just fine with @mjeās supplied example code:
package main
import (
"fmt"
"os"
"time"
)
func main() {
SendMail()
}
// Simulate db call
func getall(sql string) []map[string]interface{} {
return []map[string]interface{}{
{"user_id": 3, "user_name": "John Doe"},
{"user_id": 23, "user_name": "Donald Duck"},
{"user_id": 24, "user_name": "Jane Doe"},
}
}
// SendMail with some of the superfluous lines removed
func SendMail() {
data := getall("SELECT * FROM usr")
t := time.Now()
fileName := "test_" + t.Format("2006-01-02T15-04-05.000Z") + ".tsv"
f, err := os.Create(fileName)
if err != nil {
fmt.Println(err)
}
defer f.Close()
// Print headers
fmt.Fprint(f, "user_id\tuser_name\n")
// Print each record
for _, entryMap := range data {
fmt.Fprintf(f, "%d\t%v\n", entryMap["user_id"], entryMap["user_name"])
}
}
I got a file with the following:
user_id user_name
3 John Doe
23 Donald Duck
24 Jane Doe
What problem are you running into? Also worth noting that you can use encoding/csv to write TSV as well. Both reader and writer support changing the comma rune. Itās just set to ā,ā by default:
// Comma is the field delimiter.
// It is set to comma (',') by NewReader.
// Comma must be a valid rune and must not be \r, \n,
// or the Unicode replacement character (0xFFFD).
Comma rune
Of course this will also be more robust for quoting when the values contain the separator character and for proper handling of the quote character contained within quoted values.
This looks like a flat slice of all values in data. I donāt think thatās what you want.
Furthermore, you canāt count on the iteration over row to be the same order from row-to-row or to match your cols. Itās better to iterate over your keys slice to get consistent ordering. It may or may not be relevant, but you might need to account for missing values for a column in a row.
I would first caution you that []map[string]interface{} is possibly not ideal for the data youāre trying to represent. But, if you wanted a generic TSV function, hereās a naive implementation:
// writeTSV writes tab-separated data to w. It doesn't support jagged maps, so
// all maps must contain the same keys. It also doesn't support escaped tabs so
// it is up to caller to sanitize data.
func writeTSV(w io.Writer, items []map[string]interface{}) {
// Empty data
if len(items) == 0 {
fmt.Fprint(w, "Empty dataset")
return
}
var headers = make([]string, len(items[0]))
i := 0
// Iterate over first item in our array to get headers (AKA keys)
// These will not be in any specific order. See also:
// https://stackoverflow.com/questions/9619479/go-what-determines-the-iteration-order-for-map-keys
for key := range items[0] {
headers[i] = key
i++
}
// Since not in specific order, sort columns to make this predictable.
sort.Strings(headers)
// Print our header
fmt.Fprintln(w, strings.Join(headers, "\t"))
// Iterate over our maps and populate each row with data
for _, row := range items {
for i, column := range headers {
// Print our row value. This is the part that would break
// with jagged maps. TODO: support jagged maps?
fmt.Fprint(w, row[column])
// For the last column, we print newline
if i+1 == len(headers) {
fmt.Fprint(w, "\n")
} else {
// Otherwise, print our delimiter (tab in this case).
fmt.Fprint(w, "\t")
}
}
}
}
And to use it:
func SendMail() {
data := getall("SELECT * FROM usr")
t := time.Now()
fileName := "test_" + t.Format("2006-01-02T15-04-05.000Z") + ".tsv"
f, err := os.Create(fileName)
if err != nil {
fmt.Println(err)
}
defer f.Close()
writeTSV(f, data)
}
Putting it together hereās a go playground link that writes to os.stdout:
I noted that the implementation was naĆÆve and didnāt support escaped characters. The solution in this case is probably to use encoding/csv as I also mentioned above. First, refactor our test data to include problematic test cases:
func getall(sql string) []map[string]interface{} {
return []map[string]interface{}{
{"user_id": 3, "user_name": "John Doe"},
{"user_id": 23, "user_name": "Donald Duck"},
{"user_id": 24, "user_name": "Jane tabbed\t Doe"}, // Test tabs
{"user_id": 33, "user_name": `problematic
name
with
crlf`}, // Test crlf
}
}
Then refactor writeTSV to use encoding/csv:
// writeTSV writes tab-separated data to w. It doesn't support jagged maps, so
// all maps must contain the same keys.
func writeTSV(w io.Writer, items []map[string]interface{}) {
// Empty data
if len(items) == 0 {
fmt.Fprint(w, "Empty dataset")
return
}
// Create a new CSV writer
encoder := csv.NewWriter(w)
// Set our delimiter to tab
encoder.Comma = '\t'
var headers = make([]string, len(items[0]))
i := 0
// Iterate over first item in our array to get headers (AKA keys)
// These will not be in any specific order. See also:
// https://stackoverflow.com/questions/9619479/go-what-determines-the-iteration-order-for-map-keys
for key := range items[0] {
headers[i] = key
i++
}
// Since not in specific order, sort columns to make this predictable.
sort.Strings(headers)
// Print our header
encoder.Write(headers)
// Create array to store row data. We COULD re-use headers here but for
// readability I'm going to create a new variable.
var rowValues = make([]string, len(headers))
// Iterate over our maps and populate each row with data
for _, row := range items {
// For each row, build up our rowValues and then write to our encoder
for i, column := range headers {
// This is the part that would break with jagged maps.
// TODO: support jagged maps?
rowValues[i] = fmt.Sprint(row[column])
}
encoder.Write(rowValues)
}
// Write any buffered data to the underlying writer
encoder.Flush()
}
And putting it all together yields the following properly escaped output:
user_id user_name
3 John Doe
23 Donald Duck
24 "Jane tabbed Doe"
33 "problematic
name
with
crlf"