A Go tool indexes FindStringSubmatch's result at m[1] and panics mid-rewrite — what went wrong?
answer
- read the length in the panic message
- an empty result means the search found nothing
- a successful match is never short
- the replace call has nothing to index
- rename over the original instead of writing in place
basics
~20 sA line did not match, so FindStringSubmatch returned nil and indexing it panicked with index out of range, length 0. Guard the result for nil before indexing, and write rewritten files atomically so a panic cannot leave half the tree edited.
solid answer
~50 sThe panic message says it: `index out of range [1] with length 0`, so the slice was `nil`, and the only way `FindStringSubmatch` returns `nil` is that the pattern matched nowhere in that line. A migration tool sees comments, blank lines and placeholders written in an older syntax, and every one of those returns `nil`. Two fixes, and I want both. Locally, guard the call — `if m == nil { continue }` — or stop hand-indexing entirely and let `ReplaceAllString` do the substitution, since it is a no-op on a line with no match and cannot panic. Structurally, the reason this hurt at 3am is that the tool wrote each file in place as it went, so a panic on file 40 left 39 rewritten and the rest untouched. Rewrite into a temp file and rename over the original, so each file is all-or-nothing and a rerun is safe.
code
go · 13 lines// Panics on the first comment or blank line: no match means m is nil.
m := re.FindStringSubmatch(line)
out = append(out, rewrite(m[1]))
// Fix 1: make "no match" an explicit branch.
if m := re.FindStringSubmatch(line); m != nil {
out = append(out, rewrite(m[1]))
} else {
out = append(out, line)
}
// Fix 2: never index a match — a line with no match comes back unchanged.
out = append(out, re.ReplaceAllString(line, "${name}"))go deeper
Take away one habit: a search result must be checked before it is indexed, because a failed search hands back nothing at all rather than blanks.
Explain from the panic text why the slice was empty, and show both repairs — the explicit no-match branch and switching to the replace call, which cannot panic because it never indexes a match.
Go past the crash. Say how you make a file rewrite atomic and idempotent, what dry-run output you would want before touching a repository, and which real lines go into the regression table.
Own the policy for tools that edit many repositories: dry run by default, atomic per file, rerunnable, and loud on anything unrecognised — so a bad run costs a rerun instead of a manual reconstruction.
## Reading the panic ``` panic: runtime error: index out of range [1] with length 0 ``` The length is the tell. Length 0 means the slice was empty, and for `FindStringSubmatch` the only empty result is `nil`, returned when the pattern matched nowhere in the input. It is not a short match, not a group that captured nothing — the search simply failed on that line. Contrast this with a successful match, where the slice is always `NumSubexp()+1` long no matter which groups participated, so a successful match can never be *short*. The stack trace names the line, and the loop above it usually reads: ```go for _, line := range lines { m := re.FindStringSubmatch(line) out = append(out, rewrite(m[1])) // panics on the first line that does not match } ``` ## Why a real input hits it On the developer's sample file every line was a placeholder, so every line matched. Real configuration files across a repository contain comment lines, blank lines, sections written before the placeholder convention existed, and a handful using a variant spelling the pattern does not cover. Each of those returns `nil`, and the first one reaching this loop takes the process down. That is why the bug survives review and testing and surfaces on the widest input available — which is production, at an unhelpful hour. ## The local fix Guard at the call site, and decide explicitly what an unmatched line means: ```go m := re.FindStringSubmatch(line) if m == nil { out = append(out, line) // pass it through untouched continue } out = append(out, rewrite(m[1])) ``` `len(m) < 2` is an equally good guard and is the better one in a helper that may be handed patterns with different group counts. Either way, "no match" is now a branch in the code with a decision attached, rather than an assumption. Often the better fix is to stop indexing at all. `ReplaceAllString` with a template such as `"${name}"` walks the line, replaces every match and returns the line unchanged when there is none. It cannot panic on an unmatched line because it never indexes a match slice, and it handles several placeholders on one line, which the hand-rolled loop above quietly did not. ## The trap the guard does not close A length check tells you the search succeeded; it does not tell you that a particular group participated. A group that matched empty text and one that took part in no match both hold `""`. If the tool must tell an empty placeholder apart from an absent one, `FindStringSubmatchIndex` is the call: it returns byte-offset pairs and uses `-1, -1` for a group that did not participate. Reach for it only when the distinction actually matters; the string form is easier to read. ## The diagnostic worth writing Before trusting the fix, write a throwaway test that prints `re.SubexpNames()` next to the returned slice for three inputs: a line that matches fully, a line that does not match at all, and a line that matches with one optional group empty. Two same-length slices side by side make three things obvious at once — that slot 0 is the whole match, which index each name occupies, and that the non-matching line yields nothing to index. Then keep it as a table-driven test whose table includes the comment line, the blank line and the old-syntax line taken verbatim out of the repository that broke. That table is the real fix; the nil check is just the code that makes it pass. ## The structural fix The panic was one bug. Half a repository in a mixed state was a second, independent one, and at 3am it is the expensive one. A tool that rewrites files in place should be written so that any failure leaves the tree exactly as it found it: - **Per file, be atomic.** Build the new contents in memory, write them to a temp file in the same directory, then rename over the original. A rename is atomic, so a file is either fully old or fully new, never truncated. - **Be rerunnable.** Make the rewrite idempotent — running it twice must produce the same result — so recovery is "fix and run again" rather than "work out which files were done". - **Fail the whole run, loudly.** A migration that stops on the first unrecognised line and reports it is more useful than one that guesses, provided it has changed nothing yet. - **Offer a dry run** that prints the diff it would apply. On a repository-wide rewrite, that is the review artefact. ## What to say in an interview Name the cause from the message, fix it with the nil guard or by switching to `ReplaceAllString`, and then take the second step unprompted: the panic was recoverable, the half-applied rewrite was not, and a tool that edits many files owes its user atomicity and a dry run.
- Is checking len(m) > 1 a complete guard?It stops the panic, and it is the right check in a helper that handles several patterns. What it cannot do is distinguish a group that matched empty text from one that took part in no match — both read as an empty string. When that difference matters, use `FindStringSubmatchIndex`, which reports a non-participating group as the offset pair -1, -1.
- How do you stop a panic from leaving half the repository rewritten?Make each file's rewrite atomic: build the new contents, write a temp file beside the original, then rename over it, so a file is never partially written. Make the whole rewrite idempotent so rerunning after a fix is safe, and add a dry-run mode that prints the diff before anything is touched.
- What would you put in the scratch test that reproduces this?A table with a fully matching line, a comment line, a blank line and a line using the older placeholder spelling — copied verbatim from the repository that broke — and, while debugging, a print of `re.SubexpNames()` next to the returned slice for each. Same-length slices side by side make both the alignment and the nil case obvious.
- Why did this pass review and testing?The sample input was uniform: every line was a placeholder, so the search never failed. The nil branch has no test until somebody writes one, and the widest, messiest input in existence is the real repository. It is a class of bug that only appears when the input distribution widens, which is an argument for tables built from real files rather than hand-written ones.
saying these in an interview costs you the question
- Blames the regular expression instead of the missing nil check
- Thinks an unmatched line yields a slice of empty strings
- Says a nil slice can be indexed because reads of nil are safe
- Wraps the loop in recover instead of handling the no-match case
- Ignores that the rewrite was left half applied