---
title: "Типы данных: строки и руны"
description: "Строки в Go — это байты: UTF-8, руны, длина строки и срезы строк с примерами, которые запускаются в браузере. Разбираем len(\"привет\") и подводные камни."
course: "Go с нуля: базовый синтаксис и конструкции"
url: "https://golangmaster.ru/courses/go-basics/lessons/06-stroki-i-runes/"
published: "2026-09-19"
updated: "2026-09-28"
minutes: 20
---

# Типы данных: строки и руны

*Курс «Go с нуля: базовый синтаксис и конструкции», урок 6 из 25. ~20 минут. Обновлено 28.09.2026. Источник: https://golangmaster.ru/courses/go-basics/lessons/06-stroki-i-runes/*

Строки в Go устроены иначе, чем в Python или JavaScript: строка — это последовательность байт, а не символов, поэтому len("привет") возвращает 12, хотя в слове шесть букв. Разберём, как строки лежат в памяти, что такое руны и UTF-8, как безопасно резать строку срезами и какие операции даёт пакет strings — весь код запускается прямо на странице.

### Строки в Go — это байты, а не символы

Попробуйте ответить на простой вопрос: сколько места занимает слово «привет»? Интуиция подсказывает «шесть букв», но Go отвечает иначе: 12 байт. Строка в Go — это неизменяемая последовательность байт, упакованная в тип `string`. Нулевое значение строки — пустая строка `""`, склеиваются строки плюсом: `"Go" + "-" + "самоучитель"`.

Откуда берутся лишние байты? Go хранит строки в кодировке UTF-8: латинские буквы, цифры и знаки препинания занимают по одному байту, а кириллица — по два. Компьютер не видит «букв», он видит числа-байты, и все операции со строкой по умолчанию работают именно с байтами: `len(s)` возвращает длину в байтах, а `s[0]` — не первый символ, а первый байт, то есть число от 0 до 255.

Это не причуда Go, а осознанный инженерный выбор: байтовое представление делает срезы, конкатенацию и передачу строк по сети дешёвыми. Плата за скорость — необходимость понимать, где заканчиваются байты и начинаются символы. Этой границей мы и займёмся весь урок.

### UTF-8: почему len считает не буквы

UTF-8 — кодировка переменной длины: один символ занимает от 1 до 4 байт в зависимости от своего кода в таблице Unicode. Латиница и цифры — 1 байт, кириллица — 2 байта, редкие иероглифы и эмодзи — 3-4 байта. Отсюда точные цифры: `len("Go")` — это 2, `len("привет")` — это 12 (шесть букв по два байта), а `len("Hi 🙂")` — 7: две латинские буквы, пробел и четырёхбайтовый эмодзи.

Красивый исторический факт: UTF-8 в 1992 году придумали Кен Томпсон и Роб Пайк — те самые люди, которые позже создали Go. Поэтому байтовая модель строк для языка «родная», а не вынужденная. В настоящем Go для подсчёта именно символов есть функция `utf8.RuneCountInString` из пакета unicode/utf8 — в нашей песочнице этого пакета нет, но принцип стоит запомнить: байтов в строке и символов в ней — разные числа, и различать их обязан каждый, кто обрабатывает пользовательский ввод на кириллице.

### Песочница: длина строки и байты по индексам

Запустите и сверьте: 2 и 12 байт, затем коды первых байтов — 71 (латинская G) и 208 (начало кириллической буквы). Замените "привет" на "мир": длина останется 6 — три буквы по два байта, счёт идёт не по буквам.

```go
package main

import "fmt"

func main() {
        latin := "Go"
        cyrillic := "привет"
        fmt.Println(len(latin), len(cyrillic))
        fmt.Println(latin[0])
        fmt.Println(cyrillic[0])
}
```

### Руны go utf-8: символ как число

