Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 6 из 25 ~20 мин +50 XP

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

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

Содержание урока · 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). Строка хранится байтами, а мыслится рунами.

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

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("привет"))`?

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

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

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

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

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

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

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

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", "-", "/")) — получите дату в другом формате.

Вывод

Запустите код, чтобы увидеть вывод.

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

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

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

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

Термин

Что такое строка в Go на уровне памяти?

нажмите, чтобы перевернуть
Определение

Неизменяемая последовательность байт в кодировке UTF-8. Тип string, нулевое значение "".

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 Base · Типы данных: строки и руны)

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

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

В настоящем 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 Pro25 уроков~14 чпрактика25 уроков уже доступны

Продвинутые возможности Go: от замыканий до рефлексии

Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.

  • Писать и читать конкурентный код: go-функции, каналы, select
  • Управлять жизненным циклом операций через context
  • Проектировать API на интерфейсах
  • Использовать дженерики без злоупотребления