Start learning free

GO · STRINGS

Intro To Strings in Go, explained

What a string is in Go

A Go string is an immutable sequence of bytes. Usually those bytes hold text in UTF-8, the standard encoding that stores ASCII characters in one byte and other characters in two to four. Two facts follow from that definition and explain most of how strings behave in Go: you can read a string but never change it in place, and its length counts bytes, not visible characters.

s := "hello"
fmt.Println(len(s))       // 5
fmt.Println(s == "hello") // true

Strings are compared with ==, < and the other operators, which compare their contents. They are ordered alphabetically by byte value, so "hello" < "help" is true. The zero value of a string is the empty string "", with length 0. A string variable is never nil.

Literals: interpreted and raw

A double-quoted literal is interpreted: escape sequences such as \n (newline) and \t (tab) are converted. A literal in backticks is raw: nothing is converted, and it can span several lines.

raw := `a\nb`
fmt.Println(raw)     // a\nb
fmt.Println("a\nb")  // a
                     // b

Raw strings are handy for file paths on Windows and for patterns that contain many backslashes.

Indexing gives a byte

Indexing a string with s[i] returns the byte at position i, with type byte (an alias for uint8). It is a number, not a one-character string:

s := "hello"
fmt.Println(s[0])         // 104
fmt.Println(string(s[0])) // h

Wrapping the byte in string(...) turns it into a one-character string, which is right for ASCII. This matters in code such as result += string(s[i]), because a byte can't be added to a string directly. Indexes start at 0, and an index outside 0 to len(s) - 1 stops the program with a run-time panic.

Slicing

The expression s[low:high] gives the substring from low up to, but not including, high. Either bound can be left out.

s := "hello"
fmt.Println(s[1:3]) // el
fmt.Println(s[:2])  // he
fmt.Println(s[2:])  // llo

Slicing doesn't copy the text, because strings can't change, so sharing is safe. The bounds are byte positions, so with non-ASCII text a cut can land in the middle of a character.

Bytes against runes

Take u := "héllo". The letter é occupies two bytes in UTF-8, so the string has 5 characters and 6 bytes. A rune is Go's name for a Unicode code point, and its type is rune, an alias for int32.

u := "héllo"
fmt.Println(len(u))         // 6
fmt.Println(len([]rune(u))) // 5

for i, r := range u {
    fmt.Print(i, ":", string(r), " ")
}
// 0:h 1:é 3:l 4:l 5:o

A for loop with range over a string decodes the text for you. It yields the byte position where each character starts, and the character as a rune. Notice that the positions skip from 1 to 3, because é takes bytes 1 and 2. A plain loop over i < len(u) with u[i] would visit the six raw bytes instead: 104, 195, 169, 108, 108, 111.

A common trap is string(u[1]). Byte 195 alone is half of a character, and converting it gives the character with code point 195, which is Ã, not é. When text may contain non-ASCII characters, use range or convert to []rune.

Strings can't be modified

An assignment such as s[0] = 'J' doesn't compile. To change text, convert to a mutable slice, edit it and convert back. Use []byte for ASCII and []rune for general text:

s := "hello"
b := []byte(s)
b[0] = 'J'
fmt.Println(string(b), s) // Jello hello

r := []rune("héllo")
r[1] = 'e'
fmt.Println(string(r))    // hello

Both conversions copy the data, which is why the original s is untouched.

Building and searching strings

The + operator joins strings, and += builds a new string each time. For a few joins that's fine. To assemble a long string in a loop, use strings.Builder, which appends into a growing buffer:

var sb strings.Builder
for i := 0; i < 3; i++ {
    sb.WriteString("x")
}
fmt.Println(sb.String()) // xxx

The standard strings package, which has to be imported, covers the everyday operations:

  • strings.ToUpper(s) returns HELLO for "hello".
  • strings.Contains(s, "ell") reports whether the text appears, here true.
  • strings.Index("chicken", "ken") returns the byte position, 4. When the text isn't there, as in strings.Index("chicken", "dmr"), it returns -1.
  • strings.Split("a,b,c", ",") gives the slice [a b c], and strings.Join([]string{"a", "b"}, "-") gives a-b.
  • strings.Fields(" a b ") splits on runs of whitespace and gives [a b].
  • strings.TrimSpace(" hi ") removes the whitespace at both ends and gives hi.
  • strings.Repeat("ab", 3) gives ababab.

Each of these returns a new string and leaves the original alone.

Common mistakes

  • Treating s[i] as a string. It is a byte. Comparing it with "a" doesn't compile; compare with the byte literal 'a' instead.
  • Using len as a character count. It counts bytes. Use len([]rune(s)) to count characters.
  • Trying to assign to an index. Strings are immutable. Copy to []byte or []rune.
  • Mixing quote styles. Double quotes make a string and single quotes make a single rune. 'ab' is an error.
  • Going out of range. Reading s[len(s)] or slicing beyond the length panics. Check the length first, especially for empty strings.

Key takeaways

  • A Go string is an immutable sequence of bytes, normally UTF-8 text.
  • len(s) counts bytes, s[i] is a byte, and s[a:b] slices by byte position.
  • for i, r := range s walks characters (runes); []rune(s) gives a slice you can index by character.
  • Change text by converting to []byte or []rune; build long strings with strings.Builder.
  • The strings package provides searching, splitting, joining and trimming.
Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
Start learning free