How to Convert a String to Uppercase or Lowercase in Go

Use the `strings` package functions `ToUpper` and `ToLower` to convert strings, as Go does not have built-in methods on the `string` type itself.

The normalization problem

You are building a search feature. A user types "Go" into the query box. Your database stores the tag as "go". The query returns zero results. The user assumes the search is broken. It isn't. The case doesn't match.

You need to normalize the input. In JavaScript, you call .toLowerCase(). In Python, you call .lower(). In Go, you reach for the strings package. Go does not attach methods to the string type. The language keeps the string type small and simple, pushing all manipulation logic into a dedicated package. This design choice keeps the core language lean and forces a consistent pattern across the standard library.

Go strings are values, not objects

A Go string is an immutable sequence of bytes. Immutability means the content cannot change after creation. You cannot modify a character at a specific index. You cannot append to a string in place. Every operation that transforms a string returns a brand new string.

This behavior has trade-offs. It prevents accidental mutation bugs. It allows strings to be shared safely across goroutines without locks. It also means case conversion always allocates memory. The runtime creates a new byte slice for the result, copies the transformed data, and hands back a new string header. The original string remains untouched until the garbage collector reclaims it.

Go's strings package handles Unicode correctly out of the box. The functions understand that characters are not single bytes. They iterate over runes, which represent Unicode code points. This prevents the common bug where byte-level manipulation corrupts multi-byte characters.

Minimal example

Here is the standard way to convert case. Import strings and call the function.

package main

import (
	"fmt"
	"strings"
)

func main() {
	// Input contains mixed case and non-ASCII characters.
	input := "Hello, World! δ½ ε₯½"

	// ToUpper returns a new string with all letters mapped to uppercase.
	// The original input remains unchanged.
	upper := strings.ToUpper(input)
	fmt.Println(upper)

	// ToLower returns a new string with all letters mapped to lowercase.
	lower := strings.ToLower(input)
	fmt.Println(lower)
}

The output shows the transformation. The Chinese characters remain identical because they have no case mapping. The ASCII letters flip case. The function handles the entire string in one pass.

Strings don't change. You get a new one.

What happens under the hood

When you call strings.ToUpper, the runtime allocates a new string buffer. The size of the buffer depends on the input. In most cases, the output length matches the input length. The function iterates over the runes in the string. For each rune, it looks up the uppercase mapping in the Unicode data tables. It writes the result to the new buffer.

Case conversion is not always a one-to-one mapping. Some characters expand when converted. The German sharp s, ß, becomes SS in uppercase. The length increases by one byte per character. The strings package handles this expansion automatically. It pre-allocates enough space for the worst-case expansion or grows the buffer dynamically. This is why you cannot do case conversion in place: the result might not fit in the original storage.

The compiler enforces immutability. If you try to modify a string directly, the program fails to compile.

s := "hello"
s[0] = 'H' // Error: cannot assign to s[0]

The compiler rejects this with cannot assign to s[0]. Strings are read-only. You must create a new string if you want different content.

Realistic example: Search indexing

Consider a search indexer that processes a stream of tags. Each tag needs to be normalized to lowercase for storage. Doing this one by one in a loop works, but it generates many small allocations. A better approach uses strings.Builder to accumulate results efficiently.

Here is a function that normalizes a slice of tags and joins them.

package main

import (
	"fmt"
	"strings"
)

// NormalizeTags converts all tags to lowercase and joins them with commas.
// It uses a Builder to minimize allocations during the join operation.
func NormalizeTags(tags []string) string {
	// Builder grows its internal buffer as needed.
	// This avoids creating intermediate strings for each concatenation.
	var b strings.Builder

	for i, tag := range tags {
		// ToLower creates a new string for each tag.
		// This allocation is unavoidable for the transformation itself.
		normalized := strings.ToLower(tag)
		
		// WriteString copies the normalized tag into the builder's buffer.
		b.WriteString(normalized)

		// Add a comma separator between tags, but not after the last one.
		if i < len(tags)-1 {
			b.WriteString(", ")
		}
	}

	// String returns the final content as a single string.
	// The builder's buffer is consumed to create this result.
	return b.String()
}

func main() {
	tags := []string{"Go", "RUST", "Python", "JAVA"}
	result := NormalizeTags(tags)
	fmt.Println(result)
}

The strings.Builder type is designed for efficient string construction. It holds a byte slice that grows as you write. WriteString copies data into the buffer. When you call String, the builder returns a string header pointing to the buffer data. This pattern reduces allocation pressure compared to repeated string concatenation with +.

Builder is for loops, not one-offs.

Pitfalls and compiler errors

Case conversion in Go has several traps. The first is manual byte manipulation. Some developers try to optimize by iterating over bytes and flipping bits. This works for ASCII but breaks on UTF-8.

// BAD: This only works for ASCII.
func badToUpper(s string) string {
	b := []byte(s)
	for i := range b {
		if b[i] >= 'a' && b[i] <= 'z' {
			b[i] -= 32
		}
	}
	return string(b)
}

This code fails on characters like Γ© or Γ±. The byte representation of these characters spans multiple bytes. Modifying individual bytes corrupts the encoding. The result is invalid UTF-8. Always use strings.ToUpper or iterate over runes.

The second trap is strings.Title. This function was deprecated in Go 1.18. It attempted to capitalize the first letter of each word. The implementation relied on a simple "is lower case" check that failed for many Unicode scripts. It also treated any non-letter as a word boundary, which produced unexpected results for punctuation.

// Deprecated: strings.Title uses a simple title casing algorithm
// that does not handle Unicode correctly in all cases.
t := strings.Title("hello world") // "Hello World"

Use strings.ToTitle instead. It converts all letters to title case. It is more consistent, though it still uses the default Unicode case mapping. If you need locale-aware title casing, the standard library does not provide it. You must use the golang.org/x/text/cases package.

The third trap is assuming case conversion is cheap in tight loops. Every call allocates a new string. If you are processing millions of strings, the garbage collector will work hard. Profile your code. If allocation is the bottleneck, consider reusing buffers or processing data in a streaming fashion.

The compiler catches type mismatches. If you try to assign the result of ToUpper to a byte slice, you get an error.

s := "hello"
var b []byte
b = []byte(strings.ToUpper(s)) // Valid, but creates two allocations.

This works, but it allocates a string and then a byte slice. If you need a byte slice, convert the input first, then transform the bytes. Or use strings.ToUpper and convert the result. The cost is usually negligible unless you are in a hot path.

Unicode complexity and locale

Go's strings package uses the default Unicode case mapping. This mapping is language-agnostic. It works for most use cases. It does not handle locale-specific rules.

In Turkish, the letter i has two different uppercase forms. Dotted i becomes Δ°. Dotless Δ± becomes I. The default mapping converts i to I. This is correct for English but wrong for Turkish. If your application serves Turkish users, strings.ToUpper will produce incorrect results for some inputs.

The same issue applies to case folding for comparison. Comparing strings after converting to lowercase is a common pattern. It fails for Turkish I and i. The golang.org/x/text/cases package provides locale-aware conversion. It takes a language.Tag and applies the correct rules.

import "golang.org/x/text/cases"
import "golang.org/x/text/language"

// Turkish case converter.
turkishUpper := cases.Upper(language.Turkish)
result := turkishUpper.String("istanbul") // "Δ°STANBUL"

Using the x/text package adds a dependency. It is heavier than the standard strings package. Use it only when locale correctness is required. For internal identifiers, database keys, or English-only text, the standard library is sufficient.

Unicode breaks simple math. Trust the library.

Decision matrix

Choose the right tool based on your constraints.

Use strings.ToUpper when you need to normalize ASCII or basic Unicode text for comparison or display. Use strings.ToLower when you want case-insensitive matching for English or default Unicode rules. Use strings.ToTitle when you need to capitalize all letters in a string, such as for headers or labels. Use strings.Builder when you are constructing a large string from many parts in a loop to reduce allocation overhead. Use golang.org/x/text/cases when your application must handle locale-specific case rules like Turkish or Lithuanian. Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.

Where to go next