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
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 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.