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