Skip to main content

Strings

8 minutes read•Filed underGo Programming Languageon

Deep dive into Go strings — stored as UTF-8 byte sequences, immutable, and fully Unicode-aware. Learn indexing, slicing, conversions, and how to iterate correctly.

What a string is

In Go, a string is a read-only sequence of bytes. It is not a sequence of characters, not a sequence of Unicode code points — it is bytes. The language makes no guarantee about what those bytes represent. By convention, and by default in all Go source code, string data is expected to be valid UTF-8, but the type itself does not enforce that.

Concretely, a string is a two-field data structure: a pointer to the underlying byte array and a length. That is all. No null terminator, no capacity field — just a pointer and a count.

String literals are written with double quotes. Go also supports raw string literals delimited by backticks — these span multiple lines and ignore escape sequences:

Indexing and slicing

Because a string is a byte sequence, index notation gives you a byte — not a character:

You can extract a substring using a slice expression. The syntax is the same as for slices: s[low:high] returns the bytes from index low up to (but not including) index high:

The result is still a string — slicing does not copy the underlying bytes. The new string shares memory with the original.

Strings are immutable

Once created, a string cannot be modified. You can reassign the variable, but you cannot change the bytes the string points to:

This is a deliberate design choice. Because strings are immutable, they are safe to share — multiple variables can point to the same underlying bytes without any risk of one modifying what the other sees. It also means copying a string is cheap: you copy the pointer and length, not the bytes.

To build a modified string, you convert to a mutable type, change it, and convert back:

String, rune, and byte conversions

The three types string, rune, and byte are closely related, and Go allows explicit conversions between them. Each conversion has a specific meaning.

ConversionWhat it does
string(r) where r is a runeCreates a string containing the UTF-8 encoding of that code point
string(b) where b is a byteCreates a one-byte string containing that byte value
string(n) where n is an integerCreates a string with the UTF-8 encoding of code point n
[]byte(s)Copies the string bytes into a new []byte
[]rune(s)Decodes the string as UTF-8 and returns each code point as a rune
rune(b)Widens the byte value to a rune
byte(r)Truncates the rune value to a single byte

You cannot implicitly convert between these types. Attempting to assign a rune or byte value directly to a string variable — or pass one where the other is expected — is a compile error:

UTF-8 and Unicode

Go source files are always UTF-8. String literals in your source code are stored as the UTF-8 encoding of whatever characters you wrote. For ASCII text — letters, digits, punctuation — each character occupies exactly one byte, so indexing by byte and indexing by character are the same thing. For non-ASCII characters, they are not.

UTF-8 is a variable-width encoding. A single Unicode code point (a rune in Go terminology) can take anywhere from 1 to 4 bytes:

  • ASCII characters (U+0000 to U+007F): 1 byte
  • Characters like é, ñ, ü (U+0080 to U+07FF): 2 bytes
  • CJK characters and most of the BMP (U+0800 to U+FFFF): 3 bytes
  • Emoji and supplementary characters (U+10000 and above): 4 bytes

This means len(s) gives you the number of bytes, not the number of characters. For a string with multi-byte characters, these differ:

Indexing gives bytes, not runes

Because a string is a byte sequence, s[i] always gives the byte at position i, not the character at position i. For strings that contain multi-byte characters, this produces the raw byte value — not the rune:

To work with characters rather than bytes, use []rune:

Ranging over a string

The for range loop over a string is aware of UTF-8. It automatically decodes each code point and gives you the rune value along with the byte offset where that rune starts:

Notice that é starts at byte offset 1 and the next character starts at byte offset 3, because é takes 2 bytes. for range handles all of this automatically — it is the idiomatic way to iterate over the characters of a string.

The strings package

The strings package provides the standard toolkit for working with strings. A few of the most commonly used functions:

FunctionWhat it does
strings.Contains(s, substr)Reports whether substr is within s
strings.HasPrefix(s, prefix)Reports whether s starts with prefix
strings.HasSuffix(s, suffix)Reports whether s ends with suffix
strings.Count(s, substr)Counts non-overlapping instances of substr in s
strings.Index(s, substr)Returns the byte index of the first occurrence of substr
strings.Replace(s, old, new, n)Replaces the first n occurrences of old with new; -1 replaces all
strings.ToUpper(s)Returns s converted to uppercase
strings.ToLower(s)Returns s converted to lowercase
strings.TrimSpace(s)Returns s with leading and trailing whitespace removed
strings.Split(s, sep)Splits s into a slice of substrings separated by sep
strings.Join(elems, sep)Joins elements of a slice with sep between each
strings.BuilderEfficient buffer for building strings incrementally

For building strings from many pieces, use strings.Builder instead of concatenation — concatenation with + creates a new string on every operation, while Builder accumulates bytes in a buffer and produces one string at the end: