Skip to content
Theory · Strings · lesson 1 of 8

Strings and bytes

What you'll learn

What you'll learn:

  • explain why len("привет") returns 12, not 6;
  • tell a byte from a rune and walk through a string character by character;
  • build a new string from parts, since the old one cannot change;
  • search, split and join strings with the functions of the strings package.
Two letter tiles G and o from a word game, each with its code printed on it, 71 and 111

Picture a row of letter tiles, like the tiles of a word board game. Each tile is one byte: a small number from 0 to 255, and the letter on it is only a way to show that number. A string in Go is built like that row: bytes that hold the text. A Latin letter, a digit or a space takes one tile, and a Cyrillic letter takes two.

This picture leads to two surprises. You can index a string in Go like a slice: s[0] is its first element. But the result looks unexpected:

  • the first element of the string "Go" prints as a number;
  • the length of "Привет" is twice its number of letters.
GO
package main

import "fmt"

func main() {
    s := "Go"
    fmt.Println(len(s), s[0])  // 2 71
    fmt.Println(len("Привет")) // 12
}

Let's walk through the output:

  1. len(s) returns 2: "Go" has two Latin letters, one tile each.
  2. s[0] returns 71, the number on the first tile.
  3. len("Привет") returns 12: the word has six letters of two tiles each.

A string of bytes

A string in Go holds a sequence of bytes that store the text in the UTF-8 encoding. An encoding is the rule that turns each letter into bytes. In UTF-8, a Latin letter, a digit or a space takes one byte, and a Cyrillic letter takes two.

The len function returns the number of bytes, not letters: it counts tiles. That is why the string "Привет" has six letters and twelve bytes.

Interviewers like to ask: "What does len("привет") return?" The right answer is 12, because len on a string always counts bytes.

An index returns a byte

The index s[i] returns one byte, a value of type byte. Here is what follows from that:

  • the type byte is another name for uint8, an integer from 0 to 255;
  • so fmt.Println prints not the letter G but its code 71;
  • you compare a byte with a letter using single quotes: s[0] == 'G' gives true.

Press the button to read "Go Привет" byte by byte, as s[i] does, and watch how many bytes a Latin letter, a space and a Cyrillic letter take.

LiveA string: UTF-8 byteslen(s) = 15 · characters: 9

A string holds UTF-8 bytes. Press the button to read it one byte at a time, the way s[i] does.

Cells are bytes written in hexadecimal

The index bounds are the same as for a slice:

  • indexes run from 0 to len(s) - 1;
  • an index past the end causes a panic;
  • the empty string "" has length 0, and any index into it panics.

A string cannot change

Strings in Go are immutable: the bytes of a created string never change. Here the tile picture stops working: you can take a real tile out and replace it, but not a byte of a string.

What you can and cannot do:

  • you cannot assign a new value to the element s[i];
  • you can assign a whole different string to the variable;
  • a new string is built with the + operator from parts of the old one.

A new string from parts

GO
s := "Go"
t := "g" + s[1:]
fmt.Println(t, s) // go Go

Let's walk through the example line by line:

  1. s := "Go" creates a string of two bytes.
  2. t := "g" + s[1:] builds a new string. The expression s[1:] takes a part of the string by the same rules as slicing a slice, and returns a string. The bytes from index 1 to the end go into t together with the letter "g".
  3. fmt.Println(t, s) prints go Go. The string s itself did not change: the + operator creates a new string from the bytes of both parts.

The bounds in s[1:] are counted in bytes too, not in letters. For "Go" that is the same thing. But "Привет"[1:] starts in the middle of the letter П: such a slice cuts the letter in half, and printing shows a garbled sign in its place.

Copying and comparing

Two operations work on a string as a whole:

  • the assignment t := s does not copy bytes: both variables see the same text, and neither can spoil it;
  • the == operator compares two strings: it compares their bytes in order.

Common mistake

The first letter of a string is replaced by assigning through an index:

GO
s := "hello"
s[0] = 'H' // ✗
TEXT
cannot assign to s[0] (neither addressable nor a map index expression)

The compiler rejects the assignment because the string is immutable, and the program does not build. Build a new string from parts instead: s = "H" + s[1:].

Key points

  • A string is a row of bytes in UTF-8; len counts bytes, so len("привет") is 12.
  • s[i] returns one byte of type byte: a number from 0 to 255, not a letter.
  • Slicing a string, as in s[1:], also cuts by bytes and can split a letter in half.
  • A string cannot change: s[0] = 'H' does not compile, and a new string is built from parts with +.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way