Standard Library in Practice
io and bufio, encoding/json, time, strconv, regexp, embed
- 01
Your handler builds a response struct,
json.Marshalreturns no error, and the client receives{}— the debugger shows every field populated. What is wrong with the struct?Easyencoding/jsonworks through reflection and can only see exported fields, so a struct whose fields start with a lowercase letter marshals to{}and decodes into nothing.// ❌ nothing is exported: Marshal returns "{}" and a nil error type order struct { id int64 total int64 `json:"total"` // a tag on an unexported field does nothing status string } … - 02
A log line comes out as the literal text
YYYY-MM-DD hh:mm:ss, andtime.Parse("DD/MM/YYYY", s)errors on every input. What does Go want in that argument instead?EasyA Go layout is not a pattern of placeholders but an example of the reference time
Mon Jan 2 15:04:05 MST 2006— anything that is not part of that timestamp is copied through literally, which is whyYYYY-MM-DDformats as itself.const layout = "2006-01-02 15:04:05" // an example, not a pattern func main() { t := time.Date(2024, 3, 5, 9, 7, 0, 0, time.UTC) // ❌ Format never errors: unknown letters are copied through as themselves … - 03
An export job writes 200k rows through a
bufio.Writerand the file on disk is a few kilobytes short, always cut mid-row. No call returned an error. What is missing?Easybufio.Writerkeeps the tail of your output in a 4 KB in-memory buffer, so without aFlush— and a check of the error it returns — whatever is still buffered when the function ends never reaches the file.type Row struct { ID int64 Name string } // ❌ the last partial buffer is lost; Close does not flush the writer … - 04
os.ReadFile("templates/welcome.html")works undergo run .and fails withno such file or directoryonce the binary is copied to the server. Why, and what do you ship instead?EasyA relative path is resolved against the process's working directory, not the binary and not the package that named it — locally
go runhappens to start you in the package directory, and systemd or a containerWORKDIRdoes not.//go:embed all:templates var templates embed.FS // compiled into the binary: no cwd, nothing to copy func render(w io.Writer, name string, data any) error { // ❌ resolved against whatever directory the process happens to be started in // b, err := os.ReadFile("templates/" + name) … - 05
You have to upload a file while gzipping it, hashing the plaintext and counting the bytes that actually go out — one pass, nothing held in memory. Why does this end up being a handful of lines of stdlib?
MediumBecause every streaming API in the standard library speaks
io.Readerandio.Writer— one method each — so compression, hashing and counting are wrappers you stack, andio.TeeReader,io.MultiWriterandio.Copydo the plumbing.// One pass over the disk: gzip to the wire, sha256 of the plaintext, // and a count of the compressed bytes. func upload(dst io.Writer, name string) (sum []byte, n int64, err error) { f, err := os.Open(name) if err != nil { return nil, 0, err … - 06
A 12 GB POST to an endpoint whose first line is
body, _ := io.ReadAll(r.Body)takes the whole service down. What do you wrap the reader in, and what does that wrapper not tell you?MediumWrap it in
io.LimitReader(r, n)so the client can no longer choose how much memory you allocate — but a bareLimitReaderreports the cut as a cleanio.EOF, so truncation is invisible unless you read one byte past your ceiling and check.const maxBody = 1 << 20 // 1 MiB // ❌ the caller decides how much memory you allocate func readBad(r io.Reader) ([]byte, error) { return io.ReadAll(r) } … - 07
A line-by-line importer has run for a year and now dies on one customer's file with
bufio.Scanner: token too long— and only because someone recently added anErr()check. What is the limit, and what are the ways out?Mediumbufio.Scannerrefuses any token bigger thanbufio.MaxScanTokenSize, 64 KB:Scan()simply returnsfalse, and the reason shows up only inErr().func importLines(r io.Reader, handle func([]byte) error) error { s := bufio.NewScanner(r) // Default ceiling is bufio.MaxScanTokenSize (64 KB): one long line ends // the loop exactly like a clean EOF. Raise it deliberately. s.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) // 64 KB start, 4 MB max … - 08
The mobile client crashes on
response.tags.lengthfor exactly the users who have no tags, and atime.Timefield you taggedomitemptyis in every payload as0001-01-01T00:00:00Z. Explain both.MediumA nil slice marshals to
nullwhile an allocated empty one marshals to[], andomitemptyonly recognises Go's empty values —false,0,"", a nil pointer, interface or map, and a length-zero map, slice or array — so a zero struct such astime.Timeis never empty.type Profile struct { Tags []string `json:"tags"` // nil -> null, []string{} -> [] Nick string `json:"nick,omitempty"` // "" -> key absent Score int `json:"score,omitempty"` // 0 -> absent: "sent 0" is unsayable Seen *int `json:"seen,omitempty"` // nil -> absent, &0 -> "seen":0 Created time.Time `json:"created,omitempty"` // ❌ a struct is never "empty" … - 09
A webhook relay decodes each payload into
map[string]any, re-encodes it and forwards it on. Orders started arriving downstream with an id one off from the source system's. What happened in between?MediumDecoding into
anyturns every JSON number into afloat64, and afloat64carries 53 bits of mantissa — an id above 2^53 is rounded on the way in, and the re-encode faithfully writes the rounded value out.const raw = `{"order_id":12345678901234567,"amount":"10.50"}` func main() { // ❌ any makes every number a float64: 53 bits of mantissa var m map[string]any json.Unmarshal([]byte(raw), &m) … - 10
Ops renamed a deploy-template key from
max_connstomaxConnections, the service booted without a word and ran with a pool of zero. Which twoencoding/jsondecode rules made that silent, and how do you make it loud?MediumUnknown keys are silently discarded and known ones are matched case-insensitively, so a renamed key vanishes without a trace while a mis-cased one is quietly accepted —
Decoder.DisallowUnknownFields()turns the first case into an error.type Config struct { Addr string `json:"addr"` MaxConns int `json:"max_conns"` } func load(r io.Reader) (Config, error) { … - 11
A pprof profile of a hot request path puts
fmt.Sprintfandregexp.Compiletogether at about a third of CPU. Neither call site looks expensive. What is each one doing per call?Mediumfmt.Sprintfboxes every argument into ananyand walks the format string with reflection, and aregexp.MustCompileinside a function rebuilds the whole automaton on every call — both are per-call work that belongs tostrconvand to a package-level variable.// ✅ compiled once at init; *Regexp is safe for concurrent use var slugRe = regexp.MustCompile(`^[a-z0-9_]{3,16}$`) // ❌ ~4.3 µs and ~180 allocations per call: the automaton is rebuilt every time func validBad(s string) bool { return regexp.MustCompile(`^[a-z0-9_]{3,16}$`).MatchString(s) … - 12
After a port to
log/slogthe JSON lines contain"!BADKEY":"user_id", and one line carries the keyattempttwice. What do those two mean, and what should the call have looked like?MediumThe variadic form reads its arguments as alternating key/value pairs — a lone or mis-ordered argument becomes
!BADKEY, and nothing deduplicates keys — so the fix is typedslog.Attrvalues, one argument per field.func handle(ctx context.Context, id string, d time.Duration) { log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) // ❌ odd argument: there is no key to pair it with … - 13
You add a
MarshalJSONtoEventso its timestamp serialises as a plain date, and the first request that touches it kills the process withfatal error: stack overflow. The method body is onejson.Marshalcall. Explain the loop — and whytype alias = Eventdoes not break it.Hardjson.Marshalchecks for thejson.Marshalerinterface before it looks at fields, so marshalling the same type re-enters your method forever — the escape is a defined type (type eventJSON Event), which copies the fields and starts with an empty method set.type Event struct { Name string `json:"name"` At time.Time `json:"at"` } // ❌ re-enters itself: fatal error: stack overflow … - 14
NTP steps a box's clock back 400 ms. Every latency computed as
time.Since(start)stays sane, while a colleague'send.Unix() - start.Unix()goes negative. What is atime.Timecarrying that makes the first one safe?HardA
time.Timereturned bytime.Now()carries two clocks — a wall-clock reading and a monotonic one — andSub,SinceandUntiluse the monotonic reading whenever both operands have it; that clock only ever moves forward.func main() { start := time.Now() fmt.Println(start) // 2026-09-19 12:10:25.070834 +0500 +05 m=+0.000883876 // ^ the monotonic reading … - 15
A poller written as
select { case <-ctx.Done(): return; case <-time.After(30*time.Minute): }grew RSS on Go 1.22 and is flat on Go 1.24, while a second service that creates atime.Tickerper retry and keeps it is wrong on both. What changed, and what did not?HardBefore Go 1.23 the runtime kept every pending timer alive until it fired, so
time.Afterin a loop pinned a channel and its closure for the full interval; Go 1.23 made unreachableTimers andTickers collectible, but a ticker you still hold is reachable —Stopis still your job.// ❌ a fresh 30-minute timer every iteration. Before Go 1.23 each one stayed // alive until it fired; it is still an allocation, and the interval restarts // whenever ctx wins the select. func pollBad(ctx context.Context) { for { select { … - 16
You have to ingest a 40 GB NDJSON export whose every line is
{"type":..., "data":{...}}, wheredata's shape depends ontype, and write the lines you reject back out as NDJSON.io.ReadAllplusjson.UnmarshalOOMs, and decoding twice throughmap[string]anyis too slow. What does the stdlib give you?HardOne
json.Decoderover the reader decodes one value at a time in constant memory, and ajson.RawMessagefield parks thedatabytes undecoded untiltypesays which struct they belong in — so every line is parsed exactly once.type Envelope struct { Type string `json:"type"` Data json.RawMessage `json:"data"` // raw bytes until Type is known } type Push struct { …