Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Go  Module 4 – Arrays, slices, maps and strings

Strings, bytes and runes: UTF-8 done right

Why len("नमस्ते") is 18 in Go: strings hold UTF-8 bytes, indexing reads bytes, range reads runes, and conversions copy. Count, build and cut text safely.

  • Intermediate
  • 15 minutes
  • Examples run with Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: A string is a read-only sequence of UTF-8 bytes · Indexing reads bytes; range reads runes · Converting to []byte and []rune copies. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Explain how a Go string stores text as read-only UTF-8 bytes, and predict len for text that is not ASCII
  • Iterate over a string by byte and by rune, and count runes with utf8.RuneCountInString
  • Convert between string, []byte and []rune, and predict what each conversion copies
  • Build strings with strings.Builder, and split, cut and compare them with the strings package
  • Limit text to n runes without splitting one, and handle invalid UTF-8 and characters made of several runes

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how a Go string stores text as read-only UTF-8 bytes, and predict len for text that is not ASCII
  • Iterate over a string by byte and by rune, and count runes with utf8.RuneCountInString
  • Convert between string, []byte and []rune, and predict what each conversion copies
  • Build strings with strings.Builder, and split, cut and compare them with the strings package
  • Limit text to n runes without splitting one, and handle invalid UTF-8 and characters made of several runes

Before you start

On this page

len("नमस्ते") is 18 in Go, although the word is written with 6 code point: The number Unicode gives a character, written U+00E9 for é. A Go rune holds one code point, and UTF-8 encodes it in one to four bytes.: Go strings hold bytes, and the UTF-8: The encoding of Go source code and string literals: each Unicode character takes one to four bytes, so 'A' takes one byte and '₹' three. len of a string counts these bytes. More in “Basic types: numbers, strings, bools, conversion” encoding takes three bytes for each of these Devanagari code points. Code that counts, indexes or cuts text by bytes is right for English and wrong for most of the world’s writing.

You will see the bytes of a string, loop over it by byte and by rune: Go's type for one Unicode character, an alias of int32 that holds its code point. A rune such as ₹ takes several bytes in UTF-8, and ranging over a string gives one rune per pass. More in “Control flow: if, for, range and switch”, and convert it to []byte and []rune knowing what each conversion copies. The programs here all ran with Go 1.27.1, and each shows the output that run produced.

A string is a read-only sequence of UTF-8 bytes

The specification defines a string as a sequence of bytes, and Go source files are UTF-8, so a string literal holds the UTF-8 bytes of its text. This program prints four strings with their lengths and their bytes in hex:

bytes.go: four strings, their lengths and their bytes Go · bytes.go
// bytes.go: a string is a sequence of bytes, and UTF-8 uses one to four of them per code point.
package main

import "fmt"

func main() {
	for _, s := range []string{"go", "héllo", "€5", "नमस्ते"} {
		fmt.Printf("%-8q len %2d  bytes % x\n", s, len(s), s)
	}
	s := "héllo"
	fmt.Println(s[0], s[1], s[2]) // indexing gives bytes, as numbers
}

Output

"go"     len  2  bytes 67 6f
"héllo"  len  6  bytes 68 c3 a9 6c 6c 6f
"€5"     len  4  bytes e2 82 ac 35
"नमस्ते" len 18  bytes e0 a4 a8 e0 a4 ae e0 a4 b8 e0 a5 8d e0 a4 a4 e0 a5 87
104 195 169

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run bytes.go

ASCII letters took one byte each, so “go” has length 2. “é” took two bytes, c3 a9, and “€” three, so “héllo” has length 6 and “€5” length 4. Each Devanagari code point of “नमस्ते” took three bytes, 18 in all. len counts bytes, always, and indexing a string gives a byte, not a character: s[1] of “héllo” is 195, the first half of é.

Strings are also immutable. The specification says that once a string is created, its contents cannot change, and the compiler refuses to assign to a byte of one:

immutable.go: s[0] = 'H' Go · immutable.go
// immutable.go: a byte of a string cannot be assigned.
package main

import "fmt"

func main() {
	s := "hello"
	s[0] = 'H'
	fmt.Println(s)
}

