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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Streaming with bufio.Scanner lets you process large inputs line by line without loading everything into memory.
- 2Wrapping errors with line numbers and %w gives callers both context and the original cause for debugging.
- 3Symmetric read/write helpers should agree on a format contract, including a header row and delimiter escaping.
Related explainers
ruby
class QueryParser def self.parse(query_string) new(query_string).parse end
Parsing nested query strings in Ruby
parsing
recursion
tokenization
Intermediate
9 steps
go
package config import ( "fmt"
Loading and saving YAML config in Go
yaml
struct-tags
defaults
Intermediate
8 steps
typescript
type QueuedRequest = { id: string; url: string; method: string;
A retry queue with exponential backoff
retry
exponential-backoff
persistence
Intermediate
10 steps
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
python
from flask import Blueprint, jsonify, request, abort v1 = Blueprint("users_v1", __name__) v2 = Blueprint("users_v2", __name__)
Versioning a Flask API with Blueprints
api versioning
blueprints
rest
Intermediate
7 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/reading-and-writing-tsv-records-in-go-explained-go-bc38/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.