AlgoMaster Logo

The strings Package

Last Updated: May 17, 2026

14 min read

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.

Searching and Checking

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.

Contains, ContainsAny, ContainsRune

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.

HasPrefix and HasSuffix

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.

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.

Index, LastIndex, IndexByte, IndexRune

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.

Count

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.

Splitting and Joining

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".

Split, SplitN, SplitAfter

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.

FunctionTrailing separator kept?Limit on pieces?Returns on empty input
Split(s, sep)NoNo[""]
SplitN(s, sep, n)NoYes (n pieces max)[""] if n > 0, nil if n == 0
SplitAfter(s, sep)YesNo[""]
SplitAfterN(s, sep, n)YesYes[""] if n > 0, nil if n == 0

Fields

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.

Join

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.

Cut, CutPrefix, CutSuffix

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 casePreferWhy
Split into exactly two parts on first separatorCutOne call, no slice allocation, returns found flag
Split into exactly two parts on last separatormanual LastIndex + sliceCut always uses the first occurrence
Split into many partsSplit or SplitNCut only handles two
Iterate one field at a timechained Cut callsCleaner 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.

Modifying

These functions return a new string with some transformation applied. Strings are immutable in Go, so none of these touch the input.

Replace and ReplaceAll

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.

NewReplacer

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.

*Replacer is also goroutine-safe, so you can stash one in a package-level variable and use it from any goroutine.

ToLower, ToUpper, ToTitle

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.

Trim Family

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.

FunctionRemovesAnchored?
TrimSpace(s)Leading/trailing Unicode whitespaceBoth ends
Trim(s, cutset)Any rune in cutsetBoth ends
TrimLeft(s, cutset)Any rune in cutsetLeft only
TrimRight(s, cutset)Any rune in cutsetRight only
TrimPrefix(s, prefix)Exact substring prefixLeft only
TrimSuffix(s, suffix)Exact substring suffixRight 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.

Repeat

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 +=.

Map

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.

Comparing

Comparison functions answer "are these two strings equivalent in some sense?" with more nuance than ==.

EqualFold

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.

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.

Compare

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.

Misc

A few strings functions don't fit a category but come up often enough to mention.

NewReader

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.

Builder Mention

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 Quick Reference

FunctionReturnsUse 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)boolAnchored prefix/suffix test
Index(s, sub) / LastIndex(s, sub)intFirst/last byte offset of sub, or -1
IndexByte(s, c) / IndexRune(s, r)intFast single-character search
Count(s, sub)intNumber of non-overlapping occurrences
Split(s, sep) / SplitN(s, sep, n)[]stringMulti-piece split
SplitAfter(s, sep)[]stringLike Split but keeps separators attached
Fields(s)[]stringSplit on runs of whitespace, drop empties
Join(elems, sep)stringConcatenate 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)stringSubstring replacement
NewReplacer(pairs...)*ReplacerMulti-pair replacement, one pass
ToLower(s) / ToUpper(s) / ToTitle(s)stringCase conversion
TrimSpace(s)stringStrip leading/trailing whitespace
Trim(s, cutset) / TrimLeft/Right(s, cutset)stringStrip any rune in cutset
TrimPrefix(s, p) / TrimSuffix(s, p)stringStrip exact prefix/suffix if present
Repeat(s, count)stringConcatenate s with itself n times
Map(f, s)stringTransform every rune
EqualFold(s, t)boolCase-insensitive equality
Compare(s, t)intThree-way comparison (prefer ==, <, >)
NewReader(s)*ReaderWrap a string as io.Reader

Putting It Together

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.

Summary

  • 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.