Каждому символу Unicode присвоен номер — кодовая точка. В Go для кодовых точек есть отдельный тип: `rune`, а под капотом это обычный int32. Название взято у букв древнескандинавских алфавитов — рун, каждая из которых обозначала и звук, и число одновременно. Рунный литерал пишется в одинарных кавычках: `'Ж'` — это число 1046, код кириллической Ж в таблице Unicode.

Руна превращается в символ обратным преобразованием: `string(1046)` даёт "Ж", а глагол `%c` в Printf печатает символ прямо из числа — о нём подробнее в уроке про [fmt и форматирование](/courses/go-basics/lessons/09-fmt-vyvod/). Из этого следует сюрприз, который регулярно всплывает на форумах: `fmt.Println('Ж')` напечатает не букву, а 1046 — ведь переменная хранит число. Тип тоже честный: `%T` для руны покажет int32.

Запомните пары: байт — единица хранения (uint8), руна — единица смысла (int32). Строка хранится байтами, а мыслится рунами.

### Руны в действии

```go
package main

import "fmt"

func main() {
        letter := 'Ж'
        fmt.Println(letter)
        fmt.Println(string(letter))
        fmt.Printf("Код: %d, символ: %c, тип: %T\n", letter, letter, letter)
}
```

Переменная letter — руна, то есть число 1046, поэтому Println печатает именно число. string(letter) собирает из кода настоящий символ. Глагол %d выводит код, %c — символ, %T подтверждает: под видом руны скрывается int32.

### Проверьте себя: длина кириллической строки

**Вопрос:** Что напечатает `fmt.Println(len("привет"))`?

- 6
- ✅ 12
- 3
- ошибка компиляции

len считает байты, а каждая кириллическая буква в UTF-8 занимает два байта: шесть букв умножить на два — 12. Чтобы посчитать именно символы в настоящем Go, нужна utf8.RuneCountInString из пакета unicode/utf8.

### Что выведет код?

```go
package main

import "fmt"

func main() {
        word := "мир"
        fmt.Println(len(word))
        fmt.Println(word[0])
}
```

**Что выведет программа?**

- ✅ 6 и 208
- 3 и м
- 3 и 208
- 6 и м

len("мир") — 6 байт: три кириллические буквы по два байта. word[0] возвращает первый байт строки, а не первую букву: код 208 — это половинка UTF-8-представления буквы м. Букву из индекса без руны и границ байтов не достать.

> **Строку нельзя изменить.** Присваивание в строку не скомпилируется: запись s[0] = 'X' даёт ошибку "cannot assign to s[0]" — строки в Go неизменяемы. Никаких исключений: даже функцию из strings нельзя «применить» к старой переменной, всё возвращает новые строки. Нужна модификация — собирайте новую строку конкатенацией или срезами.

### Срезы строк: режем по байтам

Из строки, как из среза, достаётся кусок: `s[i:j]` возвращает байты с i-го по j-1. Для латиницы всё прозрачно: `"Go-самоучитель"[0:2]` — это "Go". Но границы считаются по байтам, и с кириллицей нужна аккуратность: буква с занимает байты 3 и 4, буква а — байты 5 и 6, поэтому срез `[3:7]` отрезает ровно "са". Сместите границу на единицу — `[3:8]` — и от буквы м откушен первый байт: на экран вылезет испорченный символ, потому что получившаяся последовательность байт больше не является корректной UTF-8. Настоящий компилятор этого не заметит — ошибка проявится только при выводе.

Практическое правило: резать строки можно уверенно, если границы кратны длине символов, и опасно, если вы считаете байты руками. Все тонкости срезов — длина, ёмкость, шаринг памяти — ждут вас в уроке про [срезы: создание и индексацию](/courses/go-basics/lessons/17-srezy-basics/).

### Срезаем по границам букв

```go
package main

import "fmt"

func main() {
        s := "Go-самоучитель"
        fmt.Println(len(s))
        fmt.Println(s[0:2])
        fmt.Println(s[3:7])
        fmt.Println(s + "!")
}
```

