День 04 · дополнительные материалы

День 04 — глубже

Всё здесь необязательно. Материалы на английском помечены.


Ctrl+S сортирует import

Порядок пакетов в скобках неважен: gofmt расставит их по алфавиту сам, а в VS Code это делает Ctrl+S. Ниже не программа, а показ правки: минус в начале строки значит «было до форматирования», плюс — «стало после». Файл с "strings" выше "fmt" после gofmt выглядит так:

Образец · набирать не нужно
 import (
-	"strings"
 	"fmt"
+	"strings"
 )

Сортирует Ctrl+S только то, что уже написано: добавлять и удалять пакеты в import он не будет — в настройках VS Code из подготовки это выключено. Какие пакеты нужны, решаете вы, как и в терминале.

Проверить можно так: поменяйте в черновике строки "fmt" и "strings" местами, нажмите Ctrl+S — строки встанут по алфавиту. Потом в терминале:

▶ Выполните
cd ~/gocourse/day04/scratch
gofmt -l .

Пустой вывод — форматировать нечего. Если бы файл был не в формате, gofmt -l . напечатал бы его имя: main.go.

Функция с двумя результатами: что скажет компилятор

strings.CutPrefix отрезает начало строки и сообщает, было ли это начало:

Код для чтения · разбираем, набирать не нужно
number, found := strings.CutPrefix("SKU-4821", "SKU-")
fmt.Println(number, found)
number, found = strings.CutPrefix("4821", "SKU-")
fmt.Println(number, found)
Вывод · это печатает программа, набирать не нужно
4821 true
4821 false

Оставите слева одну переменную — компилятор откажет. Программа целиком, с такой строкой вместо первой:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"fmt"
	"strings"
)

func main() {
	number := strings.CutPrefix("SKU-4821", "SKU-")
	fmt.Println(number, found)
	number, found = strings.CutPrefix("4821", "SKU-")
	fmt.Println(number, found)
}

Сообщений четыре:

Вывод · это печатает программа, набирать не нужно
./main.go:9:12: assignment mismatch: 1 variable but strings.CutPrefix returns 2 values
./main.go:10:22: undefined: found
./main.go:11:10: undefined: found
./main.go:12:22: undefined: found

Причина одна — первое сообщение. Три undefined: found — её следствие: переменная found так и не объявлена, и компилятор отмечает каждую строку, где она встречается. Верните вторую переменную — уйдут все четыре.

Где ещё ломается len

Код для чтения · разбираем, набирать не нужно
  fmt.Printf("[%-8s]\n", "заказ")
  fmt.Printf("[%-8s]\n", "order")
Вывод · это печатает программа, набирать не нужно
  [заказ   ]
  [order   ]

Здесь Printf дополнил оба слова до 8 символов одинаково.

Ещё сигнатуры

Все строки ниже — из вывода go doc для каждой функции:

Код для чтения · разбираем, набирать не нужно
func Count(s, substr string) int
func CutPrefix(s, prefix string) (after string, found bool)
func Ceil(x float64) float64
func Itoa(i int) string
func RuneCountInString(s string) (n int)

Паника внутри библиотечной функции

It panics if count is negative из описания strings.Repeat — не пустые слова. Вызов strings.Repeat("-", -1) соберётся, но при запуске программа упадёт. В программе, где строка line := strings.Repeat("-", -1) — девятая:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day04/scratch$ go run .
panic: strings: negative Repeat count

goroutine 1 [running]:
strings.Repeat({0x49b000?, 0x70?}, 0x0?)
	/usr/local/go/src/strings/strings.go:609 +0x56b
main.main()
	/home/stagiaire/gocourse/day04/scratch/main.go:9 +0x26
exit status 2

Паники вы видели в дне 03 — integer divide by zero. Читаются так же: первая строка — причина, ниже — где случилось. Но здесь «где» два. Паника случилась внутри библиотечной функции, поэтому первой идёт она: strings.Repeat(...) и путь /usr/local/go/src/strings/strings.go:609. Всё, что лежит в /usr/local/go/src, — исходники самого Go, этот файл открывать и править не нужно. Своё место ищите ниже, по имени своего файла: main.main() и …/scratch/main.go:9 — строка 9 вашей программы, где Repeat вызвали с отрицательным числом. Числа в скобках и +0x… пропускайте.

Список функций пакета

go doc strings без имени функции покажет описание пакета и все его функции — 67 строк. Начало:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~$ go doc strings
package strings // import "strings"

Package strings implements simple functions to manipulate UTF-8 encoded strings.

For information about UTF-8 strings in Go, see https://blog.golang.org/strings.

func Clone(s string) string
func Compare(a, b string) int
func Contains(s, substr string) bool
func ContainsAny(s, chars string) bool
func ContainsFunc(s string, f func(rune) bool) bool
func ContainsRune(s string, r rune) bool
func Count(s, substr string) int

Список не помещается на экран — прокрутите терминал колесом мыши.

Та же документация в браузере и в редакторе

go doc ценен тем, что работает везде: без интернета и на сервере, где нет ни браузера, ни редактора.

Строки, байты и символы (англ.)

