What does the n argument to strings.SplitN control, and what do n=0 and n<0 mean?
answer
- a cap on pieces, not on cuts
- the tail is left whole
- zero means something surprising
- negative means unlimited, like Split
basics
~20 sn caps how many substrings come back, not how many cuts: with n above zero the last element is the unsplit remainder. A negative n means no limit, like strings.Split, and an n of zero returns nil.
solid answer
~50 s`strings.SplitN(s, sep, n)` bounds the **length of the result**, not the number of separators consumed. With `n > 0` you get at most `n` elements, and the final element is whatever is left of the string, separators and all — `strings.SplitN("a:b:c", ":", 2)` gives `["a", "b:c"]`. With `n < 0` there is no limit and it behaves exactly like `strings.Split`. With `n == 0` the result is `nil`, which is the case people forget: a variable that happens to be zero turns a parse into an empty result rather than an error, and ranging over `nil` runs zero iterations without complaining. The `n == 2` form was the standard way to split a `key=value` line before `strings.Cut` existed, and `Cut` is now clearer for that job because it also reports whether the separator was present at all.
code
go · 4 linesfmt.Println(strings.SplitN("a:b:c", ":", 2)) // [a b:c]
fmt.Println(strings.SplitN("a:b:c", ":", 1)) // [a:b:c]
fmt.Println(strings.SplitN("a:b:c", ":", 0)) // [] — the slice is nil
fmt.Println(strings.SplitN("a:b:c", ":", -1)) // [a b c]go deeper
Learn the shape first: n caps how many strings come back, and the last one keeps everything that was left, separators included. Split is just SplitN with a negative count.
Be able to state all three regimes without hesitating, including that n equal to zero returns nil, and explain why the unsplit remainder is the feature rather than an oddity.
Demonstrate the defensive habit: n is a maximum, so the result may be shorter, and any computed n that can reach zero silently produces nothing. Cover it with a table test over separator-absent, leading-separator and empty inputs.
Decide where the format's structure lives. Pushing a two-field parse into a named function that returns an error beats scattering SplitN calls with ad hoc length checks across a codebase other teams read.
## The signature ```go func SplitN(s, sep string, n int) []string ``` `SplitN` is `Split` with a cap. `strings.Split(s, sep)` is defined as `SplitN(s, sep, -1)`, and the `bytes` package mirrors both. ## The three regimes of n **n > 0 — at most n substrings.** The function performs at most `n-1` cuts and then stops looking. Whatever remains of the string after the last cut becomes the final element **verbatim**, including any further separators it contains. ```go strings.SplitN("a:b:c", ":", 2) // ["a" "b:c"] strings.SplitN("a:b:c", ":", 1) // ["a:b:c"] — zero cuts strings.SplitN("a:b:c", ":", 9) // ["a" "b" "c"] — cap not reached ``` Note the last line: `n` is a *maximum*, not a promise. If the input has fewer separators than the cap allows, you get a shorter slice, so code that indexes `parts[1]` after `SplitN(..., 2)` still has to check `len(parts)`. **n < 0 — no limit.** Identical to `Split`: every occurrence of the separator is cut, and the result length is the separator count plus one. **n == 0 — nil.** This is the surprising one and the reason the question gets asked. You do not get an empty-but-non-nil slice and you certainly do not get the whole string; you get `nil`. That is harmless when the zero is a literal, because nobody writes `SplitN(s, sep, 0)` on purpose. It is dangerous when `n` is computed — a configured field limit, a length subtraction, a value parsed from a config file that defaulted to zero — because ranging over `nil` runs zero iterations and `len(nil)` is `0`, so the parse silently produces nothing at all rather than failing. ## Why the remainder rule matters The "last element is the unsplit remainder" rule is the whole point of the function, and it exists because many formats have exactly one structural separator followed by a payload that is allowed to contain the same byte. A message-broker handshake frame is a good example. Its header lines are `key=value`, and a value such as a URL or a base64 blob may itself contain `=`. Splitting with an unlimited `n` would shred the value; `SplitN(line, "=", 2)` keeps it whole. The same shape appears in a `name:value` pair whose value is a timestamp containing colons, or a two-field record whose second field is free text. What `SplitN` does *not* tell you is whether the separator was present. `strings.SplitN("CONNECT", "=", 2)` returns a one-element slice, so you must check `len(parts) == 2` before touching `parts[1]`. That check is exactly the boolean `strings.Cut` hands you, which is why `Cut` is the better tool for a two-way split of a single line and `SplitN` is better when the cap is larger than two or is computed. ## A table test is the way to pin this down The behaviours worth enumerating in a test, once, for whatever parsing helper you build on top: | input | separator | n | result | |---|---|---|---| | `"a:b:c"` | `":"` | 2 | `["a", "b:c"]` | | `"a:b:c"` | `":"` | 1 | `["a:b:c"]` | | `"a:b:c"` | `":"` | 0 | `nil` | | `"a:b:c"` | `":"` | -1 | `["a", "b", "c"]` | | `"abc"` | `":"` | 2 | `["abc"]` | | `":abc"` | `":"` | 2 | `["", "abc"]` | | `""` | `":"` | 2 | `[""]` | The last three rows are the ones that break naive parsers. A separator-absent input yields one element, so `parts[1]` panics. A leading separator yields an empty first element, so a key can be the empty string and must be rejected explicitly. And the empty input yields a slice of length one containing an empty string, not an empty slice — inherited from `Split`. ## The related functions `strings.SplitAfterN` is the same but keeps the separator attached to the end of each element, which is useful when you want to reassemble the input exactly. `strings.SplitAfter` is its unlimited form. All four have `bytes` counterparts with identical rules. ## Summary rule to carry into review Say it as: **`n` counts pieces, not cuts; the tail is left whole; zero means nil.** Every misuse of `SplitN` in real code is a failure of one of those three clauses, and the third only bites when `n` is a variable.
- Given SplitN(line, "=", 2), what must you still check before using parts[1]?That `len(parts) == 2`. `n` is a maximum, not a guarantee: if the line has no `=`, `SplitN` returns a single-element slice and indexing `parts[1]` panics. This is exactly the check `strings.Cut` returns as a boolean, which is why `Cut` reads better for a two-way split of one line.
- How does strings.SplitAfterN differ from strings.SplitN?`SplitAfterN` cuts after each separator instead of around it, so the separator stays attached to the end of the preceding element: splitting `"a:b:c"` on `":"` with no limit gives `["a:", "b:", "c"]`. Concatenating the elements reproduces the input exactly, which `SplitN` cannot do because it discards the separators.
saying these in an interview costs you the question
- Says n counts the number of cuts performed
- Expects the trailing separators to be removed from the last element
- Thinks n equal to zero means unlimited
- Indexes parts[1] without checking the result length
- Believes a large n pads the result to that length