In Go's encoding/csv, what does Reader.FieldsPerRecord do at 0, at a positive value, and at -1?
answer
- zero means infer, not disable
- the first record sets the bar
- one sign switches the check off
- the sentinel says wrong number of fields
basics
~10 sAt 0 the reader takes the field count from the first record and requires every later record to match. A positive value demands exactly that many fields. A negative value switches the check off.
solid answer
~40 s`FieldsPerRecord` is the shape check on every record. Its zero value, 0, does not mean "off" — it means "infer": the first record read fixes the count, and any later record with a different number of fields fails with an error wrapping `csv.ErrFieldCount`. Setting it to a positive number asserts the schema up front, which is useful when you know the export has, say, seven columns and want the very first record checked too. Setting it to -1 disables the check entirely, so records of different lengths are returned as-is and validating `len(record)` becomes your job. The failure arrives as a `*csv.ParseError`, so `errors.Is(err, csv.ErrFieldCount)` matches it and `errors.As` gets you `StartLine` and `Line` for the rejects report.
code
go · 8 linesr := csv.NewReader(f)
// FieldsPerRecord stays 0: the first record fixes the required count.
_, err := r.ReadAll()
var pe *csv.ParseError
if errors.As(err, &pe) && errors.Is(pe.Err, csv.ErrFieldCount) {
log.Printf("ragged record starting at line %d", pe.StartLine)
}go deeper
Know that the reader checks the field count by default and that the message about a wrong number of fields comes from that check, not from your own code.
Be able to state all three modes and, in particular, that the zero value infers the count from the first record rather than turning the check off. Name the sentinel error.
Show the judgement: assert the column count explicitly when you have a schema, and if you disable the check, say what replaces it and how the offending line reaches a rejects report.
Decide where the contract with a data supplier lives — an asserted column count in code, a schema file, or a validation stage before the import — and who is paged when a partner changes their export shape.
## What the field counts `csv.Reader` returns each row as a `[]string` of parsed fields. `FieldsPerRecord` decides whether the reader polices how many fields each row has, and it is one of the few Go struct fields where the zero value is not the permissive option. Three modes, decided by sign: | Value | Behaviour | |---|---| | `0` (the zero value) | The number of fields in the **first record read** is recorded, and every subsequent record must match it. | | `> 0` | Every record must have exactly that many fields, including the first. | | `< 0` (idiomatically `-1`) | No check at all. Records may have any length. | ## The 0 case is the one people get wrong Because `0` is what you get by default, most Go programs read CSV with the check on without ever having asked for it. That is usually what you want — a column count that changes mid-file almost always means the file is broken — but it explains a class of bug reports that read "the import worked yesterday and today it fails at line 40,000". Nothing changed in your code; the partner appended a trailing summary line with three fields instead of seven. Note that the count is taken from the first record *read*, not from a header conceptually. If you consume the header with its own `Read` call, that header sets the count. If the header has a stray trailing comma and the data rows do not, every data row then fails. Also note what counts as a field: the reader has already handled quoting, so a quoted field containing commas is one field. The count is of parsed fields, never of comma characters on the line. Blank lines are skipped rather than treated as one-empty-field records, and lines beginning with the `Comment` rune, if you set one, are skipped too — neither participates in the count. ## The error you get A mismatch produces a `*csv.ParseError` whose `Err` is the sentinel `csv.ErrFieldCount` ("wrong number of fields"). Because `*csv.ParseError` implements `Unwrap`, both of the standard error helpers work: ```go if errors.Is(err, csv.ErrFieldCount) { /* ragged record */ } var pe *csv.ParseError if errors.As(err, &pe) { /* pe.StartLine, pe.Line */ } ``` `StartLine` is the line where the record began — which differs from `Line` when a quoted field spans multiple lines — and that is the number worth showing a human, because it is the row they will look for in the file. With `ReadAll`, a single ragged record aborts the whole call and you get no records back. With `Read`, the failure concerns one record and you can log it and keep going. ## When to set what **Leave it at 0** for a file you expect to be internally consistent and where you have no independent schema. It costs nothing and catches truncation and stray delimiters early. **Set a positive count** when you do have a schema and want the check applied to the first record as well. `r.FieldsPerRecord = 7` turns "the partner shipped last quarter's six-column format" into an immediate error on row one, instead of an import that succeeds and writes nonsense into the wrong columns. **Set -1** when the format is genuinely ragged by design, or when you want to own the validation so you can produce a better message than the package's. The moment you set -1 you have taken on the check: `if len(rec) != 7 { … }` inside the loop, with your own error carrying the line number, is the replacement. ## The trap worth naming The most common mistake is reading `FieldsPerRecord = 0` as "disabled" and then being astonished that an unchecked import fails on a ragged row. The second most common is setting `-1` to make an error go away without adding a replacement check — the rows still get imported, they are just wrong now, and the failure has moved from the parser to whatever downstream code indexes `rec[5]` and panics or, worse, silently reads the wrong column.
- Which value disables the check, 0 or -1?-1, or any negative value. 0 is the zero value and means infer: the reader remembers how many fields the first record had and enforces that from then on. With -1 records of differing lengths come back untouched, and validating len(record) becomes your responsibility.
- How do you detect specifically a field-count failure and report the row?Match the sentinel with errors.Is(err, csv.ErrFieldCount) — *csv.ParseError unwraps to it. Then pull the *csv.ParseError out with errors.As and use StartLine, the line where the record began, in your message. That is the number a human can find in the file, and it differs from Line when a quoted field spanned several lines.
- You read the header row with its own Read call. Does that affect the check?Yes, when FieldsPerRecord is 0: the header is simply the first record read, so its field count becomes the required count for every data row. If the header and the data disagree — a trailing comma in the header, say — every data row then fails. Setting the count explicitly to the schema's column count avoids letting the header define the contract.
saying these in an interview costs you the question
- Thinks 0 disables the field-count check
- Says the header row is exempt from the count
- Counts comma characters rather than parsed fields
- Sets -1 and adds no replacement length check
- Expects ReadAll to skip the ragged record and continue