go 63 lines · 10 steps

Reading and writing TSV records in Go

A pair of functions that parse tab-separated rows into typed structs and serialize them back out.

Explained by highlit
1package tsvio
2 
3import (
4 "bufio"
5 "fmt"
6 "io"
7 "strconv"
8 "strings"
9)
10 
11type Record struct {
12 ID int
13 Name string
14 Quantity int
15 Price float64
16}
17 
18func ReadRecords(r io.Reader) ([]Record, error) {
19 scanner := bufio.NewScanner(r)
20 scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
21 
22 var records []Record
23 line := 0
24 for scanner.Scan() {
25 line++
26 text := strings.TrimRight(scanner.Text(), "\r\n")
27 if line == 1 || text == "" {
28 continue
29 }
30 fields := strings.Split(text, "\t")
31 if len(fields) != 4 {
32 return nil, fmt.Errorf("line %d: expected 4 fields, got %d", line, len(fields))
33 }
34 id, err := strconv.Atoi(strings.TrimSpace(fields[0]))
35 if err != nil {
36 return nil, fmt.Errorf("line %d: invalid id: %w", line, err)
37 }
38 qty, err := strconv.Atoi(strings.TrimSpace(fields[2]))
39 if err != nil {
40 return nil, fmt.Errorf("line %d: invalid quantity: %w", line, err)
41 }
42 price, err := strconv.ParseFloat(strings.TrimSpace(fields[3]), 64)
43 if err != nil {
44 return nil, fmt.Errorf("line %d: invalid price: %w", line, err)
45 }
46 records = append(records, Record{ID: id, Name: fields[1], Quantity: qty, Price: price})
47 }
48 return records, scanner.Err()
49}
50 
51func WriteRecords(w io.Writer, records []Record) error {
52 bw := bufio.NewWriter(w)
53 if _, err := fmt.Fprintln(bw, "id\tname\tquantity\tprice"); err != nil {
54 return err
55 }
56 for _, rec := range records {
57 name := strings.ReplaceAll(rec.Name, "\t", " ")
58 if _, err := fmt.Fprintf(bw, "%d\t%s\t%d\t%.2f\n", rec.ID, name, rec.Quantity, rec.Price); err != nil {
59 return err
60 }
61 }
62 return bw.Flush()
63}
01 / 01
STEP 01

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1Streaming with bufio.Scanner lets you process large inputs line by line without loading everything into memory.
  2. 2Wrapping errors with line numbers and %w gives callers both context and the original cause for debugging.
  3. 3Symmetric read/write helpers should agree on a format contract, including a header row and delimiter escaping.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Reading and writing TSV records in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code