Strings #
Text manipulation is one of the most fundamental needs in almost every program. Go provides the strings package in the standard library with more than 40 ready-to-use functions for working with strings — from simple searching, letter transformations, and text splitting, to mass content replacement. Understanding this package well will keep you from rewriting logic that already exists, while ensuring your code is idiomatic and performant. This article covers all function groups in strings, when to use each one, and common pitfalls to avoid.
Import and Basic Usage #
The strings package is part of the Go standard library — no installation needed, just import and use it. All its functions work with Go’s built-in string type, which is immutable (can’t be changed in place), so every function always returns a new string.
import (
"fmt"
"strings"
)
func main() {
s := "Hello, Golang!"
fmt.Println(strings.ToUpper(s)) // "HELLO, GOLANG!"
fmt.Println(strings.Contains(s, "Go")) // true
fmt.Println(strings.Count(s, "l")) // 2
}
All functions in strings are safe to use concurrently — there’s no internal state being modified. You can call these functions from many goroutines at once without any risk of race conditions.Checking String Content #
This group of functions is used to check whether a string contains, starts with, or ends with specific text. These are among the most needed operations, for example when validating input, checking file extensions, or filtering data.
Contains and ContainsAny #
strings.Contains checks whether a substring exists inside a string. strings.ContainsAny checks whether the string contains at least one character from a given set of characters.
s := "golang programming"
// Contains — check for a substring
fmt.Println(strings.Contains(s, "golang")) // true
fmt.Println(strings.Contains(s, "python")) // false
fmt.Println(strings.Contains(s, "")) // true (an empty string is always present)
// ContainsAny — check for any one character
fmt.Println(strings.ContainsAny(s, "aeiou")) // true (contains vowels)
fmt.Println(strings.ContainsAny(s, "xyz")) // false
// ContainsRune — check for one specific rune
fmt.Println(strings.ContainsRune(s, 'g')) // true
HasPrefix and HasSuffix #
These two functions are very useful for checking beginnings and endings — for example checking URL protocols, file extensions, or message formats.
url := "https://api.example.com/v1/users"
filename := "report_2024.pdf"
fmt.Println(strings.HasPrefix(url, "https://")) // true
fmt.Println(strings.HasPrefix(url, "http://")) // false
fmt.Println(strings.HasSuffix(filename, ".pdf")) // true
fmt.Println(strings.HasSuffix(filename, ".csv")) // false
// Common pattern: validation and processing
func processURL(url string) error {
if !strings.HasPrefix(url, "https://") {
return fmt.Errorf("only HTTPS is supported, got: %s", url)
}
// continue processing...
return nil
}
EqualFold — Case-Insensitive Comparison #
To compare two strings without regard to letter case, use EqualFold — far more efficient than converting both to lowercase and then comparing.
// ANTI-PATTERN: unnecessary conversion before comparing
if strings.ToLower(input) == strings.ToLower("admin") {
// ...
}
// CORRECT: use EqualFold directly
if strings.EqualFold(input, "admin") {
// ...
}
fmt.Println(strings.EqualFold("Go", "go")) // true
fmt.Println(strings.EqualFold("Go", "GO")) // true
fmt.Println(strings.EqualFold("Go", "Java")) // false
Searching and Positions #
When you need to know not just whether a substring exists, but where it is, use the Index function group. These functions return the byte index (not the Unicode character index) of the first or last occurrence of a substring.
Index, LastIndex, and IndexAny #
s := "go is a fast go language"
// Index — the position of the first occurrence
fmt.Println(strings.Index(s, "go")) // 0
fmt.Println(strings.Index(s, "python")) // -1 (not found)
// LastIndex — the position of the last occurrence
fmt.Println(strings.LastIndex(s, "go")) // 16
// IndexAny — the position of the first character matching any in the set
fmt.Println(strings.IndexAny(s, "aeiou")) // 3 (the 'a' character at position 3)
// IndexByte — search for a single byte (faster than Index for one character)
fmt.Println(strings.IndexByte(s, 'a')) // 3
// IndexRune — search for a specific Unicode rune
fmt.Println(strings.IndexRune(s, 'ā')) // -1
Pattern: Simple Parsing with Index #
Combining Index with string slicing is a very common pattern for parsing simple formats without regex.
func parseKV(kv string) (string, string, bool) {
idx := strings.Index(kv, "=")
if idx == -1 {
return "", "", false
}
return kv[:idx], kv[idx+1:], true
}
key, val, ok := parseKV("name=golang")
fmt.Println(key, val, ok) // "name" "golang" true
key, val, ok = parseKV("invalid-format")
fmt.Println(key, val, ok) // "" "" false
Letter and Whitespace Transformations #
The most common text transformations — changing capitalization and cleaning up whitespace — are all directly available in the strings package.
Capitalization #
s := " Hello World golang "
fmt.Println(strings.ToUpper(s)) // " HELLO WORLD GOLANG "
fmt.Println(strings.ToLower(s)) // " hello world golang "
fmt.Println(strings.Title(s)) // Deprecated in Go 1.18+
// For proper Unicode title case, use golang.org/x/text
// strings.Title still works but doesn't handle Unicode perfectly
strings.Titlehas been deprecated since Go 1.18 because it doesn’t correctly handle Unicode title case rules. For applications requiring internationalization, use thegolang.org/x/text/casespackage instead.
Trim — Removing Characters from the Edges of a String #
Go provides several Trim variants for different needs:
| Function | Behavior |
|---|---|
strings.TrimSpace(s) | Removes whitespace (spaces, tabs, newlines) from left and right |
strings.Trim(s, cutset) | Removes cutset characters from left and right |
strings.TrimLeft(s, cutset) | Removes cutset characters only from the left |
strings.TrimRight(s, cutset) | Removes cutset characters only from the right |
strings.TrimPrefix(s, prefix) | Removes prefix if the string starts with it |
strings.TrimSuffix(s, suffix) | Removes suffix if the string ends with it |
strings.TrimFunc(s, f) | Removes edge characters satisfying function f |
s := " golang "
// TrimSpace — the most commonly used
fmt.Println(strings.TrimSpace(s)) // "golang"
// Trim with a character cutset
fmt.Println(strings.Trim("***golang***", "*")) // "golang"
fmt.Println(strings.TrimLeft("***golang***", "*")) // "golang***"
// TrimPrefix and TrimSuffix — only remove on an exact match
path := "/api/v1/users"
fmt.Println(strings.TrimPrefix(path, "/api")) // "/v1/users"
fmt.Println(strings.TrimSuffix(path, "users")) // "/api/v1/"
// ANTI-PATTERN: using Trim to remove a specific prefix/suffix
// This can produce unexpected results because Trim works per-character
fmt.Println(strings.Trim("/api/v1/", "/")) // "api/v1" — all '/' at the edges removed
// ✓ Use TrimPrefix / TrimSuffix for definite prefixes/suffixes
Map — Per-Character Transformations #
strings.Map lets you transform every character in a string with a custom function. Returning -1 from the function removes that character.
// Remove all digits from a string
removeDigits := func(r rune) rune {
if r >= '0' && r <= '9' {
return -1 // remove this character
}
return r
}
fmt.Println(strings.Map(removeDigits, "go1.21.0")) // "go.."
// Simple encryption — shift each letter by one position
rot1 := func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
return 'a' + (r-'a'+1)%26
case r >= 'A' && r <= 'Z':
return 'A' + (r-'A'+1)%26
}
return r
}
fmt.Println(strings.Map(rot1, "Hello")) // "Ifmmp"
Splitting Strings (Split) #
Splitting a string by a delimiter is one of the most frequently used operations — reading CSV lines, processing CLI arguments, parsing configuration, and much more.
Split and SplitN #
data := "apple,orange,mango,banana"
// Split — split by a separator, producing all parts
parts := strings.Split(data, ",")
fmt.Println(parts) // ["apple" "orange" "mango" "banana"]
fmt.Println(len(parts)) // 4
// SplitN — split into at most N parts
parts2 := strings.SplitN(data, ",", 2)
fmt.Println(parts2) // ["apple" "orange,mango,banana"]
// Split with an empty string — splits into individual runes
chars := strings.Split("golang", "")
fmt.Println(chars) // ["g" "o" "l" "a" "n" "g"]
// Edge case: the separator doesn't exist
fmt.Println(strings.Split("golang", ",")) // ["golang"] — a one-element slice
SplitAfter — Split But Keep the Separator #
Unlike Split, SplitAfter includes the separator at the end of each result element. Useful when you want to keep the delimiter as part of the token.
sentence := "This is the first sentence. This is the second sentence. This is the third sentence."
parts := strings.SplitAfter(sentence, ". ")
for _, p := range parts {
fmt.Printf("%q\n", p)
}
// "This is the first sentence. "
// "This is the second sentence. "
// "This is the third sentence."
Fields — Split by Whitespace #
strings.Fields is the easiest way to split a string by whitespace (including double spaces, tabs, newlines) — the result is already clean with no empty elements.
// ANTI-PATTERN: Split with " " doesn't handle double spaces
parts := strings.Split(" golang is awesome ", " ")
fmt.Println(parts) // ["" "" "golang" "" "is" "" "awesome" "" ""]
// There are many unwanted empty strings
// CORRECT: use Fields for whitespace
words := strings.Fields(" golang is awesome ")
fmt.Println(words) // ["golang" "is" "awesome"]
fmt.Println(len(words)) // 3
// Very useful for processing user input
func parseCommand(input string) []string {
return strings.Fields(strings.TrimSpace(input))
}
FieldsFunc — Split with a Custom Condition #
// Split by non-letter characters
isNotLetter := func(r rune) bool {
return !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z'))
}
words := strings.FieldsFunc("golang,is;the.best!", isNotLetter)
fmt.Println(words) // ["golang" "is" "the" "best"]
Joining Strings (Join and Repeat) #
The opposite of Split is Join. For building a string from many small pieces repeatedly, Go provides efficient functions.
Join #
strings.Join combines a slice of strings with a separator. This is the recommended way to build a string from many parts.
fruits := []string{"apple", "orange", "mango"}
// Join with a separator
fmt.Println(strings.Join(fruits, ", ")) // "apple, orange, mango"
fmt.Println(strings.Join(fruits, " - ")) // "apple - orange - mango"
fmt.Println(strings.Join(fruits, "")) // "appleorangemango"
// Common pattern: building a query or path
segments := []string{"api", "v1", "users", "123"}
path := "/" + strings.Join(segments, "/")
fmt.Println(path) // "/api/v1/users/123"
Repeat #
strings.Repeat repeats a string N times. Useful for making padding, visual separators, or simple templates.
fmt.Println(strings.Repeat("=", 40)) // "========================================"
fmt.Println(strings.Repeat("Go! ", 3)) // "Go! Go! Go! "
fmt.Println(strings.Repeat("-", 0)) // "" (zero repetitions = empty string)
// Pattern: create padding for neat output
func printTitle(title string) {
width := 50
border := strings.Repeat("=", width)
fmt.Println(border)
fmt.Println(title)
fmt.Println(border)
}
Content Replacement (Replace and ReplaceAll) #
Replacing substrings in text is a very common operation — input sanitization, simple templates, data normalization, and so on.
Replace and ReplaceAll #
s := "golang golang golang is the best golang choice"
// Replace — replace the first N occurrences
fmt.Println(strings.Replace(s, "golang", "Go", 1)) // "Go golang golang is the best golang choice"
fmt.Println(strings.Replace(s, "golang", "Go", 2)) // "Go Go golang is the best golang choice"
fmt.Println(strings.Replace(s, "golang", "Go", -1)) // replace all (same as ReplaceAll)
// ReplaceAll — replace every occurrence (more expressive)
fmt.Println(strings.ReplaceAll(s, "golang", "Go"))
// "Go Go Go is the best Go choice"
// ReplaceAll with an empty string = remove the substring
clean := strings.ReplaceAll("<b>golang</b>", "<b>", "")
clean = strings.ReplaceAll(clean, "</b>", "")
fmt.Println(clean) // "golang"
NewReplacer — Replace Many Pairs at Once #
To replace many different patterns in a single string, strings.NewReplacer is far more efficient than calling ReplaceAll repeatedly because it only does a single scan.
// ANTI-PATTERN: many ReplaceAll calls = many string scans
template := "Hello, {name}! Welcome to {city}."
result := strings.ReplaceAll(template, "{name}", "Budi")
result = strings.ReplaceAll(result, "{city}", "Jakarta")
// CORRECT: use NewReplacer for many replacements
r := strings.NewReplacer(
"{name}", "Budi",
"{city}", "Jakarta",
"{year}", "2024",
)
fmt.Println(r.Replace(template))
// "Hello, Budi! Welcome to Jakarta."
// The Replacer is reusable (thread-safe)
message1 := r.Replace("Welcome, {name}!")
message2 := r.Replace("{name} from {city}")
flowchart LR
A[Input String] --> B{How many\npatterns to replace?}
B -- "1 pattern" --> C[ReplaceAll]
B -- "2+ different patterns" --> D[NewReplacer]
C --> E[Output String]
D --> ECounting and Repeating #
Count #
strings.Count counts how many times a substring appears in a string — non-overlapping.
s := "golang is a great language for backend"
fmt.Println(strings.Count(s, "a")) // 9
fmt.Println(strings.Count(s, "golang")) // 1
fmt.Println(strings.Count(s, "")) // len(s)+1 = number of runes + 1
// Counting lines in text
text := "first line\nsecond line\nthird line"
lineCount := strings.Count(text, "\n") + 1
fmt.Println(lineCount) // 3
// Check whether a string contains exactly N occurrences
func exactly(s, sub string, n int) bool {
return strings.Count(s, sub) == n
}
strings.Builder — Building Strings Efficiently #
When you need to build a string iteratively (for example in a loop), don’t use the + or += operators repeatedly because every operation allocates a new string.
// ANTI-PATTERN: concatenation in a loop = many allocations
result := ""
for i := 0; i < 1000; i++ {
result += fmt.Sprintf("line %d\n", i) // 1000 new string allocations!
}
// CORRECT: use strings.Builder
var sb strings.Builder
for i := 0; i < 1000; i++ {
fmt.Fprintf(&sb, "line %d\n", i) // write to an internal buffer
}
result := sb.String() // only convert to a string at the end
The strings.Builder API #
var sb strings.Builder
// Writing to the builder
sb.WriteString("Hello")
sb.WriteRune(',')
sb.WriteByte(' ')
sb.WriteString("Golang!")
fmt.Println(sb.String()) // "Hello, Golang!"
fmt.Println(sb.Len()) // 13
// Reset for reuse
sb.Reset()
fmt.Println(sb.Len()) // 0
// With Grow — pre-allocate if you know the final size
var sb2 strings.Builder
sb2.Grow(512) // allocate 512 bytes up front
sequenceDiagram
participant Code
participant Builder
participant Memory
Code->>Builder: sb.Grow(512)
Builder->>Memory: Allocate a 512-byte buffer
Code->>Builder: sb.WriteString("Hello")
Builder->>Memory: Write to the buffer (no new allocation)
Code->>Builder: sb.WriteString(", Golang!")
Builder->>Memory: Write to the buffer (no new allocation)
Code->>Builder: sb.String()
Builder-->>Code: Final string (one conversion)strings.Reader — Reading a String as an io.Reader #
strings.NewReader converts a string into an io.Reader. This is very useful when a function expects an io.Reader as input, for example for testing or passing data to an API that requires a stream.
import (
"io"
"strings"
"fmt"
)
func processReader(r io.Reader) {
data, _ := io.ReadAll(r)
fmt.Println(string(data))
}
// Turn a string directly into a Reader
reader := strings.NewReader("this is the string content")
processReader(reader) // "this is the string content"
// Useful for testing functions that accept an io.Reader
func TestUpload(t *testing.T) {
body := strings.NewReader(`{"name": "golang", "version": "1.21"}`)
// send the body to a function that reads an io.Reader...
}
// The Reader also supports Seek
reader.Seek(0, io.SeekStart) // back to the beginning
Real-World Usage Patterns #
Understanding each function individually is important, but knowing how to combine them in real contexts is far more valuable. Here are some patterns very commonly found in production applications.
Normalizing User Input #
func normalizeInput(input string) string {
// 1. Remove edge whitespace
s := strings.TrimSpace(input)
// 2. Convert to lowercase for consistency
s = strings.ToLower(s)
// 3. Replace double spaces with a single space
s = strings.Join(strings.Fields(s), " ")
return s
}
fmt.Println(normalizeInput(" HELLO WORLD ")) // "hello world"
fmt.Println(normalizeInput("\tGolang\n")) // "golang"
Parsing Simple HTTP Headers #
func parseHeader(header string) (string, string) {
// "Content-Type: application/json"
idx := strings.Index(header, ": ")
if idx == -1 {
return header, ""
}
return strings.TrimSpace(header[:idx]),
strings.TrimSpace(header[idx+2:])
}
key, val := parseHeader("Content-Type: application/json")
fmt.Printf("Key: %q, Value: %q\n", key, val)
// Key: "Content-Type", Value: "application/json"
Creating a URL Slug from a Title #
func makeSlug(title string) string {
// Lowercase first
s := strings.ToLower(title)
// Replace spaces with hyphens
s = strings.ReplaceAll(s, " ", "-")
// Remove unwanted characters
var sb strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
sb.WriteRune(r)
}
}
// Remove double hyphens
result := sb.String()
for strings.Contains(result, "--") {
result = strings.ReplaceAll(result, "--", "-")
}
return strings.Trim(result, "-")
}
fmt.Println(makeSlug("Complete Go 1.21 Guide!"))
// "complete-go-121"
Building a Query String #
func buildQueryString(params map[string]string) string {
parts := make([]string, 0, len(params))
for k, v := range params {
parts = append(parts, k+"="+v)
}
return strings.Join(parts, "&")
}
qs := buildQueryString(map[string]string{
"page": "1",
"limit": "10",
"sort": "created_at",
})
fmt.Println(qs) // "limit=10&page=1&sort=created_at" (map order not guaranteed)
When to Switch to Alternatives #
The strings package is already very complete for everyday needs. However, there are situations where you need to switch to another approach:
Keep using the strings package if:
✓ Searching, splitting, or joining strings with fixed patterns
✓ Letter transformations and whitespace cleaning
✓ Substring replacement with literal patterns
✓ Building strings iteratively (use strings.Builder)
✓ Converting a string into an io.Reader for testing
Consider the regexp package if:
✗ Complex, dynamic search patterns (like email validation, date formats)
✗ Replacement based on capture groups (e.g. changing "2024-01-15" to "15/01/2024")
✗ Searches with conditions that can't be expressed as a string literal
Consider golang.org/x/text if:
✗ You need deep Unicode handling (normalization, collation)
✗ Correct title case for various languages
✗ Locale-aware string comparison
Consider strconv if:
✗ Converting between strings and numeric types (int, float, bool)
✗ Parsing and formatting numbers with specific formats
Summary #
strings.Contains,HasPrefix,HasSuffix— check for substring existence; useEqualFoldfor case-insensitive comparisons without manual conversion.strings.IndexandLastIndex— return the first or last byte position of a substring; return -1 if not found.strings.TrimSpaceandstrings.Fields— the two most important functions for cleaning and splitting user input;Fieldsautomatically handles double spaces.strings.Splitvsstrings.Fields— useSplitwhen the separator is fixed and definite; useFieldswhen splitting by any whitespace.strings.NewReplacer— more efficient than a chain ofReplaceAllcalls because it only scans once; safe for concurrent use.strings.Builder— required when building strings in a loop; avoid repeated+concatenation, which creates many memory allocations.strings.NewReader— the idiomatic way to adapt a string into anio.Reader; very useful for testing functions that accept streams.- All
stringsfunctions are immutable — none modify the original string; every function always returns a new value.