Start with a parser that reads a single field and end with a library that turns messy CSV text into records, streams one record at a time, maps rows to headers, and writes records back out quoting exactly the fields that need it. Every lesson is one concrete spec with exact records, fields, output, and error positions: a quoted field keeps an embedded comma and newline, a doubled quote collapses to one, a lone carriage return inside quotes survives, an unterminated quote errors at its exact line and column, and write-then-parse round-trips.
Over 27 lessons you build a working CSV library from scratch. It begins as a finite-state machine that walks the input one rune at a time: first splitting a plain comma-and-newline table into records and fields, then growing a quoting state machine so a field wrapped in double quotes can hold the delimiter, a newline, and a doubled-quote escape, with clear errors that name the line and column of an unterminated quote or stray text after a closing quote. From there you handle the real world: carriage-return and CRLF line endings, a lone carriage return preserved inside quotes, significant leading and trailing spaces, and a leading UTF-8 byte-order mark.
On top of that core you add a configurable dialect (a custom delimiter, optional trimming, comment lines), a streaming reader that yields one record at a time with optional strict field-count checking for ragged rows, and a header mode that maps each row to a name-to-value record. Then you build the other half of the library, a writer that emits records and quotes exactly the fields that need it, proven correct by round-trip equivalence, and finish with a capstone that streams a messy real-world file (quotes, an embedded newline, CRLF, a byte-order mark, a ragged row) into records and writes a normalized file back.
This is a teaching-grade CSV library built around RFC 4180 and the finite-state-machine design that real parsers use: it parses and writes conforming CSV exactly, handles the common messy variations, and reports malformed input with precise positions. It is honest about its choices, which it states as it goes and collects in the caveats: it works on decoded UTF-8 text, treats a blank line as one empty field, and resolves duplicate header names last-wins. What you finish with is the honest core that production CSV libraries extend with type inference, richer encodings, and larger dialect matrices.
A CSV file is a table of records, and each record is a list of fields. Today you build the smallest possible parser, one that reads a single unpunctuated chunk of text as a table with one record holding one field, so the public surface exists from day one and every later lesson thickens it.
Parse a chunk of text with no commas or newlines into a table of one record holding one field.
Every CSV document is a table: an ordered list of records (the rows), and every record is an ordered list of fields (the cells). That two-level shape, records made of fields, is the whole data model, so the type your parser returns is a list of lists of strings. Start with the smallest input that has any content at all, a single run of characters with no commas and no line breaks. It is one record, and that record has exactly one field.
The one decision worth making now is what empty input means. A file with no
bytes has no rows in it, so parsing "" returns zero records, an empty table,
not a table containing one empty row. Keep that distinction in mind: absence of
any record is different from a record that happens to hold an empty field, which
is a case you will meet very soon. Return a plain list of records for now; the
commas, the newlines, and the quoting all arrive one lesson at a time on top of
this shape.
// records are a slice of records; each record is a slice of string fieldsfunc Parse(input string) [][]string {if input == "" {return [][]string{}}return [][]string{{input}}}
The library and CLI are complete for a well-behaved comma or simple-dialect CSV workflow with clear positioned error reporting, but parsing loads the whole input into memory and the csvtool CLI does not yet expose the library dialect options as flags.
The closest thing CSV has to a grammar: how records, fields, quoting, and the doubled-quote escape are defined, and why CRLF is the canonical line ending. Keep it open beside the quoting and line-ending chapters.
The clearest treatment of dialects: delimiter, quoting rules, trimming, and line terminators as configurable knobs. The Dialect this project builds mirrors its design.
A survey of every way a naive split on commas and newlines is wrong: embedded delimiters, embedded newlines, quotes, encodings, and ragged rows. This is the CSV-is-harder-than-you-think list the project pins down one lesson at a time.
A production reference implementation of exactly this design: a state-machine reader with positioned errors and field-count checking, plus a writer with minimal quoting. Useful to compare your API against once each chapter lands.
The modern attempt to give CSV real semantics: dialects, headers, and typed columns. Read it for where a library like this one goes next, beyond raw string records.