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
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')) }
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
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"}, "-")) }
["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:
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) } }
timeout 30s true false docs/2026 report.final.pdf report.final pdf token: abc123
Changing case and trimming
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")) }
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
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("&", "&", "<", "<", ">", ">") fmt.Println(r.Replace("a < b & c > d")) fmt.Println(strings.Repeat("=", 20)) }
one cat two cat red cat one cat two cat red fish a < b & c > 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:
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)) }
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:
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() }
"first line\n" "second line\n" "third" a b c
Lesson 50 explains how these iterators work.
Quick reference
| Task | Function |
|---|---|
| Contains / starts / ends with | Contains, HasPrefix, HasSuffix |
| Find position | Index, LastIndex, IndexAny, IndexFunc |
| Split | Split, SplitN, Fields, Cut, CutLast, Lines |
| Join, repeat | Join, Repeat |
| Case | ToUpper, ToLower, EqualFold |
| Trim | TrimSpace, Trim, TrimPrefix, TrimSuffix, CutPrefix |
| Replace | ReplaceAll, Replace, NewReplacer, Map |
| Count | Count |
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.
getting-started-with-go-127 maps-slices-structs
strconv.