Output (exit status 1)

Printed as an error (standard error)

# command-line-arguments
./immutable.go:8:2: cannot assign to s[0] (neither addressable nor a map index expression)

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run immutable.go

cannot assign to s[0] (neither addressable nor a map index expression): to change text, build a new string. That rule is what lets Go strings be shared freely, sliced without copying and used as map keys.

Checkpoint

Check: Strings as UTF-8 bytes

  1. Question 1 of 2 What is len("naïve")?

    Type a number.

    A hint

    ï is not ASCII.

    Show the answer: Question 1 of 2

    Answer: 6 bytes

    n, a, v and e take one byte each and ï takes two in UTF-8, so the length is 6 bytes for 5 runes, as "héllo" had length 6 in bytes.go.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    s := "€5"
    fmt.Println(s[0], len(s))
    Show the answer: Question 2 of 2

    Answer: 226 4

    bytes.go printed the bytes of "€5" as e2 82 ac 35 and its length as 4.

Indexing reads bytes; range reads runes

A loop over the indexes visits bytes. A range loop over a string decodes it instead: each pass gives the byte offset where a code point starts and that code point as a rune:

runes.go: the same string read by byte and by rune Go · runes.go
// runes.go: a byte loop and a range loop over the same string, and the count of runes.
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "héllo"
	for i := 0; i < len(s); i++ {
		fmt.Printf("%d:%x ", i, s[i]) // one byte per pass
	}
	fmt.Println()
	for i, r := range s {
		fmt.Printf("%d:%c ", i, r) // one rune per pass, with the byte offset where it starts
	}
	fmt.Println()
	fmt.Println(len(s), utf8.RuneCountInString(s))
	fmt.Println(len("नमस्ते"), utf8.RuneCountInString("नमस्ते"))
}

Output

0:68 1:c3 2:a9 3:6c 4:6c 5:6f 
0:h 1:é 3:l 4:l 5:o 
6 5
18 6

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run runes.go

The byte loop printed six bytes, c3 and a9 among them. The range loop printed five runes, and its offsets jumped from 1 to 3, because é covers offsets 1 and 2. Step through the range loop pass by pass:

Interactive · stage by stage A range loop over "héllo", one rune per pass Step through the passes and watch the byte offset each one starts at.
A range loop over "héllo", one rune per pass, step 5 of 5. Showing offset 0: h, offset 1: é, offset 3: l, offset 4: l, offset 5: o, offset 6: the end. In focus: offset 6: the end.offset 0: h1 byte: 68offset 1: é2 bytes: c3 a9offset 3: l1 byte: 6coffset 4: l1 byte: 6coffset 5: o1 byte: 6foffset 6: the endlen(s) is 6

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The first pass. The loop starts at byte offset 0. Each pass decodes one rune from the UTF-8 bytes and gives the loop two values, the offset where the rune starts and the rune itself.
  2. é takes two bytes. At offset 1 the bytes c3 and a9 together encode é, U+00E9. The loop gets i = 1 and r = é, then moves on by two bytes.
  3. Offset 3, not 2. The next pass starts at offset 3. Offset 2 holds the second byte of é, which can never start a rune, so the loop never stops there.
  4. One byte each. The l at offset 4 and the o at offset 5 are ASCII, one byte each, so these passes move on by one byte.
  5. Five passes, six bytes. At offset 6 the string ends. The loop made 5 passes over 6 bytes, which is why runes.go printed 6 for len and 5 for utf8.RuneCountInString.

So utf8.RuneCountInString is the count of runes, 5 for “héllo” and 6 for “नमस्ते”, while len stays the count of bytes. A rune is an int32 that holds one code point; the specification calls the values a range loop produces over a string exactly that, Unicode code points. A byte that is not valid UTF-8 comes out of the loop as the replacement character: U+FFFD, the code point that stands in for bytes that are not valid UTF-8. A range loop over a string gives it for each invalid byte., U+FFFD.

Key idea

len and indexes count bytes; range and utf8.RuneCountInString count runes. Choose by what the code needs, and never index by bytes into text that is not ASCII.

Checkpoint

