Essential Go  Edit on GitHub      File Issue

CSV

Package encoding/csv in standard library provides functionality for reading and writing CSV files.

Reading records from CSV file

Let’s read stock quotes from a CSV file:

date,open,high,low,close,volume,Name
2013-02-08,15.07,15.12,14.63,14.75,8407500,AAL
2013-02-11,14.89,15.01,14.26,14.46,8882000,AAL
2013-02-12,14.45,14.51,14.1,14.27,8126000,AAL
2013-02-13,14.3,14.94,14.25,14.66,10259500,AAL
f, err := os.Open("stocks.csv")
if err != nil {
	log.Fatalf("os.Open() failed with '%s'\n", err)
}
defer f.Close()

r := csv.NewReader(f)
var record []string
nRecords := 0
for {
	record, err = r.Read()
	if err != nil {
		if err == io.EOF {
			err = nil
		}
		break
	}
	nRecords++
	if nRecords < 5 {
		fmt.Printf("Record: %#v\n", record)
	}
}
if err != nil {
	log.Fatalf("r.Read() failed with '%s'\n", err)
}
fmt.Printf("Read %d records\n", nRecords)
Record: []string{"date", "open", "high", "low", "close", "volume", "Name"}
Record: []string{"2013-02-08", "15.07", "15.12", "14.63", "14.75", "8407500", "AAL"}
Record: []string{"2013-02-11", "14.89", "15.01", "14.26", "14.46", "8882000", "AAL"}
Record: []string{"2013-02-12", "14.45", "14.51", "14.1", "14.27", "8126000", "AAL"}
Read 25 records

As per Go best practices, CSV reader operates on io.Reader interface, which allows it to work on files, network connections, bytes in memory etc.

Read() method reads one CSV line at a time and returns []string slice with all fields in that line and an error.

Returning io.EOF as an error signifies successfully reaching end of file.

Reading all records from CSV file

Instead of calling Read() in a loop, we could read all records in one call:

r := csv.NewReader(f)
records, err := r.ReadAll()
if err != nil {
    log.Fatalf("r.ReadAll() failed with '%s'\n", err)
}
// records is [][]string
fmt.Printf("Read %d records\n", len(records))

This time we don’t have to special-case io.EOF as ReadAll does that for us.

Reading all records at once is simpler but will use more memory, especially for large CSV files.

Writing records to CSV file

Let’s now write simplified stock quotes to a CSV file:

func writeCSV() error {
	f, err := os.Create("stocks_tmp.csv")
	if err != nil {
		return err
	}

	w := csv.NewWriter(f)
	records := [][]string{
		{"date", "price", "name"},
		{"2013-02-08", "15,07", "GOOG"},
		{"2013-02-09", "15,09", "GOOG"},
	}
	for _, rec := range records {
		err = w.Write(rec)
		if err != nil {
			f.Close()
			return err
		}
	}

	// csv.Writer might buffer writes for performance so we must
	// Flush to ensure all data has been written to underlying
	// writer
	w.Flush()

	// Flush doesn't return an error. If it failed to write, we
	// can get the error with Error()
	err = w.Error()
	if err != nil {
		return err
	}
	// Close might also fail due to flushing out buffered writes
	err = f.Close()
	return err
}
date,price,name
2013-02-08,"15,07",GOOG
2013-02-09,"15,09",GOOG

Error handling here is not trivial.

We need to remember to Flush() at the end of writing, check if Flush() failed with Error() and also check that Close() didn’t fail.

The need to check Close() errors is why we didn’t use a simpler defer f.Close(). Correctness and robustness sometimes require more code.

Nalues that had , in them were quoted because comman is used as field separator.

In production code we would also delete the CSV file in case of errors. No need to keep corrupt file around.

Writing all records to CSV file

Just like we can read all records at once, we can write multiple records at once:

w := csv.NewWriter(f)
err = w.WriteAll(records)
if err != nil {
    f.Close()
    return err
}
  ↑ ↓ to navigate     ↵ to select     Esc to close