Кириллица и len
Эта страница — про ловушку с русским текстом: программа собирается, на английских словах работает правильно, а на русских — нет.
Ситуация: черта под названием
Под названием нужна черта ровно его длины. Длину строки возвращает встроенная len. Пробуем на двух словах:
name := "order"
fmt.Println(name)
fmt.Println(strings.Repeat("=", len(name)))
name = "заказ"
fmt.Println(name)
fmt.Println(strings.Repeat("=", len(name)))Что увидите:
order
=====
заказ
==========Под order пять знаков — верно. Под заказ, в котором тоже пять букв, — десять.
Почему: байты и символы
Компьютер хранит текст как последовательность байтов — чисел от 0 до 255, а превращает буквы в байты кодировка. В Go строки хранятся в UTF-8, и в ней разные символы занимают разное число байтов:
| Что | Байтов на символ | Пример |
|---|---|---|
латиница, цифры, пробел, = - + # |
1 | len("order") — 5 |
кириллица, в том числе ё |
2 | len("заказ") — 10 |
кавычки-ёлочки «» |
2 | len("«»") — 4 |
знак № |
3 | len("№") — 3 |
| эмодзи | 4 | len("📦") — 4 |
len считает байты, а не символы. Это не ошибка, а так задумано: go doc builtin.len прямо говорит String: the number of bytes in v. Байты нужны, когда строку записывают в файл или отправляют по сети; человеку на экране нужны символы.
В "Склад №5" пять русских букв (10 байт), пробел (1), № (3) и цифра (1):
fmt.Println(len("«»"), len("№"), len("Склад №5"), len("ё"), len("📦"))4 3 15 2 4Попробуйте сейчас: len под русским словом.
Цель: увидеть черту двойной длины под русским словом и настоящие значения
lenдля пяти строк.Наберите черновик целиком. В
importтолько"fmt"и"strings": пакет, который не вызывается, дастimported and not used.✎ Наберите в файлscratch/main.gopackage main import ( "fmt" "strings" ) func main() { name := "order" fmt.Println(name) fmt.Println(strings.Repeat("=", len(name))) name = "заказ" fmt.Println(name) fmt.Println(strings.Repeat("=", len(name))) }Запустите:
▶ Выполнитеcd ~/gocourse/day04/scratch go run .Потом добавьте строку с пятью
lenиз блока выше, а вnameподставьте своё название товара изlabel/TASK.txt: насколько черта длиннее?Готово, когда: вывод совпал с блоком «Что увидите», строка с пятью
lenнапечатала4 3 15 2 4, а для своего названия вы знаете, на сколько знаков черта длиннее.
utf8.RuneCountInString — число символов
Символы считает функция RuneCountInString из пакета unicode/utf8:
package main
import (
"fmt"
"strings"
"unicode/utf8"
)
func main() {
name := "заказ"
fmt.Println(len(name))
fmt.Println(utf8.RuneCountInString(name))
fmt.Println(name)
fmt.Println(strings.Repeat("=", utf8.RuneCountInString(name)))
}10
5
заказ
=====Как читать. Путь пакета unicode/utf8 состоит из двух частей: в import пишется путь целиком, а в коде — только последняя часть, utf8.. Rune — так в Go называют один символ, и название функции читается «посчитать символы в строке» (подробно о символах — в блоке 4). Последний вызов стоит прямо в аргументе Repeat: его результат, число 5, и становится вторым аргументом.
Ловушка не только про черту: len подведёт и в правиле «название не длиннее 20 символов», и в выравнивании столбцов Printf — разборы в course extra 04.
Правило на весь курс: для текста, который увидит человек, длина — utf8.RuneCountInString; len у строки — только когда нужны именно байты.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
| черта под русским словом вдвое длиннее | ширина посчитана len, в байтах |
utf8.RuneCountInString |
./main.go:6:14: undefined: utf8 |
пакет не подключён | "unicode/utf8" в import |
main.go:5:2: package utf8 is not in std (/usr/local/go/src/utf8) |
в import написано "utf8", такого пакета нет |
путь целиком: "unicode/utf8" |
./main.go:5:2: "unicode/utf8" imported and not used |
подключили, но не вызвали | вызвать или убрать |
В ошибке package utf8 is not in std нет строки # day04/... и нет ./ перед именем файла: компилятор споткнулся ещё на import, до сборки пакета, — а формат файл:строка:колонка тот же.
Попробуйте сейчас: что выведет, не запуская.
Цель: предсказать строку вывода по коду и отправить её.
Не запуская, предскажите вывод:
Код для чтения · разбираем, набирать не нужноpackage main import ( "fmt" "unicode/utf8" ) func main() { word := "заказ-15" fmt.Println(len(word), utf8.RuneCountInString(word)) }Отправьте предсказанную строку вывода целиком:
▶ Выполните · выделенное замените своимcourse answer day04.q4 ВАШ_ОТВЕТГотово, когда:
course answerответилПринято: day04.q4 = …(пункт проверкиq4).
Попробуйте сейчас: дописать этикетку label.
Цель:
labelпечатает все шесть строк изTASK.txt, черта — ровно по названию.Допишите этикетку
label: первая строка — название в верхнем регистре, вторая и последняя — строки из=ширины, равной числу символов в названии.▶ Выполнитеcd ~/gocourse/day04/label go run . gofmt -l . go vet .
go vet .здесь не случайно: привычка с дня 03 — перед сдачейgo vet .— действует и сегодня. Сравните вывод сTASK.txtстрока за строкой: черта должна кончаться ровно под последней буквой названия. Пробел в названии из двух слов — тоже символ.Готово, когда: вывод
go run .совпал сTASK.txtстрока в строку, аgofmt -l .иgo vet .ничего не напечатали (пунктыlabelиlabel_funcs).
Дальше: как узнать, что принимает и возвращает функция, — course next