День 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
- Ограничение длины. Правило «название не длиннее 20 символов», проверенное через
len, отсечёт русское название из 11 букв: дляlenв нём 22. - Выравнивание столбцов. Ширина в
fmt.Printf— например%-8s— считается в символах, аlen— в байтах. Смешаете их в одной таблице — столбцы разъедутся:
fmt.Printf("[%-8s]\n", "заказ")
fmt.Printf("[%-8s]\n", "order") [заказ ]
[order ] Здесь Printf дополнил оба слова до 8 символов одинаково.
- Колонка в сообщениях компилятора. Она тоже считается в байтах: в строке с русским текстом номер колонки больше, чем символов до ошибки. Место ищите по номеру строки и подчёркиванию в VS Code.
- Регистр не меняет байты.
strings.ToUpper("ёлка")—ЁЛКА, и в обеих по 8 байт. Считать ширину можно доToUpperили после — ответ один.
Ещё сигнатуры
Все строки ниже — из вывода 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)Count(s, substr string)— два параметра одного типа. Тип написан один раз, после последнего имени: оба — строки. Результат —int.CutPrefix(...) (after string, found bool)— результатов два, поэтому они в скобках, у каждого имя и тип.Ceil(x float64) float64— дайтеfloat64, получитеfloat64. Отсюдаfloat64(qty)перед вызовом иint(boxes)после.Itoa(i int) string— целое на входе, строка на выходе.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Список не помещается на экран — прокрутите терминал колесом мыши.
Та же документация в браузере и в редакторе
- pkg.go.dev — сайт документации Go:
https://pkg.go.dev/strings#Repeat. Текст тот же, плюс примеры, которые запускаются кнопкой Run прямо на странице. - VS Code — наведите мышь на имя функции в коде: всплывёт сигнатура и описание. Это тот же текст, его показывает расширение Go.
go doc ценен тем, что работает везде: без интернета и на сервере, где нет ни браузера, ни редактора.
Строки, байты и символы (англ.)
- Strings, bytes, runes and characters in Go — https://go.dev/blog/strings Статья Роба Пайка, одного из авторов Go: почему
lenсчитает байты, что такоеruneи как Go хранит текст. Это ссылка из описания пакетаstrings, которое вы видели вgo doc strings. Первые разделы понятны уже сейчас, остальное — к блоку 4.
Документация пакетов дня (англ.)
- https://pkg.go.dev/strings
- https://pkg.go.dev/math
- https://pkg.go.dev/strconv
- https://pkg.go.dev/unicode/utf8
Тот же текст, что в go doc, плюс примеры с кнопкой Run. Откройте пример у strings.Repeat, поменяйте число и запустите прямо на странице.
Пакеты и импорт в туре по Go (англ.)
- A Tour of Go, Packages / Imports / Exported names — https://go.dev/tour/basics/1 Три коротких страницы: откуда берутся пакеты, почему импорт со скобками и почему вызывать можно только имена с большой буквы.
Как написана функция
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 -3RoundToEven округляет половину к чётному — так называемое банковское округление: на длинной колонке сумм оно не накапливает ошибку в одну сторону. 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.gopackage 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?
Откуда взялись приёмы дня
- Задачи Парсонса — Dale Parsons, Patricia Haden, Parson's Programming Puzzles: A Fun and Effective Learning Tool for First Programming Courses (2006). Исследования после них показывают: на таких задачах новички учатся не хуже, чем на написании кода с нуля, и тратят меньше времени.
- PRIMM — методика Сью Сентанс (Sue Sentance) и коллег: Predict, Run, Investigate, Modify, Make — предскажи, запусти, разберись, измени, напиши своё. Главная мысль в первом шаге: кто сначала запускает, а потом «понимает», соглашается с любым выводом; кто сначала предсказал, видит собственную ошибку в понимании и исправляет её. Этим порядком вы пользуетесь с дня 01.