Документация пакетов дня (англ.)

Тот же текст, что в go doc, плюс примеры с кнопкой Run. Откройте пример у strings.Repeat, поменяйте число и запустите прямо на странице.

Пакеты и импорт в туре по Go (англ.)

Как написана функция

go doc с ключом -src показывает не описание, а код функции:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~$ go doc -src strings.Repeat
package strings // import "strings"

// Repeat returns a new string consisting of count copies of the string s.
//
// It panics if count is negative or if the result of (len(s) * count)
// overflows.
func Repeat(s string, count int) string {
	switch count {
	case 0:
		return ""
	case 1:
		return s

Вывод длиннее — здесь только начало. Описание, которое печатает обычный go doc, — это комментарий над функцией в её коде. switch и return вы разберёте в днях 06 и 11; пока достаточно увидеть, что стандартная библиотека написана на том же Go, что и ваши программы.

Два округления

В math есть Round и RoundToEven. Они расходятся ровно на половинках:

Код для чтения · разбираем, набирать не нужно
fmt.Println(math.RoundToEven(2.5), math.Round(2.5), math.Trunc(-2.7), math.Floor(-2.7))
Вывод · это печатает программа, набирать не нужно
2 3 -2 -3

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

Проверка задания label этим и пользуется: она запускает копию вашей программы, в которой math.Ceil заменена на math.Floor, а math.Round — на math.Trunc. Если коробки и цену считают эти функции, вывод копии обязан отличаться от вашего.

min и max — без import

min и max встроены в язык, как len: подключать ничего не нужно. Аргументов сколько угодно, все одного типа:

Код для чтения · разбираем, набирать не нужно
fmt.Println(min(12, 5, 9), max(3.5, 2))
a := 40
b := 25
fmt.Println(min(a, b), max(a, b, 100))
stock := 40
reserved := 55
fmt.Println("Можно отгрузить:", min(stock, reserved))
Вывод · это печатает программа, набирать не нужно
5 3.5
25 100
Можно отгрузить: 40

Заказано 55, на полке 40 — отгрузить можно 40: это и отвечает min(stock, reserved).

Смешать целую переменную и дробное число нельзя. min(a, 2.5) при a := 3 не соберётся:

Вывод · это печатает программа, набирать не нужно
./main.go:7:21: 2.5 (untyped float constant) truncated to int

В пакете math есть похожие math.Min и math.Max, но они работают только с float64 и только с двумя числами. Для целых берите встроенные min и max. Описание — go doc builtin.min; строку с [T cmp.Ordered] в сигнатуре пока пропускайте.

fmt.Sprintf — Printf в строку

fmt.Printf из дня 03 печатает по шаблону на экран. fmt.Sprintf берёт тот же шаблон с теми же %d, %s, %.2f, но не печатает, а возвращает готовую строку. Буква S — string.

Код для чтения · разбираем, набирать не нужно
code := fmt.Sprintf("%s-%04d", "TK", 42)
fmt.Println(code)
price := fmt.Sprintf("%.2f руб.", 1299.9)
fmt.Println(price)
label := fmt.Sprintf("%s: %d шт. по %.2f руб.", "Лампа", 3, 459.5)
fmt.Println(label)
Вывод · это печатает программа, набирать не нужно
TK-0042
1299.90 руб.
Лампа: 3 шт. по 459.50 руб.

%04d — целое шириной 4 знака, как %02d в дне 03: недостающие цифры слева заполняются нулями, поэтому 42 стало 0042.

Sprintf нужен, когда строку надо не напечатать, а передать дальше. Числа любых типов он превращает в текст сам.

Выброшенный результат Sprintf компилятор пропустит, как и у ToUpper. Зато его поймает go vet — проверка подозрительного кода из дня 03:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"fmt"
)

func main() {
	price := fmt.Sprintf("%.2f", 12.5)
	fmt.Sprintf("%s руб.", price)
	fmt.Println(price)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day04/scratch$ go run .
12.50
stagiaire@lab:~/gocourse/day04/scratch$ go vet .
main.go:9:2: result of fmt.Sprintf call not used

Программа собралась и напечатала 12.50 без « руб.». go vet указал строку 9, где результат пропал.

Рамка из звёздочек в черновике

Табличка со страницы 6 печатала рамку по названию из одного слова. Наберите её в черновик и сделайте рамку для своего товара из label/TASK.txt: подставьте своё название в name. В самом label рамку делать не нужно — там проверка сверяет шесть строк буква в букву, и лишние строки её сломают.

✎ Наберите в файл scratch/main.go
package main

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

func main() {
	name := "  лампа "
	name = strings.TrimSpace(name)
	title := strings.ToUpper(name)
	width := utf8.RuneCountInString(title) + 4
	fmt.Println(strings.Repeat("*", width))
	fmt.Println("* " + title + " *")
	fmt.Println(strings.Repeat("*", width))
}
▶ Выполните
cd ~/gocourse/day04/scratch
go run .

Проверьте на названии из двух слов и на латинском lamp: где рамка сойдётся, а где разъедется, если считать ширину через len?

Откуда взялись приёмы дня