Строка занимает 25 байт: три латинских символа плюс одиннадцать кириллических по два. Срез [0:2] забирает байты Go, срез [3:7] — два полных кириллических символа. Конкатенация создаёт новую строку, исходная не меняется: строки неизменяемы.

### Сравнение строк: байт против байта

Строки сравниваются операторами ==, !=, <, > — и сравнение тоже идёт по байтам. Равенство проверяет, что все байты совпадают: "Go" == "Go" — истина, "go" == "Go" — ложь, регистр важен. Порядковое сравнение "apple" < "banana" вернёт true: код латинской a меньше кода b. С кириллицей порядок тоже работает корректно — разработчики UTF-8 позаботились, чтобы порядок байт в кодировке совпадал с порядком кодов символов, поэтому алфавитная сортировка фамилий из двух байт на букву внезапно оказывается правильной. Нюансы начинаются с регистрами и буквой ё, но это уже тема для полноценных алгоритмов сортировки, а не базового урока.

### Пакет strings: операции со строками в Go

Ручные циклы по байтам нужны редко: почти всё, что вы захотите сделать со строкой, уже написано в стандартном пакете strings. Проверить вхождение подстроки — `strings.Contains(s, "текст")`; убедиться, что путь начинается с префикса, — `strings.HasPrefix(path, "/api")`; срезать пробелы по краям пользовательского ввода — `strings.TrimSpace`. Разбить строку на части — `strings.Split("a,b", ",")` вернёт срез строк, склеить обратно — `strings.Join`. Замена, повтор, перевод регистра — `ReplaceAll`, `Repeat`, `ToUpper` и `ToLower`.

Все функции пакета возвращают новые строки или срезы — помните про неизменяемость. Полный набор глаголов fmt для вывода строк в разных видах мы разбираем отдельно, а здесь — таблица-шпаргалка по самому востребованному.

### Шпаргалка по пакету strings

| Функция | Что делает | Пример вызова | Результат |
| --- | --- | --- | --- |
| strings.Contains | есть ли подстрока внутри | Contains("Go-самоучитель", "сам") | true |
| strings.HasPrefix | начинается ли с префикса | HasPrefix("/api/users", "/api") | true |
| strings.HasSuffix | заканчивается ли суффиксом | HasSuffix("report.pdf", ".pdf") | true |
| strings.ToUpper / ToLower | перевод регистра | ToUpper("go") | "GO" |
| strings.TrimSpace | убрать пробелы по краям | TrimSpace("  hi  ") | "hi" |
| strings.Split | разбить в срез строк | Split("2024-09-01", "-") | [2024 09 01] |
| strings.Join | склеить срез с разделителем | Join([]string{"a", "b"}, "-") | "a-b" |
| strings.Repeat | повторить n раз | Repeat("ab", 3) | "ababab" |
| strings.ReplaceAll | заменить все вхождения | ReplaceAll("a-b", "-", "+") | "a+b" |

### Песочница: strings в деле

Запустите и сверьте вывод с таблицей-шпаргалкой выше. Затем замените Contains(phrase, "браузере") на Contains(phrase, "ноутбуке") и проверьте, что вернётся false. Добавьте строку fmt.Println(strings.ReplaceAll("2024-09-01", "-", "/")) — получите дату в другом формате.

```go
package main

import (
        "fmt"
        "strings"
)

func main() {
        phrase := "  Go учат в браузере  "
        fmt.Println(strings.TrimSpace(phrase))
        fmt.Println(strings.ToUpper("go"), strings.Contains(phrase, "браузере"))
        parts := strings.Split("2024-09-01", "-")
        fmt.Println(len(parts), parts[0], parts[2])
        fmt.Println(strings.Repeat("ab", 3))
}
```

### Соберите проверку имени файла

