Содержание урока · 10 разделов
Строки в 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 utf-8: символ как число#
Каждому символу Unicode присвоен номер — кодовая точка. В Go для кодовых точек есть отдельный тип: rune, а под капотом это обычный int32. Название взято у букв древнескандинавских алфавитов — рун, каждая из которых обозначала и звук, и число одновременно. Рунный литерал пишется в одинарных кавычках: 'Ж' — это число 1046, код кириллической Ж в таблице Unicode.
Руна превращается в символ обратным преобразованием: string(1046) даёт "Ж", а глагол %c в Printf печатает символ прямо из числа — о нём подробнее в уроке про fmt и форматирование. Из этого следует сюрприз, который регулярно всплывает на форумах: fmt.Println('Ж') напечатает не букву, а 1046 — ведь переменная хранит число. Тип тоже честный: %T для руны покажет int32.
Запомните пары: байт — единица хранения (uint8), руна — единица смысла (int32). Строка хранится байтами, а мыслится рунами.
Руны в действии
GOpackage 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("привет"))`?
Что выведет код?
Прочитайте код и выберите, что он выведет:
package main
import "fmt"
func main() {
word := "мир"
fmt.Println(len(word))
fmt.Println(word[0])
}Присваивание в строку не скомпилируется: запись 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. Настоящий компилятор этого не заметит — ошибка проявится только при выводе.
Практическое правило: резать строки можно уверенно, если границы кратны длине символов, и опасно, если вы считаете байты руками. Все тонкости срезов — длина, ёмкость, шаринг памяти — ждут вас в уроке про срезы: создание и индексацию.
Срезаем по границам букв
GOpackage 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", "-", "/")) — получите дату в другом формате.
Запустите код, чтобы увидеть вывод.
Соберите проверку имени файла
Заполните пропуски функциями пакета strings так, чтобы программа проверила имя файла: начинается ли оно с report и заканчивается ли расширением .pdf.
Когда доберётесь до циклов, учтите: range по строке идёт по рунам со смещениями — так и в настоящем Go, и в основном движке песочницы; байтовый обход строки — упрощение резервного движка, который включается, только если основной недоступен (подробно — в уроке про range). А вот правило «len и срезы считают байты» от движка не зависит и действует всегда — проверяйте перебор не-латинских строк с оглядкой на него.
Карточки: строки и руны
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Строка в 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", 'Ж').
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Продвинутые возможности Go: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления