How to Split a String in Go with strings.Split

Use the `strings.Split` function from the standard library to break a string into a slice of substrings based on a specific delimiter.

Parsing breaks when data fights back

You're writing a parser for a configuration file. Each line follows the pattern key:value. You split on the colon, grab the key, and grab the value. It works until a user sets a password that contains a colon, or an IPv6 address appears in the value. Your parser eats the wrong field and the application crashes. Or you're processing a CSV where a description field contains a comma. strings.Split shreds the row into pieces that don't match the columns. Splitting strings feels trivial until the data contains the delimiter.

Go's strings package provides Split to break a string into a slice of substrings. The function scans the input, finds every occurrence of a separator, and returns a slice containing the parts between those separators. The result is a []string. Strings in Go are immutable. Split does not modify the original string. It allocates a new slice and copies the substrings into new string values. Every call creates new memory.

How Split works

strings.Split takes two arguments: the source string and the separator string. It returns a slice of strings. The function iterates over the source, locates each instance of the separator, and extracts the segments. If the separator does not appear, the function returns a slice of length one containing the original string. If the source is empty and the separator is not empty, the function returns a slice of length one containing an empty string. This preserves the invariant that the number of parts equals the number of separators plus one.

The separator can be any string, including multi-byte UTF-8 sequences. Split works on bytes, so it handles Unicode correctly as long as the separator is a valid string. The function does not understand runes directly. You must convert a rune to a string before using it as a delimiter.

Minimal example

Here's the basic split: pass the string, pass the delimiter, get back a slice.

package main

import (
	"fmt"
	"strings"
)

func main() {
	// Input string with comma separators.
	line := "apple,banana,cherry"
	// Split returns a slice of substrings.
	// Allocates a new slice and copies content.
	fruits := strings.Split(line, ",")

	// Print the resulting slice.
	fmt.Println(fruits)
	// Access index 1 for the second item.
	fmt.Println(fruits[1])
}

Walkthrough

At compile time, the compiler checks that line is a string and "," is a string. strings.Split returns []string, so fruits is inferred as a slice of strings. The compiler also verifies that you import the strings package. If you forget the import, the compiler rejects the program with undefined: strings.

At runtime, strings.Split allocates a slice header and a backing array. It scans line to find the commas. It calculates the number of parts and grows the slice to fit them. It copies the bytes for each segment into new string values. The original line remains unchanged. Strings are immutable in Go. The function returns the new slice. If you access fruits[1], you get "banana". If you access fruits[3], the program panics with index out of range [3] with length 3. Always check the length of the slice before indexing.

Realistic example: SplitN and Cut

Real data rarely behaves perfectly. When a delimiter appears inside a value, strings.Split breaks the structure. Use strings.SplitN to cap the number of splits. This keeps the remainder of the string intact. SplitN takes a third argument: the maximum number of parts. If the separator appears more times than the limit, the last part contains the rest of the string.

Here's how to parse a key-value pair where the value might contain the delimiter:

package main

import (
	"fmt"
	"strings"
)

func main() {
	// Config line where value might contain the delimiter.
	raw := "server:192.168.1.1:8080"
	// SplitN limits splits to 2 parts.
	// Keeps the rest of the string intact.
	parts := strings.SplitN(raw, ":", 2)

	// Check length before indexing.
	// Avoids panic if input is malformed.
	if len(parts) == 2 {
		fmt.Println("Key:", parts[0])
		fmt.Println("Value:", parts[1])
	}
}

Go 1.18 introduced strings.Cut. It splits on the first occurrence and returns the parts plus a boolean. It's more efficient than SplitN for a single split because it avoids allocating a slice. Cut returns before, after, and found. If the separator is not found, before is the original string, after is empty, and found is false.

package main

import (
	"fmt"
	"strings"
)

func main() {
	// URL with query string.
	url := "https://example.com/path?key=value"
	// Cut splits on the first question mark.
	// Returns before, after, and found.
	path, query, found := strings.Cut(url, "?")

	// Use the boolean to handle missing delimiter.
	if found {
		fmt.Println("Path:", path)
		fmt.Println("Query:", query)
	} else {
		fmt.Println("No query string found")
	}
}

Pitfalls and edge cases

strings.Split has quirks that trip up beginners. If the input is empty and the delimiter is not empty, the function returns a slice with one empty string. strings.Split("", ",") returns []string{""}. The length is 1. Accessing index 0 gives an empty string. This trips up code that assumes an empty input yields an empty slice. Always check the length before indexing.

If the input is just the delimiter, you get two empty strings. strings.Split(",", ",") returns ["", ""]. Trailing delimiters produce empty strings at the end. strings.Split("a,b,", ",") returns ["a", "b", ""]. If you need to ignore empty tokens, strings.Split is the wrong tool. Use strings.Fields for whitespace, or filter the result manually.

strings.Split("", "") returns an empty slice. This is a special case. An empty separator matches every position, but the function treats it as a no-op and returns nothing.

Convention: The strings package functions are pure. They don't mutate inputs. They return new values. This matches Go's preference for explicit returns over hidden side effects. Also, gofmt handles the formatting of your code. Don't worry about indentation. Focus on the logic. Trust the tool.

Performance and allocation

Allocation is the cost. In a tight loop processing millions of lines, Split allocates heavily. Each call creates a slice and new strings. This pressures the garbage collector. If you only need to check for a delimiter, strings.Index returns the position without allocating a slice. If you need to parse a line into fixed fields, consider strings.Cut or strings.SplitN with a small limit.

For large strings, strings.Split copies the entire content into the result. If you're processing a 1MB string, you allocate 1MB or more for the result. If you split into 1000 parts, you have 1000 small strings. This can fragment memory. In performance-critical paths, consider using strings.Index in a loop to process segments without allocating the full slice. Or use strings.Reader to read chunks.

Unicode handling is correct but subtle. Split works on bytes. If your delimiter is a multi-byte UTF-8 character, pass it as a string. strings.Split handles this correctly. However, Split does not understand runes directly. You must convert a rune to a string before using it as a delimiter. strings.Split(s, string(rune)) works. Passing a rune directly causes a type error. The compiler complains with cannot use r (untyped rune constant) as string value in argument.

Decision matrix

Use strings.Split when you need all substrings and the delimiter is consistent throughout the input. Use strings.SplitN when the delimiter might appear in the value, or you only need a prefix and want to keep the rest intact. Use strings.Cut when you need to split on the first occurrence and want a boolean flag to check if the delimiter existed. Use strings.Fields when splitting on arbitrary whitespace and ignoring empty tokens. Use strings.Index when you only need to find the delimiter position without allocating a slice. Use strings.SplitAfter when you need to keep the delimiter attached to the end of each substring. Use regexp.Split when the delimiter is a pattern, not a literal string.

Strings are immutable. Splits allocate. Check the length. Indexing blindly panics.

Where to go next