Last Updated: May 17, 2026
The strings package is the standard toolkit for working with text in Go. It covers the operations you reach for daily: searching, splitting, joining, replacing, trimming, and case folding. Most of the functions are simple wrappers over careful, well-tested loops, which is exactly why you should use them instead of writing your own. This lesson walks through the API by category, with code examples drawn from product catalogs, customer input, and review text.
These functions answer questions about a string without modifying it: does it contain something, where does that something start, and how many times does it appear. They all run in O(n) over the input.
strings.Contains(s, substr string) bool reports whether substr appears anywhere in s. It's the most-used function in the package.
The empty-string case is worth pausing on. By definition, every string contains the empty string, so strings.Contains(s, "") always returns true. That can trip up filter code that builds a search term dynamically: if the user-supplied query is empty, every product in the catalog "matches". Guard with if query == "" before calling Contains when that's not what you want.
strings.ContainsAny(s, chars string) bool reports whether any rune from chars appears in s. It's not about substring matching, it's about character-set membership.
This is a quick sanity check for HTML-unsafe characters in user input. Don't rely on it as a security boundary, proper escaping is the answer, but it works as a first-pass filter.
strings.ContainsRune(s string, r rune) bool is the rune-typed cousin. Reach for it when you have a rune value (say, from a for range loop) and want to ask whether it appears in another string.
Using Contains for a single character also works (strings.Contains("$€¥£", "€")), but ContainsRune is clearer about intent and avoids the implicit string-to-rune conversion thinking.
strings.HasPrefix(s, prefix string) bool and strings.HasSuffix(s, suffix string) bool answer "does this string start (or end) with this exact substring?".
These are byte-exact, case-sensitive comparisons. They're cheap, they short-circuit on the first mismatch, and they're the right choice for tag-style prefixes ("BOOK-", "TOY-"), file extensions, and URL schemes.
Cost: HasPrefix and HasSuffix are O(len(prefix)) and O(len(suffix)) respectively, not O(len(s)). They look at one end of s and stop as soon as a byte differs. Even for very long strings, the work is bounded by the affix length.
A common pattern is routing on a prefix:
A switch with strings.HasPrefix is the idiomatic alternative to a chain of if/else if blocks when you're dispatching on string prefixes.
strings.Index(s, substr string) int returns the byte offset of the first occurrence of substr in s, or -1 if it's not there.
The -1 sentinel matters. Don't treat the result as a length, and don't slice with it without checking first. The classic bug is s[strings.Index(s, sep):] when sep isn't in s, which becomes s[-1:] and panics.
strings.LastIndex(s, substr string) int returns the offset of the final occurrence instead of the first. It's useful when the separator can appear inside a value:
Index would have split on the first dot and given you product and images.notebook.png, which isn't what you want for "everything before the final dot".
strings.IndexByte(s string, c byte) int and strings.IndexRune(s string, r rune) int are faster, single-character variants. IndexByte is the fastest because it never has to decode UTF-8, it just scans for a literal byte.
IndexByte is the right call when the thing you're searching for is a literal ASCII character. Use IndexRune when the character is non-ASCII (€, é, an emoji). Use Index when the search target is a string of more than one character.
Cost: IndexByte uses architecture-optimized SIMD on most platforms, so it's noticeably faster than scanning manually in a loop. For ASCII separator hunting in hot paths, prefer it over Index.
strings.Count(s, substr string) int returns the number of non-overlapping occurrences of substr in s. As a special case, Count(s, "") returns utf8.RuneCountInString(s) + 1.
A common e-commerce use is keyword frequency in reviews. If you want a case-insensitive count, lowercase both sides first:
The empty-string special case rarely comes up in real code, but it's worth knowing about so it doesn't surprise you when reading the docs.
These functions turn one string into many, or many strings into one. The split-family functions all return []string, and they each take a different view of "what counts as a separator".
strings.Split(s, sep string) []string cuts s at every occurrence of sep and returns the pieces. The separator itself is removed.
If sep doesn't appear in s, you get back a one-element slice containing s unchanged. If s is empty, you get a one-element slice containing the empty string, not an empty slice. If sep is empty, Split returns one slice element per rune.
The empty-string case is the one most people overlook: splitting "" by any non-empty separator gives [""], length 1, which is rarely what loop code expects.
strings.SplitN(s, sep string, n int) []string adds a cap on how many pieces you want. The last piece holds the rest of the string, separators included.
SplitN with n = 3 produces at most three pieces. Everything past the second comma is bundled into the final element. Pass n = -1 for unlimited (identical to Split). Pass n = 0 for nil.
strings.SplitAfter(s, sep string) []string works like Split but keeps the separator attached to each piece.
The arrows are still there on each piece except the last (which has nothing after it). SplitAfter is useful when you want to preserve the original delimiter, for instance when re-joining a subset of pieces and getting the formatting right for free.
| Function | Trailing separator kept? | Limit on pieces? | Returns on empty input |
|---|---|---|---|
Split(s, sep) | No | No | [""] |
SplitN(s, sep, n) | No | Yes (n pieces max) | [""] if n > 0, nil if n == 0 |
SplitAfter(s, sep) | Yes | No | [""] |
SplitAfterN(s, sep, n) | Yes | Yes | [""] if n > 0, nil if n == 0 |
strings.Fields(s string) []string splits on runs of whitespace. Multiple spaces, tabs, and newlines all collapse to a single delimiter, and leading and trailing whitespace is dropped entirely.
Fields is the right tool for parsing user-typed input where whitespace is sloppy. Split(s, " ") would have produced six pieces, four of them empty strings caused by the consecutive spaces. Fields gives you the three actual tokens.
If you need a custom definition of "whitespace" (say, comma or pipe as delimiters), use strings.FieldsFunc(s, func(r rune) bool { ... }) which takes a predicate over each rune.
FieldsFunc also drops empty pieces, which is exactly the difference between it and Split. For mixed-delimiter parsing, this beats writing a state machine.
strings.Join(elems []string, sep string) string is the inverse of Split: it concatenates a slice of strings, inserting sep between adjacent elements.
Join is the right call whenever you're tempted to write for i, v := range items { result += v; if i < len(items)-1 { result += "," } }. The package handles the "no separator after the last item" edge case for you.
Cost: strings.Join pre-computes the total length and allocates the result buffer once. A naive += loop in your own code allocates a new string on every iteration, which is O(n^2) in total bytes written. For more than a handful of pieces, Join is dramatically faster.
strings.Cut(s, sep string) (before, after string, found bool) was added in Go 1.18 and is now the preferred way to split a string into exactly two pieces around a separator.
Before Cut existed, the standard pattern was parts := strings.SplitN(s, sep, 2) followed by a length check and indexing into parts[0] and parts[1]. That works but it's noisy. Cut is a single call that returns both halves and a found flag, with no slice allocation.
Chained Cut calls are the idiomatic way to peel fields off the front of a string one at a time. Each call gives you the next field and the remainder.
When sep isn't in s, Cut returns (s, "", false). The before field gets the entire input, which is often exactly what you want for "optional suffix" parsing:
When there's no ?, the full string lands in name and query is empty. No nil checks, no length math.
| Use case | Prefer | Why |
|---|---|---|
| Split into exactly two parts on first separator | Cut | One call, no slice allocation, returns found flag |
| Split into exactly two parts on last separator | manual LastIndex + slice | Cut always uses the first occurrence |
| Split into many parts | Split or SplitN | Cut only handles two |
| Iterate one field at a time | chained Cut calls | Cleaner than indexing into the SplitN result |
Go 1.20 added two related helpers. strings.CutPrefix(s, prefix string) (after string, found bool) strips a prefix and tells you whether it was there.
The pre-Go-1.20 equivalent was if strings.HasPrefix(s, p) { rest := s[len(p):]; ... }, which works but does the prefix check twice (once in HasPrefix, once implicitly by computing len(p)). CutPrefix collapses the check and the slice into one operation.
strings.CutSuffix(s, suffix string) (before string, found bool) is the symmetric version for trailing strings.
Use CutPrefix and CutSuffix whenever you'd otherwise write HasPrefix + slice or HasSuffix + slice. The combined call is shorter and signals intent better.
These functions return a new string with some transformation applied. Strings are immutable in Go, so none of these touch the input.
strings.Replace(s, old, new string, n int) string replaces the first n non-overlapping occurrences of old with new. Use n = -1 (or strings.ReplaceAll) to replace every occurrence.
strings.ReplaceAll(s, old, new) is a shortcut for strings.Replace(s, old, new, -1), added in Go 1.12. Reach for it when you want all matches replaced, which is the more common case.
A common gotcha: if old is empty, Replace inserts new between every rune and at both ends. This is rarely what you want, so check for an empty pattern at the call site if your old is built from user input.
When you have several substitutions to apply at once, calling ReplaceAll repeatedly works but is wasteful: each call scans the input again.
strings.NewReplacer(pairs ...string).Replace(s string) string lets you build a single replacer that does all the substitutions in one pass. The arguments are (old1, new1, old2, new2, ...) in alternation.
The replacer is built once and reused. If you're escaping HTML in a hot path (or doing any other multi-pair substitution), build the *Replacer at package init and call its Replace method per request.
Cost: NewReplacer builds an internal trie that matches all the search strings in a single left-to-right scan of the input. For k pairs, chained ReplaceAll calls do k full passes (O(k*n)). A *Replacer does one pass (O(n)), with constant factor proportional to the trie depth. For more than two or three substitutions, prefer NewReplacer.
*Replacer is also goroutine-safe, so you can stash one in a package-level variable and use it from any goroutine.
strings.ToLower(s string) string and strings.ToUpper(s string) string return case-folded copies of s. They handle Unicode correctly: accented letters, Greek, Cyrillic, and so on all fold the way the Unicode standard says they should.
This is the textbook case-insensitive search pattern: lowercase both sides, then call Contains. It works for ASCII and most Latin-script text. For "should İ (Turkish capital I-with-dot) match i?" style edge cases, you need the golang.org/x/text/cases package, which knows about locale-specific casing rules.
strings.ToTitle(s string) string uppercases every letter. It's the "title-case" mapping in the Unicode sense, not the natural-language sense of "Capitalize Each Word".
That's almost always not what people expect from a function called "Title". If you want the natural-language version (first letter of each word uppercase, the rest lowercase), strings.Title used to exist but is now deprecated.
strings.Title was deprecated in Go 1.18 because it didn't handle Unicode word boundaries properly (it considered any non-letter a word separator, which produces wrong results for many languages). The modern replacement lives in golang.org/x/text/cases:
You need to add golang.org/x/text to your module with go get golang.org/x/text. It's not part of the standard library, but it is officially maintained by the Go team.
strings.TrimSpace(s string) string removes leading and trailing whitespace as defined by Unicode (spaces, tabs, newlines, and a handful of other whitespace runes).
TrimSpace is the first function to reach for when normalizing user input. Form fields, copy-pasted email addresses, and CSV cells routinely come in with stray whitespace.
strings.Trim(s, cutset string) string removes every leading and trailing rune that appears in cutset. Note that cutset is a set of runes, not a substring to strip.
The second example strips every leading and trailing [ or ], in any order. It doesn't strip from the middle, and it doesn't care which bracket was on which side.
strings.TrimLeft(s, cutset string) string and strings.TrimRight(s, cutset string) string are the one-sided versions.
TrimLeft strips leading slashes from a path or leading zeros from a number string. Note that TrimLeft(number, "0") of "0000" produces "", the empty string, not "0". This catches people who expect "keep at least one zero" behavior.
strings.TrimPrefix(s, prefix string) string and strings.TrimSuffix(s, suffix string) string are different: they remove an exact substring, not a set of characters. If the prefix or suffix isn't there, s is returned unchanged.
The third line is unchanged because the URL doesn't start with ftp://. This is the safe "strip this prefix if present" pattern. TrimPrefix and TrimSuffix are similar to CutPrefix and CutSuffix, except they don't return a found flag, you only get the (possibly-unchanged) string back.
| Function | Removes | Anchored? |
|---|---|---|
TrimSpace(s) | Leading/trailing Unicode whitespace | Both ends |
Trim(s, cutset) | Any rune in cutset | Both ends |
TrimLeft(s, cutset) | Any rune in cutset | Left only |
TrimRight(s, cutset) | Any rune in cutset | Right only |
TrimPrefix(s, prefix) | Exact substring prefix | Left only |
TrimSuffix(s, suffix) | Exact substring suffix | Right only |
The cutset versus exact-substring distinction is the one most people get wrong. Trim(s, "abc") doesn't strip the substring "abc", it strips any combination of a, b, and c.
strings.Repeat(s string, count int) string returns s concatenated with itself count times. It panics if count is negative or if the result would overflow int.
Repeat is the right tool for building separators, indentation strings, or progress-bar visuals. It pre-allocates exactly the right amount of memory, so it's much faster than building the same thing in a loop with +=.
strings.Map(mapping func(rune) rune, s string) string runs a function on every rune and returns the transformed string. Returning a negative rune from the mapping function drops that rune entirely.
Map is the general-purpose transform. Returning -1 filters out the rune entirely, so this snippet keeps only letters and digits. Returning a different rune transforms it: strings.Map(unicode.ToUpper, s) is essentially what strings.ToUpper does internally.
The classic ROT13 example. Map calls the function once per rune in order, which makes it straightforward to express any per-character transform.
Cost: strings.Map allocates a new string and visits every rune in s. If the mapping is the identity for every rune (the function never returns a different value), the result is the same string, but the work was still done. Prefer ReplaceAll or NewReplacer for substring-based edits.
Comparison functions answer "are these two strings equivalent in some sense?" with more nuance than ==.
strings.EqualFold(s, t string) bool reports whether s and t are equal under Unicode case folding. It's the right way to do case-insensitive equality.
EqualFold is what you want for comparing user-supplied product names or category labels against a list, where casing shouldn't matter. The naive alternative is strings.ToLower(s) == strings.ToLower(t), which works but allocates two new strings for the lowercased copies. EqualFold walks the two inputs in parallel and folds case rune by rune without allocating.
Cost: EqualFold does no allocation. strings.ToLower(s) == strings.ToLower(t) allocates two new strings, each the same length as the input. For comparing many candidate names against a query in a loop, EqualFold is the obvious win.
A subtle point: EqualFold uses simple Unicode case folding, not full locale-aware folding. The Turkish dotted-versus-dotless I problem isn't handled. For locale-aware case comparison, use golang.org/x/text/cases.
For "did the user type this exact product name, casing aside?", EqualFold is one call and zero allocations.
strings.Compare(s, t string) int returns -1 if s < t, 0 if s == t, and +1 if s > t. It exists for symmetry with the bytes package, but the docs explicitly say to prefer the built-in ==, <, >, and >= operators on strings.
The only realistic reason to use Compare is when you have a generic sorting framework that expects a func(a, b T) int and you want one-liner conformance: func(a, b string) int { return strings.Compare(a, b) }. Even there, cmp.Compare from the cmp package (Go 1.21+) is preferred for new code because it works on any ordered type.
A few strings functions don't fit a category but come up often enough to mention.
strings.NewReader(s string) *strings.Reader wraps a string as an io.Reader. This is how you pass a string into any function that expects to read from a stream: json.NewDecoder, csv.NewReader, bufio.NewScanner, and friends.
strings.NewReader is the idiomatic way to test or seed any stream-based API with a literal string. It avoids the temptation to write data to a temp file just to read it back.
For building strings incrementally (concatenating many small pieces into one big result), use strings.Builder. The short version: Builder accumulates writes into an internal buffer and produces the final string with one allocation when you call String(). It's much faster than chained += for large outputs.
| Function | Returns | Use For |
|---|---|---|
Contains(s, sub) | bool | "Does s contain sub?" |
ContainsAny(s, chars) | bool | "Does any rune in chars appear in s?" |
HasPrefix(s, p) / HasSuffix(s, p) | bool | Anchored prefix/suffix test |
Index(s, sub) / LastIndex(s, sub) | int | First/last byte offset of sub, or -1 |
IndexByte(s, c) / IndexRune(s, r) | int | Fast single-character search |
Count(s, sub) | int | Number of non-overlapping occurrences |
Split(s, sep) / SplitN(s, sep, n) | []string | Multi-piece split |
SplitAfter(s, sep) | []string | Like Split but keeps separators attached |
Fields(s) | []string | Split on runs of whitespace, drop empties |
Join(elems, sep) | string | Concatenate with separator |
Cut(s, sep) | (before, after, found) | Two-piece split (Go 1.18+) |
CutPrefix(s, p) / CutSuffix(s, p) | (rest, found) | Strip + check (Go 1.20+) |
Replace(s, old, new, n) / ReplaceAll(s, old, new) | string | Substring replacement |
NewReplacer(pairs...) | *Replacer | Multi-pair replacement, one pass |
ToLower(s) / ToUpper(s) / ToTitle(s) | string | Case conversion |
TrimSpace(s) | string | Strip leading/trailing whitespace |
Trim(s, cutset) / TrimLeft/Right(s, cutset) | string | Strip any rune in cutset |
TrimPrefix(s, p) / TrimSuffix(s, p) | string | Strip exact prefix/suffix if present |
Repeat(s, count) | string | Concatenate s with itself n times |
Map(f, s) | string | Transform every rune |
EqualFold(s, t) | bool | Case-insensitive equality |
Compare(s, t) | int | Three-way comparison (prefer ==, <, >) |
NewReader(s) | *Reader | Wrap a string as io.Reader |
Here's a worked example that uses a handful of strings functions together to parse and clean a batch of product lines.
Five strings calls handle the whole pipeline. Split breaks the input into lines, TrimSpace strips whitespace, HasPrefix filters out comments, and chained Cut calls peel off each field. No regex, no state machine, no third-party dependency.
The two empty lines and the comment line all get filtered cleanly. The padding inside each line (PEN-22 | $1.50 | In Stock) gets normalized by the per-field TrimSpace. That's the value of building from strings primitives instead of writing a one-off parser: each function is small, well-tested, and easy to reason about in isolation.
The diagram traces the data flow. Each arrow is one strings call. The decision node is HasPrefix("#") || line == "", which routes comments and blank lines to the skip path. Everything else flows through the two Cut calls and out as a structured Product.
Contains, HasPrefix, HasSuffix, Index, and Count are the read-only checks. They return bool or int without allocating.Index and friends return -1 when the search fails. Always check before slicing.Split chops a string into many pieces. Cut (Go 1.18+) is the better choice when you only need two pieces and want a found flag.Fields splits on runs of whitespace and drops empties. Use it for parsing sloppy user input.Join is O(total length) and pre-allocates the result. Never build a separated string with += in a loop.Replace and ReplaceAll do single-pair substitution. For many pairs at once, build a strings.NewReplacer once and reuse it.TrimSpace strips whitespace. Trim(s, cutset) strips characters. TrimPrefix and TrimSuffix strip exact substrings. Don't mix them up.EqualFold is the right way to do case-insensitive equality. It allocates nothing, unlike ToLower + ==.strings.NewReader(s) wraps a string as an io.Reader for any stream-based API.strings.Title is deprecated; use golang.org/x/text/cases for natural-language title casing.The next lesson covers the strconv package, which handles conversions between strings and numeric types: parsing prices from form input, formatting quantities for display, and the edge cases around overflow and invalid input.