Check: Byte loops and rune loops

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    for i := range "aé b" {
    	fmt.Print(i, " ")
    }
    Show the answer: Question 1 of 2

    Answer: 0 1 3 4

    runes.go printed 0 1 3 4 5 for "héllo": the offsets jump over the second byte of é.

  2. Question 2 of 2 Which expression gives the number of runes in a string s?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: utf8.RuneCountInString(s)

    len counts bytes and utf8.RuneCountInString counts runes; runes.go printed 6 and 5 for the same string.

Converting to []byte and []rune copies

A string’s bytes cannot change, so converting it to a slice that can change must copy them. This program changes the copies and prints the string afterwards:

convert.go: changing a []byte and a []rune of one string Go · convert.go
// convert.go: []byte and []rune conversions copy, so changing them leaves the string alone.
package main

import "fmt"

func main() {
	s := "héllo"

	b := []byte(s) // a copy of the 6 bytes
	b[0] = 'H'
	fmt.Println(string(b), s)

	r := []rune(s) // a copy, decoded: one int32 per code point
	r[1] = 'e'
	fmt.Println(string(r), s, len(r))

	fmt.Println(string(rune(233)), string([]byte{0xc3, 0xa9})) // both are "é"
}

Output

Héllo héllo
hello héllo 5
é é

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run convert.go

Conversion What you get Copies?
[]byte(s) the bytes of s, which can be changed yes, every byte
[]rune(s) the decoded runes, one int32 each yes, and decodes
string(b) a string of the bytes yes
string(r) the UTF-8 encoding of the runes yes, and encodes
string(rune(233)) “é”, one character a new string

Changing b[0] gave “Héllo” and left s as it was, and changing r[1] gave “hello”. A []rune is the way to read code points by position, r[1] being é, at four bytes a rune: 20 bytes for these 5 runes against 6 for the string.

Checkpoint

Check: Conversions that copy

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    s := "go"
    b := []byte(s)
    b[0] = 'G'
    fmt.Println(s, string(b))
    Show the answer: Question 1 of 2

    Answer: go Go

    convert.go changed b[0] to H and printed Héllo héllo: the copy changed, the string did not.

  2. Question 2 of 2 How do you get the third code point of "héllo", the l, as a rune?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: []rune(s)[2], which decodes the string and indexes the runes

    Index a []rune when you need code points by position; indexing the string gives bytes.

The strings package does the rest of the work on text, and four ideas come with it. strings.Builder builds a string piece by piece without copying everything each time, which a loop of += does on every pass. Functions such as strings.Cut and strings.Fields take text apart without counting indexes, and strings.EqualFold compares two strings without regard to case. Cutting text to a length is where bytes and runes matter most: s[:n] counts bytes, so it can end in the middle of a rune. And a character on screen, a grapheme cluster: What a reader sees as one character, which can be several code points: a letter and an accent, an emoji with a modifier, or a Devanagari conjunct. Unicode Standard Annex 29 defines the boundaries., can take several runes, as a letter with an accent or an emoji with a skin tone does, so even a rune count can split one.

Building and taking apart text with the strings package, measured Pro: opens with a Pro, Premium or Ultimate pass

Text work in Go has two costs worth knowing, the copying that += does and the slices that splitting builds. This part measures the first, shows the strings functions that avoid both, and ends with case-insensitive comparison.

In this part:

  • += copies everything built so far; strings.Builder does not
  • Cut, CutPrefix and CutLast take text apart without indexes
  • Fields, Split and the iterator forms
  • Comparing without case

3 runnable examples (Go) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Invalid UTF-8, characters made of several runes, and safe truncation Pro: opens with a Pro, Premium or Ultimate pass

Real text arrives with broken bytes, accents stored two ways and emoji made of several code points. This part shows what Go does with each, and builds the two helpers that text handling needs most: cutting to a length and taking initials.

In this part:

  • Invalid bytes decode to U+FFFD
  • One character on screen can be several runes
  • Cutting by runes, and taking initials

3 runnable examples (Go) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise

Practice covers the bytes of a string, byte and rune loops, and conversions, opening with the idea your earlier answers showed least sure. Every option comes with the reason it is right or the step it skips:

