День 04 · Чужой код и готовые функции · страница 4 из 7

Кириллица и 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.go
package 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