Заполните пропуски функциями пакета strings так, чтобы программа проверила имя файла: начинается ли оно с report и заканчивается ли расширением .pdf.

```go
package main

import (
        "fmt"
        "strings"
)

func main() {
        file := "report.pdf"
        fmt.Println(strings.___(file, "report"))
        fmt.Println(strings.___(file, ".pdf"))
}
```

Подсказка: Обе функции возвращают bool. Префикс — это начало строки (Prefix), суффикс — конец (Suffix).

Правильные ответы в пропусках: `HasPrefix`, `HasSuffix`

> **Перебор строки: осторожнее с range.** Когда доберётесь до циклов, учтите: range по строке идёт по рунам со смещениями — так и в настоящем Go, и в основном движке песочницы; байтовый обход строки — упрощение резервного движка, который включается, только если основной недоступен (подробно — в уроке про range). А вот правило «len и срезы считают байты» от движка не зависит и действует всегда — проверяйте перебор не-латинских строк с оглядкой на него.

### Карточки: строки и руны

- **Что такое строка в Go на уровне памяти?** — Неизменяемая последовательность байт в кодировке UTF-8. Тип string, нулевое значение "".
- **len("привет")** — 12 — len считает байты, кириллическая буква занимает два байта.
- **Что вернёт s[0] для строки?** — Первый байт строки — число от 0 до 255 (тип byte), а не символ.
- **Что такое rune?** — Кодовая точка Unicode, псевдоним int32. Литерал в одинарных кавычках: 'Ж' — это 1046.
- **string(1046)** — "Ж" — преобразование руны (или любого целого) в строку даёт символ с этим кодом. В Printf тот же символ печатает %c.
- **Можно ли изменить строку?** — Нет, строки неизменяемы: s[0] = 'X' не скомпилируется. Функции strings всегда возвращают новые строки.

## Коротко: что запомнить

- Строка в Go — неизменяемая последовательность байт; len и индексы работают с байтами.
- UTF-8 кодирует символы 1-4 байтами: латиница — 1, кириллица — 2, эмодзи — 4.
- len("привет") — это 12: шесть букв по два байта; символы считает utf8.RuneCountInString.
- Руны — кодовые точки Unicode, тип rune = int32; string(1046) даёт "Ж", %c печатает символ.
- Срезы строк режут по байтам: граница посреди буквы портит символ.
- Почти любую операцию со строкой даёт пакет strings: Contains, HasPrefix, Split, Join, TrimSpace.
- Сравнение == и < идёт по байтам, порядок UTF-8 совпадает с алфавитным.

## Домашнее задание

Напишите программу с переменной word := "самоучитель". Выведите: длину строки в байтах (должно быть 22), первый байт строки, проверку strings.HasPrefix(word, "сам") и strings.Repeat("го", 3). Затем соберите новую строку newWord := "Go-" + word и выведите её целиком и её длину (25 байт). Сверьте каждое число с расчётом из урока: 11 букв по два байта плюс три латинских на префикс.

## Вопросы и ответы по уроку

### Как посчитать именно буквы в строке, а не байты?

В настоящем Go — функцией utf8.RuneCountInString(s) из пакета unicode/utf8: она вернёт 6 для "привет". В песочнице этого пакета нет, поэтому длина здесь всегда байтовая — делите длину кириллической строки на два только если уверены, что букв однобайтовых нет.

### Чем string отличается от []byte?

Содержимое то же самое — байты, но []byte изменяем, а string нет. Преобразования идут через копирование: []byte(s) даёт изменяемую копию, string(b) собирает строку из байтов. В песочнице работает []byte(s) и string(b), менять байты в срезе можно.

### Почему fmt.Println('Ж') печатает число, а не букву?

Рунный литерал 'Ж' имеет тип rune, под которым скрывается int32, поэтому Println выводит код 1046. Чтобы увидеть букву, преобразуйте код: string('Ж') или fmt.Printf("%c", 'Ж').
