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)returnsHELLOfor"hello".strings.Contains(s, "ell")reports whether the text appears, heretrue.strings.Index("chicken", "ken")returns the byte position, 4. When the text isn't there, as instrings.Index("chicken", "dmr"), it returns-1.strings.Split("a,b,c", ",")gives the slice[a b c], andstrings.Join([]string{"a", "b"}, "-")givesa-b.strings.Fields(" a b ")splits on runs of whitespace and gives[a b].strings.TrimSpace(" hi ")removes the whitespace at both ends and giveshi.strings.Repeat("ab", 3)givesababab.
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
lenas a character count. It counts bytes. Uselen([]rune(s))to count characters. - Trying to assign to an index. Strings are immutable. Copy to
[]byteor[]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, ands[a:b]slices by byte position.for i, r := range swalks characters (runes);[]rune(s)gives a slice you can index by character.- Change text by converting to
[]byteor[]rune; build long strings withstrings.Builder. - The
stringspackage provides searching, splitting, joining and trimming.