Practice that adapts to you

Practise this lesson

9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 What is a Go string?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: A read-only sequence of bytes, usually the UTF-8 encoding of text

    len counts those bytes, and indexing returns them, which is why "héllo" has length 6.

  2. Question 2 of 9 What is len("naïve café")?

    Type a number.

    A hint

    Two of the letters are not ASCII.

    Show the answer: Question 2 of 9

    Answer: 12 bytes

    The 10 runes are all one byte except ï and é, which take two each: 8 + 4 = 12 bytes.

  3. Question 3 of 9 A function must make the first letter of a name upper case. Which approach fits Go strings?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Build a new string from the upper-cased first rune and the rest

    Immutability is what lets strings be shared and used as map keys safely; edits make new strings.

  4. Question 4 of 9 What does a range loop over a string give on each pass?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: The byte offset where a rune starts, and the rune

    runes.go printed 0:h 1:é 3:l 4:l 5:o, offsets in bytes and decoded runes.

  5. Question 5 of 9 What does utf8.RuneCountInString("नमस्ते") return?

    Type a number.

    Show the answer: Question 5 of 9

    Answer: 6 runes

    The word is six code points, न म स ् त े, of three bytes each, so the rune count is 6 and len is 18.

  6. Question 6 of 9 Reversing "héllo" byte by byte produces invalid UTF-8. Which reversal keeps every character intact?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: Convert to []rune, reverse the runes, convert back with string

    Work on runes when the operation moves characters around; bytes only for ASCII.

  7. Question 7 of 9 After b := []byte(s), does changing b change s?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: No; the conversion copies the bytes

    Every conversion between strings and slices copies, in both directions.

  8. Question 8 of 9 What does this print?

    Read the code, then choose one answer.

    r := []rune("héllo")
    fmt.Println(len(r), len(string(r)))
    Show the answer: Question 8 of 9

    Answer: 5 6

    A []rune has one element per code point; the string it encodes back to counts bytes.

  9. Question 9 of 9 Which code replaces the second code point of a word with "e", however many bytes the first one takes?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: r := []rune(word); r[1] = 'e'; word = string(r)

    convert.go changed r[1] of héllo to e and printed hello, which byte arithmetic could not do.

Unicode Character Inspector Paste any text to see each of its characters with its code point and its UTF-8 bytes.

Interview questions

Warm-up (fresher): Why is len(“héllo”) 6 in Go?

Because len counts bytes, and Go strings hold UTF-8: h, l, l and o take one byte each and é takes two, c3 a9. The word has 5 runes, which utf8.RuneCountInString reports, and a range loop over it makes 5 passes.

Three longer questions of the module bank build on this lesson:

  1. Mid

    A notification service shortens each message to fit a 160-byte field with msg[:160]. Messages in Hindi sometimes end in a replacement character on phones. Explain what happens, fix it, and say what can still go wrong with emoji.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A report builder adds 50,000 lines of about 40 bytes to a string with report += line, and the job is slow and uses far more memory than the 2 MB report. Explain the cost with numbers and rewrite it.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A sign-up form must limit usernames to 20 characters. The Go check len(name) > 20 rejects many names written in Devanagari that look short. What should "characters" mean here, and how do you implement and test the limit?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Go strings are read-only sequences of bytes, UTF-8 for text, so len counts bytes: 6 for “héllo” and 18 for “नमस्ते”.
  • Indexing a string gives a byte; a range loop gives runes and the byte offset where each starts, and utf8.RuneCountInString counts runes.
  • []byte(s) and []rune(s) copy, so changing the slice never changes the string; []rune reads code points by position.

Terms in this lesson

code point
The number Unicode gives a character, written U+00E9 for é. A Go rune holds one code point, and UTF-8 encodes it in one to four bytes.
grapheme cluster
What a reader sees as one character, which can be several code points: a letter and an accent, an emoji with a modifier, or a Devanagari conjunct. Unicode Standard Annex 29 defines the boundaries.
replacement character
U+FFFD, the code point that stands in for bytes that are not valid UTF-8. A range loop over a string gives it for each invalid byte.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.