The strings Package

The standard library's strings package has a function for nearly every everyday text task: searching, splitting, joining, trimming, replacing, changing case and more. Because strings are immutable, every function returns a new string and leaves the original alone. This lesson tours the functions you'll reach for most.

Searching

GO
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "the cat sat on the mat"

	fmt.Println(strings.Contains(s, "cat"), strings.ContainsAny(s, "xyz"))
	fmt.Println(strings.HasPrefix(s, "the"), strings.HasSuffix(s, "hat"))
	fmt.Println(strings.Index(s, "the"), strings.LastIndex(s, "the"), strings.Index(s, "dog"))
	fmt.Println(strings.Count(s, "at"), strings.IndexByte(s, 'o'))
}
Output
true false
true false
0 15 -1
3 12

Index returns the byte position of the first match, or -1 when there's none. All these searches are case-sensitive.

Splitting and joining

GO
package main

import (
	"fmt"
	"strings"
)

func main() {
	csv := "red,green,,blue"
	parts := strings.Split(csv, ",")
	fmt.Printf("%q %d\n", parts, len(parts))

	fmt.Printf("%q\n", strings.SplitN("a=b=c", "=", 2))
	fmt.Printf("%q\n", strings.Fields("  lots   of\tspace\n"))

	fmt.Println(strings.Join([]string{"2026", "10", "08"}, "-"))
}
Output
["red" "green" "" "blue"] 4
["a" "b=c"]
["lots" "of" "space"]
2026-10-08

Split keeps empty pieces between two separators; Fields splits on any run of whitespace and never returns empty pieces, which makes it the right choice for splitting words.

Cut: split in two

When you need the part before and after a separator, Cut is cleaner than Index plus slicing. CutLast (new in Go 1.27) does the same around the last occurrence, and CutPrefix/CutSuffix remove a known start or end:

GO
package main

import (
	"fmt"
	"strings"
)

func main() {
	key, value, ok := strings.Cut("timeout=30s", "=")
	fmt.Println(key, value, ok)

	_, _, ok = strings.Cut("no separator here", "=")
	fmt.Println(ok)

	dir, file, _ := strings.CutLast("docs/2026/report.final.pdf", "/")
	name, ext, _ := strings.CutLast(file, ".")
	fmt.Println(dir, file, name, ext)

	if rest, found := strings.CutPrefix("Bearer abc123", "Bearer "); found {
		fmt.Println("token:", rest)
	}
}
Output
timeout 30s true
false
docs/2026 report.final.pdf report.final pdf
token: abc123

Changing case and trimming

GO
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.ToUpper("straße"), strings.ToLower("HeLLo"))

	raw := "  \t hello world \n"
	fmt.Printf("%q\n", strings.TrimSpace(raw))
	fmt.Printf("%q\n", strings.Trim("--==title==--", "-="))
	fmt.Printf("%q\n", strings.TrimLeft("000123", "0"))
	fmt.Printf("%q\n", strings.TrimPrefix("www.example.com", "www."))
	fmt.Printf("%q\n", strings.TrimSuffix("photo.jpeg", ".jpeg"))
}
Output
STRAßE hello
"hello world"
"title"
"123"
"example.com"
"photo"

ToUpper converts one character at a time, so letters whose capital form is two characters, like the German ß, are left as they are. Note also the difference between the trim functions: Trim, TrimLeft and TrimRight take a set of characters to strip, while TrimPrefix and TrimSuffix remove one exact string. Confusing the two is a common bug — strings.TrimLeft("www.wow.com", "www.") removes every leading w and dot, leaving "ow.com".

Replacing and repeating

GO
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "one fish two fish red fish"
	fmt.Println(strings.ReplaceAll(s, "fish", "cat"))
	fmt.Println(strings.Replace(s, "fish", "cat", 2)) // only the first 2

	r := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
	fmt.Println(r.Replace("a < b & c > d"))

	fmt.Println(strings.Repeat("=", 20))
}
Output
one cat two cat red cat
one cat two cat red fish
a &lt; b &amp; c &gt; d
====================

A Replacer makes several replacements in one pass and can be reused. (For HTML, use the html or html/template packages rather than hand-written escaping.)

Working rune by rune

strings.Map transforms every character with a function; returning a negative value drops it. The unicode package classifies characters:

GO
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	onlyLetters := strings.Map(func(r rune) rune {
		if unicode.IsLetter(r) {
			return unicode.ToLower(r)
		}
		return -1
	}, "Hello, World! 123")
	fmt.Println(onlyLetters)

	fmt.Println(strings.IndexFunc("abc123", unicode.IsDigit))
	fmt.Println(strings.FieldsFunc("a1b22c333", unicode.IsDigit))
}
Output
helloworld
3
[a b c]

Iterating without building a slice

Some functions have …Seq versions that hand you the pieces one at a time in a for … range loop instead of building a whole slice first. strings.Lines yields each line, including its newline:

GO
package main

import (
	"fmt"
	"strings"
)

func main() {
	text := "first line\nsecond line\nthird"
	for line := range strings.Lines(text) {
		fmt.Printf("%q\n", line)
	}
	for part := range strings.SplitSeq("a/b/c", "/") {
		fmt.Print(part, " ")
	}
	fmt.Println()
}
Output
"first line\n"
"second line\n"
"third"
a b c

Lesson 50 explains how these iterators work.

Quick reference

TaskFunction
Contains / starts / ends withContains, HasPrefix, HasSuffix
Find positionIndex, LastIndex, IndexAny, IndexFunc
SplitSplit, SplitN, Fields, Cut, CutLast, Lines
Join, repeatJoin, Repeat
CaseToUpper, ToLower, EqualFold
TrimTrimSpace, Trim, TrimPrefix, TrimSuffix, CutPrefix
ReplaceReplaceAll, Replace, NewReplacer, Map
CountCount

The bytes package has the same functions for []byte, which saves converting back and forth when you're working with raw data.

Try it yourself

Turn a title into a URL "slug": lowercase, words joined with hyphens, punctuation removed.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
getting-started-with-go-127
maps-slices-structs
Next: converting strings to numbers and back with strconv.