Compare Strings in Golang: 8 Methods and When to Use Them
Tutorials

Justas Vitaitis
Key Takeaways
-
Go strings are immutable byte slices, and modifying them allocates new memory. Use strings.Builder when joining multiple strings to avoid unnecessary memory overhead.
-
To count characters in Unicode strings accurately without memory bloat, use the utf8.RuneCountInString function instead of full rune conversions.
-
Go automatically checks string lengths before comparing the underlying bytes, making == the fastest way to check for exact matches.
String comparison in Go might seem trivial, but picking the wrong method can hurt performance. Some built-in functions have known performance bottlenecks by design.
We’ll cover the fastest methods for different use cases and provide live testing links so you can check how it works yourself.
What Is a String in Go?
Go strings are immutable slices of arbitrary bytes. It’s a read-only design that makes them inherently safe to share across goroutines. You cannot modify a string in place: assigning to an index, as in s[0] = ‘X’, is a compile-time error, so producing a changed version always allocates fresh memory.
The zero value of the string type is an empty string, which behaves like any other string value and simply holds a zero-length run of bytes.
To create string literals, use double quotes for interpreted text that evaluates escape characters, or backticks for raw string literals that can span multiple lines and ignore them. For package-level declarations, you can declare your string literals explicitly with the var keyword, whether they are standard or raw string literals.
Inside functions, use the := short assignment operator to assign a string value from string literals with automatic type inference. Regardless of the syntax you choose for your string literals, whether a standard or raw string, all strings in the Go language share the exact same underlying memory model.
The Best Method to Compare Strings in Golang
Here is a quick summary of the comparison methods available in Go and when you should actually use them:
- Equality operators (==, !=). The absolute fastest option for case-sensitive comparisons.
- strings.EqualFold(). The standard, memory-efficient choice for case-insensitive equality checks.
- strings.Compare(). A case-sensitive function that returns an integer instead of a boolean. Use it when an API needs a three-way result, not for ordinary string comparisons.
- len(). Useful for retrieving a string size in bytes. Do not use it as a manual performance optimization before equality checks, as the == operator already handles length verification automatically.
- strings.Contains(). The standard method for case-sensitive substring matching.
- strings.Contains() paired with strings.ToLower(). A common approach for case-insensitive substring matching, though it carries a performance penalty by allocating fresh memory for the new lowercase strings.
- Inequality operators (>, <, >=, <=). Used to sort strings by lexicographical byte order rather than strict alphabetical order (meaning uppercase letters evaluate before lowercase letters).
- strings.HasPrefix() and strings.HasSuffix(). Case-sensitive checks for the beginning or end of a string, without scanning the whole value.
len()
The len() function returns the size of a string in bytes. It is useful when you need to determine the memory footprint of a string or compare raw byte sizes.
Here is how to use it:
package main
import "fmt"
func main() {
var stringA = "John"
var stringB = "Paul"
if len(stringA) == len(stringB) {
fmt.Println("The size is the same")
} else {
fmt.Println("The size is different")
}
stringA = "Ringo"
stringB = "George"
if len(stringA) == len(stringB) {
fmt.Println("The size is the same")
} else if len(stringA) < len(stringB) {
fmt.Println("A is smaller than B")
} else {
fmt.Println("A is bigger than B")
}
}
Keep in mind that len() counts bytes, not characters. Because Go uses UTF-8 encoding, a single special Unicode character might take up multiple bytes rather than just an individual byte.
Finally, never manually check lengths before comparing strings for equality. The Go compiler already handles length verification automatically under the hood, so manual checks only clutter your code without providing any performance benefit.
Go String Equality Check Using == and !=
The == and != operators check for exact, case-sensitive matches between two string literals. Unlike in some other languages, this is the fastest and most idiomatic way to compare a string value in the Go language.
Never wrap these operators in manual len() checks. The compiler already emits a length comparison in O(1) time before any bytes are examined, which makes manual length checks completely redundant in your own string comparisons.
Case-Insensitive String Comparison
For case-insensitive comparisons, avoid pairing a mapping function like strings.ToLower() with == when comparing string literals.
Converting strings to lowercase forces Go to allocate fresh memory for the new variables. Instead, use strings.EqualFold(), which walks both operands and folds case as it goes, so it returns the same answer without creating new allocations. One caveat: EqualFold applies simple Unicode case folding, so it will not treat the German ß as equal to SS the way a full case-folding library would.
package main
import (
"fmt"
"strings"
)
func main() {
stringA := "John"
stringB := "john"
// Allocates two new lowercase strings before comparing them.
fmt.Println(strings.ToLower(stringA) == strings.ToLower(stringB)) // true
// Compares the string value in place, with no extra allocation.
fmt.Println(strings.EqualFold(stringA, stringB)) // true
// EqualFold applies simple case folding, not full Unicode folding.
fmt.Println(strings.EqualFold("Straße", "STRASSE")) // false
}
, <, >=, <=)">The Inequality Operators (>, <, >=, <=)
Inequality operators evaluate strings based on lexicographical order, which compares the raw byte values of the characters from left to right. This is strictly a byte-by-byte comparison, which behaves differently from standard alphabetical or dictionary sorting.
Because Go compares the underlying UTF-8 byte values, uppercase letters evaluate before lowercase letters since uppercase A has a lower byte value than lowercase a. Similarly, a string starting with 10 evaluates before a string starting with 2 because the byte representing the digit 1 comes before the byte representing the digit 2.
When strings share the same starting characters, Go evaluates the first divergent byte. For example, a space character has a lower byte value than any letter.
package main
import (
"fmt"
)
func main() {
var stringA = "John"
var stringB = "john"
if stringA < stringB {
fmt.Println("John comes before john")
} else {
fmt.Println("John doesn't come before john")
}
stringA = "Johnny"
stringB = "John Lennon"
if stringA < stringB {
fmt.Println("Johnny comes before John Lennon")
} else {
fmt.Println("Johnny doesn't come before John Lennon")
}
stringA = "10 John"
stringB = "2 John"
if stringA < stringB {
fmt.Println("10 John comes before 2 John")
} else {
fmt.Println("10 John doesn't come before 2 John")
}
}
strings.Compare()
The Go strings.Compare() performs a case-sensitive, lexicographical comparison and returns an integer, not a boolean.
Older tutorials describe it as a function that exists only for symmetry with the bytes package, and that used to be accurate. It is no longer. Since Go 1.23, the implementation is a thin wrapper around the same optimized routine the runtime uses for < and >, and the doc comment now points you toward it for three-way comparisons:
import "internal/bytealg"
func Compare(a, b string) int {
return bytealg.CompareString(a, b)
}
The function returns one of three integers based on the result:
- 0 if the strings are identical.
- -1 if the first string comes before the second string lexicographically.
- 1 if the first string comes after the second string lexicographically.
Here is how the return values evaluate in practice:
package main
import (
"fmt"
"strings"
)
func main() {
var stringA = "John"
var stringB = "John"
fmt.Println(strings.Compare(stringA, stringB))
stringA = "John"
stringB = "john"
fmt.Println(strings.Compare(stringA, stringB))
stringA = "Johnny"
stringB = "John Lennon"
fmt.Println(strings.Compare(stringA, stringB))
stringA = "10 John"
stringB = "2 John"
fmt.Println(strings.Compare(stringA, stringB))
}
For everyday equality and ordering work, still reach for the operators directly. The documentation is explicit that ==, <* and *> are usually clearer and always faster, because the compiler can inline them instead of routing through a function call.
Save strings.Compare() for the cases where an API genuinely needs the three-way integer, such as the comparison function you hand to slices.SortFunc().
Both methods operate at O(n) time complexity, but direct operators avoid the extra function call overhead. You can verify this difference with a standard benchmark:
package main
import (
"strings"
"testing"
)
func BenchmarkEqualOperator(b *testing.B) {
a := "The Beatles were: John, Paul, George, Ringo"
c := "The Beatles were: John, Paul, George, Ringo"
for i := 0; i < b.N; i++ {
_ = a == c
}
}
func BenchmarkCompareFunction(b *testing.B) {
a := "The Beatles were: John, Paul, George, Ringo"
c := "The Beatles were: John, Paul, George, Ringo"
for i := 0; i < b.N; i++ {
_ = strings.Compare(a, c)
}
}
Save this block as a file ending in _test.go, for example compare_test.go. Benchmark functions only run through go test, so dropping them into main.go will fail to build.
Note: the -bench flag takes a regular expression. If go test -bench=. reports "no tests to run" on Windows, use the space-separated form instead: go test -bench. as some Windows shells mishandle the =. form, and the pattern never reaches the test binary.
strings.Contains()
The strings.Contains() function checks if a specific substring exists within a larger string, which is useful when parsing raw string literals that span multiple lines. This evaluation is strictly case-sensitive.
The function returns true if the substring is present in the target string value and false otherwise. The syntax follows a simple pattern: strings.Contains(s, substr). The first argument is the full string you want to search, and the second argument is the substring you want to find.
package main
import (
"fmt"
"strings"
)
func main() {
var stringA = "The Beatles were an amazing band"
var stringB = "John"
fmt.Println(strings.Contains(stringA, stringB))
stringA = "The Beatles were: John, Paul, George, Ringo"
stringB = "John"
fmt.Println(strings.Contains(stringA, stringB))
stringA = "The Beatles were: John, Paul, George, Ringo"
stringB = "john"
fmt.Println(strings.Contains(stringA, stringB))
}
Strings.Contains() + strings.ToLower()
The standard strings.Contains() function is strictly case-sensitive. To perform a case-insensitive substring search, you must convert both strings to lowercase before running the comparison.
Keep in mind that calling strings.ToLower() allocates fresh memory for the newly generated lowercase strings. It creates a performance penalty that can add up quickly if you run this check inside a large loop.
package main
import (
"fmt"
"strings"
)
func main() {
var stringA = "The Beatles were an amazing band"
var stringB = "John"
fmt.Println(strings.Contains(strings.ToLower(stringA), strings.ToLower(stringB)))
stringA = "The Beatles were: John, Paul, George, Ringo"
stringB = "John"
fmt.Println(strings.Contains(strings.ToLower(stringA), strings.ToLower(stringB)))
stringA = "The Beatles were: John, Paul, George, Ringo"
stringB = "john"
fmt.Println(strings.Contains(strings.ToLower(stringA), strings.ToLower(stringB)))
}
Using strings.HasPrefix() and strings.HasSuffix()
When you only need to verify the beginning or end of a string, use strings.HasPrefix() or strings.HasSuffix(). Both functions evaluate exact, case-sensitive matches and are highly optimized for validating inputs or filtering logic.
Note that checking for an empty string as a prefix or suffix always evaluates to true, even if the target string itself is empty.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.HasPrefix("GoLang Rocks", "Go")) // true
fmt.Println(strings.HasSuffix("GoLang Rocks", "Rocks")) // true
fmt.Println(strings.HasPrefix("GoLang Rocks", "rocks")) // false (case-sensitive)
fmt.Println(strings.HasSuffix("GoLang Rocks", "Lang")) // false
fmt.Println(strings.HasPrefix("", "")) // true
fmt.Println(strings.HasSuffix("", "")) // true
fmt.Println(strings.HasPrefix("A", "")) // true
fmt.Println(strings.HasSuffix("A", "")) // true
}
UTF-8, Bytes, and Runes
Go strings are sequences of arbitrary bytes, conventionally encoded in UTF-8. Because of this design, the built-in len() function returns the total byte count rather than the visual character count. Multi-byte characters, such as emojis or non-English letters, span multiple bytes in memory.
Indexing a string with s[i] yields individual bytes, not characters, so on international text it hands you a fragment of a multi-byte sequence rather than the letter you expected. Slicing on those offsets produces invalid UTF-8. To process text safely, use a range loop, which decodes one UTF-8-encoded rune per iteration.
A rune is an alias for int32, and runes in Go are aliases for the Unicode code points defined in the Unicode standard. Because UTF-8 is a variable-width encoding, code points take one to four bytes each: ASCII code points fit in a single byte, while most emoji code points need four.
For advanced parsing of a complex raw string, you might use the standard utf8 package or regular expressions to decode byte sequences safely.
String Conversions
Casting the immutable string type to a byte slice using []byte(str) creates a mutable copy of the data, which allocates new memory. Converting that byte slice back to the string type with string(byteSlice) forces yet another allocation to maintain the strict read-only rules of Go strings, a design choice not always seen in other languages.
To process individual characters, never convert a full string into a rune slice using []rune(str) just to iterate over it as it forces a massive and unnecessary memory allocation. Instead, run a standard range loop directly on the string itself. The Go runtime automatically decodes the underlying UTF-8 bytes into runes on the fly with zero allocation overhead.
Always minimize type conversions inside tight loops. Repeatedly casting large strings creates heavy memory churn and quickly overwhelms the garbage collector.
String Concatenation
The + operator works perfectly for combining a few variables on a single line. However, using the + operator inside a loop destroys performance. Since Go strings are strictly read-only, every loop iteration allocates a completely new string in memory and copies the data over.
For repetitive concatenation, use strings.Builder since it manages a dynamic byte buffer to minimize allocations. You append string literals or a raw string using its WriteString method.
When you finish, calling the String method returns the final result with zero memory copying. If you know the final byte size in advance, you can call the Grow method beforehand to eliminate intermediate allocations entirely.
If you already have a slice of strings, use strings.Join. It calculates the exact required byte length upfront and builds the final text using exactly one memory allocation.
Conclusion
Choosing the right string comparison method for your string literals in Go is critical for writing efficient, idiomatic code. Stick to the standard == operator for exact matches, use strings.EqualFold() for case-insensitive checks, and avoid unnecessary type conversions or redundant length checks.
To see how these text-processing techniques apply to real-world data extraction, check out our guide on building a Go web scraper .
FAQ
Are strings mutable in Go?
No. Go strings are strictly read-only.
What's the difference between a byte and a rune?
A byte is an alias for uint8. A rune is an alias for int32 that represents a single Unicode code point.
Why does len() return the wrong length for non-English text?
The len() function does not return an incorrect value. It intentionally counts the exact number of bytes in memory. International symbols and emojis simply consume multiple bytes.
How do I get the character count of a Go string?
Use the utf8.RuneCountInString() function.
What's the fastest way to compare strings in Go?
Use the standard == operator and never manually check string lengths first. The Go compiler automatically checks the byte lengths under the hood before evaluating the actual text, making the raw operator the absolute fastest method.