Краткое содержание
Занятие по Go посвящено типам данных и управлению потоком выполнения. Деньги в сервисе заказов предлагается хранить в целых копейках в int64, потому что дробные типы дают погрешность. Далее разбираются целые и дробные типы, byte и rune, явные преобразования, переполнение, строки в UTF-8, нулевые значения, объявления через var, := и const с iota. Затем рассматриваются циклы for, ветвления if и switch, а также защита от пустого среза и переполнения. Занятие завершается разбором задачи ProcessOrder и домашним заданием, которое проверяется автотестами.
Главные выводы
- Деньги хранят в целых минимальных единицах (копейках) в int64, а не во float.
- Тип выбирают под данные: диапазон, точность и поведение при переполнении зависят от типа.
- Go не смешивает числовые типы автоматически: преобразования пишутся явно, и потеря точности видна в коде.
- Строка в Go — последовательность байт; для работы с символами используют руны.
- Переменная без значения получает нулевое значение своего типа; iota удобен для перечислений.
- Управление потоком строится на if, for и switch; for покрывает счётчик, условие и перебор.
- Пустые входы и границы типов проверяют до операции, например деление на len.
- Ошибка в Go — обычное возвращаемое значение, которое нужно проверять явно.
Шпаргалка
Деньги
- Хранить в копейках в
int64. При сложении переменныхfloat64со значениями0.1и0.2получается0.30000000000000004, не равное значениюfloat64(0.3). - Пример:
19990 * 3 = 59970коп.
Числовые типы
int8: от -128 до 127;uint8: от 0 до 255.byte=uint8,rune— символ Unicode.int,uint— размер зависит от платформы; на 64-битной системе 64 бита.float32,float64— типы с плавающей точкой; около 15–17 значащих цифр относится кfloat64.- Неявного смешивания типов нет — нужно явное приведение:
float64(b);int(f)отбрасывает дробную часть. - Переполнение переменной происходит молча: после
var x int8 = 127; x++значениеxравно -128. - Для положительных
aиbтипаint8проверка переполнения:if b > math.MaxInt8 - a, а неif a+b >= ...: сумма уже переполнилась.
Строки и руны
len("привет") = 12байт (UTF-8, в этом слове — 2 байта на букву).len([]rune("привет")) = 6символов.
Объявления и нулевые значения
var x int(ноль),x := 1(только внутри функции),const.- Zero values:
0,false,"". const ( A = iota; B; C )даёт0, 1, 2. Новые значения добавляйте в конец блока.
Циклы
for i := 0; i < n; i++ { }for cond { }— аналог whilefor i, v := range s { };_— пропуск значения
Map и if с инициализацией
v, ok := m[k]— если ключа нет,okравенfalse, аvполучает нулевое значение типа значений map.if err := f(); err != nil { }
Switch
- Без
break; нескольких значений в одномcase— через запятую. fallthrough— явный переход в следующий case.
Ошибки и срезы
- Ошибка — последнее возвращаемое значение:
if err != nil. - Целочисленное деление на
len(values)при пустом срезе даёт panic; до деления проверяйтеlen(values) == 0.
Задачи
ProcessOrder: проверкаattempts >= maxAttemptsперед попыткой;attempts++;if value { return true, attempts }; в концеreturn false, attempts.- Пример:
[false, false, true, false],maxAttempts=3→(true, 3). - Статистика
[12000, 7500, 31000, 4500]: min 4500, max 31000, sum 55000, avg 13750. - Категории: меньше 10 000 — small; 10 000–49 999 — regular; от 50 000 — large; отрицательная сумма — ошибка ввода.
- FizzBuzz: сначала проверять делимость на 15 (через switch).
Главы
Организация занятия, домашнее задание и план курса
05:00 — 07:15Занятие открывает блок о типах данных и управлении потоком. Показано, зачем нужна тема и что уже пройдено. Сообщены правила домашнего задания и тема следующего занятия.
Тема занятия
Занятие посвящено типам данных и управлению потоком: переменным, константам, ветвлениям if и switch и циклу for. Первый практический вопрос занятия: в каком типе хранить деньги в заказе.
Зачем это нужно
Выбор типа для хранения денег — практическая задача: она встречается в любом сервисе, который работает с заказами и ценами.
Цели занятия
- Хранить значения в переменных и константах.
- Сравнивать значения и выбирать ветку выполнения.
- Повторять действия циклом.
Главная цель: научиться писать небольшие алгоритмы, которые работают с данными и учитывают их границы.
Что уже пройдено
- Создание модуля.
- Запуск программы.
- Запуск тестов.
- Форматирование кода.
Тесты понадобятся и в домашней работе: по ним видно, правильно ли написана функция.
Домашнее задание
| Пункт | Содержание |
|---|---|
| Задание | Закрепить типы, циклы и ветвления. Не меняя сигнатуры функций и автотесты, решить задачи из репозитория. Проверить преобразования типов и границы значений |
| Сдача | Ссылка на ветку в репозитории |
| Оценивание | Процент пройденных автотестов |
Задачи лежат в репозитории на GitHub, решение кладут в публичный репозиторий. Правильность проверяют автотесты, общие вопросы разбирают на следующем занятии.
Следующее занятие
Функции, указатели, slice, map и struct, а также разбор домашнего задания.
Какой тип выбрать для денег
07:15 — 11:30Обсуждается, какой тип использовать для цены заказа: float64, int или int64. Показано, что дробная арифметика даёт погрешность, поэтому деньги хранят в копейках в целом типе int64.
Какой тип выбрать для цены заказа
Цену можно хранить в float64, int или int64. Мнения слушателей разделились. Чтобы выбрать, важно понять, как каждый тип влияет на точность и диапазон.
Почему не дробные числа
Тип float64 хранит около 15–17 значащих цифр, и не каждое дробное число в нём представимо точно. Для одного заказа погрешность незаметна. В сервисе с сотнями тысяч заказов в день она накапливается, и суммы начинают расходиться.
Посмотрим на это в коде:
package main
import "fmt"
func main() {
a := 0.1
b := 0.2
sum := a + b
fmt.Printf("a: %.17f\n", a)
fmt.Printf("b: %.17f\n", b)
fmt.Printf("sum: %.17f\n", sum)
fmt.Printf("0.3: %.17f\n", 0.3)
fmt.Println(sum == 0.3)
}
Вывод программы:
a: 0.10000000000000001
b: 0.20000000000000001
sum: 0.30000000000000004
0.3: 0.29999999999999999
false
Сумма 0.1 и 0.2 не равна 0.3. Поэтому дробные суммы нельзя сравнивать через ==.
Деньги в целых минимальных единицах
Деньги удобно хранить в копейках. Целые числа складываются и умножаются без погрешности, поэтому цена не теряет точности:
priceKopecks := int64(19990)
quantity := int64(3)
total := priceKopecks * quantity
fmt.Println(total) // 59970
Тип int64 — знаковое 64-битное целое. Он даёт точные значения и вмещает суммы в копейках с большим запасом.
Большие суммы и криптовалюты
У копеек два знака после запятой, а у эфира восемнадцать. Для таких минимальных единиц диапазона int64 может не хватить.
Целые и дробные типы, byte, rune и преобразования
11:30 — 18:20Разбираются целые типы со знаком и без знака, их диапазоны, byte и rune, float32 и float64. Компилятор не смешивает числовые типы автоматически, поэтому преобразования пишутся явно.
Базовые типы
boolхранит логическое значение:trueилиfalse.stringхранит строку в двойных кавычках.- Целые со знаком:
int8,int16,int32,int64иint. - Беззнаковые целые:
uint8,uint16,uint32,uint64иuint. byte,rune,float32иfloat64.
var _ int = 10
var _ uint64 = 10
var _ bool = true
var _ string = "Go"
var _ byte = 255
var _ rune = 'g'
Имя _ — пустой идентификатор. Значение отбрасывается, а переменная нужна только для демонстрации типа.
Целые типы со знаком и без знака
Целый тип со знаком хранит и отрицательные, и положительные числа. int8 принимает значения от -128 до 127.
Беззнаковый тип не хранит отрицательных чисел, зато его положительный диапазон вдвое шире. uint8 хранит значения от 0 до 255.
Разрядность целых растёт от 8 до 64 бит. int64 — самый большой из простых знаковых типов.
Int и uint без разрядности
Если указать просто int или uint, разрядность выберет компилятор при сборке, исходя из целевой архитектуры. Для 64-битной целевой архитектуры это 64 бита, то есть 8 байт. При этом int и int64 остаются разными типами, даже когда их размеры совпадают.
Byte и rune
- Byte — псевдоним (alias) для
uint8. Он ничего не меняет в работе, но делает код понятнее: сразу видно, что значение — байт. - Rune — символ Unicode. Символ пишут в одинарных кавычках:
'g'. Строку пишут в двойных:"Go".
Вещественные типы
Дробные числа хранят float32 и float64. Просто float в Go нет: разрядность нужно указывать явно.
Преобразования типов
Компилятор не смешивает разные числовые типы автоматически. Сложить float64 и int нельзя, пока одно из значений не приведено к типу другого:
var a float64 = 10.2
var b int = 10
// sum := a + b — ошибка компиляции
sum := a + float64(b)
Приводить float64 к int нужно осторожно: дробная часть отбрасывается. Поэтому здесь приводят int к float64, а не наоборот.
var count int = 3
var price float64 = 19.9
total := float64(count) * price
fmt.Println(total)
Явные преобразования дают три преимущества:
- Преобразование видно в коде.
- Потеря точности не происходит незаметно.
- Намерение автора можно проверить при чтении.
Ошибки выбора типа, строки и руны
18:20 — 26:30Неверный выбор типа даёт ошибки на границах: переполнение, потерю дробной части при приведении и неверную длину строки. Строка в Go — последовательность байт, поэтому кириллица занимает больше байт, чем букв; руны позволяют работать с символами.
Неверный выбор типа не всегда заметен: программа компилируется и работает, но ошибается на граничных значениях. Чаще всего встречаются четыре ошибки.
Переполнение целого типа
Целый тип хранит значения только в своём диапазоне. Например, int8 вмещает от −128 до 127, поэтому 10 + 127 даёт −119: результат «перескакивает» через границу. Ошибки при этом не возникает.
В Go переполняются и int, и uint. Поэтому тип выбирают под максимально возможное значение.
Сравнение дробных чисел через ==
Как показано в разделе о деньгах, 0.1 + 0.2 даёт 0.30000000000000004, и сравнение с 0.3 возвращает false. Дробные числа на точное равенство не сравнивают: сравнивают разницу с небольшим допуском. Для денег поэтому и выбирают целый тип.
byte и rune
byte — это uint8, один байт. rune — символ Unicode. Их нельзя путать: символ кириллицы не помещается в byte, а rune для него подходит. Тип должен соответствовать тому, что хранится.
Потеря данных при преобразовании
Преобразование float64 в int отбрасывает дробную часть без ошибки: если a := 10.2, то int(a) даёт 10. Поэтому явное преобразование видно в коде, и его нужно проверять так же внимательно, как выбор типа.
Строка — последовательность байт
Функция len для строки возвращает число байт. Каждая буква слова «привет» в UTF-8 занимает 2 байта.
package main
import "fmt"
func main() {
name := "привет"
fmt.Println(name)
fmt.Println(len(name)) // 12: байты
fmt.Println(len([]rune(name))) // 6: символы
}
Вывод программы:
привет
12
6
[]rune(name) переводит строку в срез рун, и его длина равна числу символов. []byte(name) переводит строку в срез байт, и его длина равна числу байт.
Руны
Руна — символ Unicode. Из зала спросили, похожа ли она на char из C++. Лектор предложил сравнить самостоятельно, так как с C++ почти не работал.
Строку можно явно преобразовать в срез рун или байт, а каждый из этих срезов — обратно в строку вызовом T(x). Прямого преобразования между []rune и []byte нет. Компилятор не выполнит такие преобразования автоматически.
Нулевые значения, объявления и константы
26:30 — 43:00Переменные без значения получают нулевое значение своего типа. Переменные объявляют через var или :=, константы — через const. Для перечислений используется iota — автоматическая нумерация констант.
Переменная, объявленная без значения, не остаётся пустой и не получает null. Она сразу содержит нулевое значение своего типа, и поведение программы становится предсказуемым ещё до явной инициализации.
Нулевые значения базовых типов:
int,uint,float64—0;bool—false;string— пустая строка"";byteиrune—0(уruneэто символ с кодом 0).
var a int // 0
var b uint64 // 0
var f float64 // 0
var c bool // false
var d string // ""
var e byte // 0
В Go нет null: переменная имеет значение своего типа. У перечисленных базовых типов без явной инициализации это 0, false или "", но допустимость такого значения для алгоритма всё равно нужно проверять.
Объявление через var
Оператор var объявляет переменную с указанным типом. Если значение не задано, используется нулевое.
Тип можно не указывать: компилятор выведет его из значения.
var n = 10 // int
var f = 10.0 // float64
Значение должно соответствовать типу. Целой переменной можно присвоить 2.0, потому что это число без дробной части, но не 2.5:
var ok int = 2.0 // компилируется
var bad int = 2.5 // ошибка компиляции
Короткое объявление :=
Оператор := объявляет переменную и сразу присваивает ей значение. Тип выводится из правой части. Работает он только внутри функции: на уровне пакета доступен только var.
package main
import "fmt"
func main() {
count := 3 // объявляем
count = 4 // меняем уже объявленную
count, total := 5, 10 // total новая, count только меняется
fmt.Println(count, total) // 5 10
}
Слева от := должна стоять хотя бы одна новая переменная. Иначе компилятор выдаст ошибку no new variables on left side of :=.
Константы
Константа задаётся оператором const. Её значение известно уже при компиляции, и компилятор подставляет его во все места использования. Константы можно объявлять вне функций.
Бывают два вида:
- нетипизированная константа:
const distance = 100. Её тип определяется контекстом; - типизированная константа:
const limit int = 100. Тип зафиксирован.
package main
import "fmt"
const distance = 100
const limit int = 100
func main() {
var f float64 = 2.5
fmt.Println(distance * f) // 250
// fmt.Println(limit * f) // ошибка: несовпадение типов int и float64
}
Нетипизированная константа distance берёт тип float64 из контекста, поэтому её можно умножить на дробное число. Типизированная limit уже имеет тип int, и умножить её на float64 без явного преобразования нельзя.
Группы констант и iota
Константы удобно объединять в блок const (...). Внутри блока работает счётчик iota.
package main
import "fmt"
func main() {
const (
a = iota
b
c
d
)
fmt.Println(a, b, c, d) // 0 1 2 3
}
Если нумерацию нужно начать со 100, достаточно записать a = iota + 100: тогда значения будут 100 101 102 103.
С битовым сдвигом удобно задавать единицы размера. Пустой идентификатор _ пропускает значение с iota = 0:
const (
_ = iota // пропускаем 0
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
)
Так же задают статусы заказа, чтобы не назначать номера вручную:
const (
StatusPaid = iota // 0
StatusComplete // 1
StatusWaiting // 2
)
Из зала спросили, являются ли эти номера индексами. Это значения констант, назначенные автоматической нумерацией. Слушатель сравнил её с протягиванием ячеек в Excel, и лектор согласился с аналогией.
Где живёт переменная
Переменная внутри функции размещается либо в стеке, либо в куче (heap). Чтобы увидеть решение компилятора, соберите программу с флагом:
go build -gcflags=-m
В выводе будет видно, какие переменные ушли в кучу. Размер int зависит от платформы: на 64-битной системе он занимает 8 байт.
Три формы объявления
| Форма | Где писать | Тип | Пример |
|---|---|---|---|
var x int |
везде, включая уровень пакета | указан явно или выведен | var retries int (0) |
x := value |
только внутри функции | выводится из значения | name := "order-service" |
const x = value |
везде | нетипизированный или явный | const maxRetries = 3 |
Циклы for
43:00 — 48:00Цикл for в Go — единственный цикл, и он покрывает три формы: классический счётчик, условие вместо while и перебор коллекции через range. Форма цикла должна показывать намерение программиста.
На этом занятии рассматриваются три управляющие конструкции Go: if, for и switch. Цикл в языке один, for, и он покрывает все случаи повторения.
Классическая форма: счётчик, условие и шаг
for i := 0; i < 3; i++ {
fmt.Println(i)
}
В заголовке три части, разделённые точкой с запятой:
i := 0— инициализация счётчика;i < 3— условие, которое проверяется перед каждым проходом;i++— шаг, который выполняется после тела цикла.
Переменная i принимает значения 0, 1, 2. Когда условие становится ложным, цикл завершается.
Форма только с условием
Если оставить одно условие, цикл заменяет while из других языков:
for ready == false {
// ... здесь меняется ready
}
range: перебор коллекции
range перебирает слайс и возвращает два значения: индекс и элемент.
arr := []int{1, 2, 3, 4}
for _, value := range arr {
fmt.Println(value)
}
Этот цикл по очереди выводит 1, 2, 3, 4. Индекс здесь не нужен, и вместо него стоит _. Go запрещает объявлять локальные переменные, которые не используются, поэтому _ отбрасывает значение, не создавая лишней переменной.
Если нужен только индекс, достаточно одной переменной: for i := range arr даёт 0, 1, 2, 3.
Какую форму выбрать
| Задача | Форма | Пример |
|---|---|---|
| Выполнить действие заданное число раз | Счётчик | for i := 0; i < 3; i++ |
| Повторять, пока выполнено условие | Только условие | for ready == false |
| Обойти все элементы коллекции | range |
for _, v := range items |
Map, if с инициализацией и ошибка как значение
48:00 — 59:10Map — словарь, по ключу которого можно узнать, есть ли значение. Конструкция if с инициализацией позволяет проверить второе возвращаемое значение. Ошибка в Go — обычное значение, которое функция возвращает и которое нужно проверять явно.
Map: словарь по ключу
Map (словарь, ассоциативный массив) хранит пары «ключ — значение». В Python для этого служит dict. Тип записывают как map[ключ]значение:
mapp := map[int]int{0: 1, 1: 2}
fmt.Println(mapp[0]) // 1
Значение по ключу 0 равно 1, по ключу 1 равно 2.
Отсутствующий ключ
Если ключа нет, программа не падает. Map возвращает нулевое значение типа значения, для map[int]int это 0.
Из одного значения нельзя понять, есть ключ или нет. Запрос mapp[3] вернёт 0, и тот же 0 вернётся, если ключ 3 существует и хранит 0.
Comma-ok: значение и признак
Запрос с двумя переменными возвращает значение и признак наличия ключа:
val, exist := mapp[3]
val— значение по ключу, а если ключа нет, нулевое значение типа;exist—bool:true, если ключ есть в map, иfalse, если нет.
Имя второй переменной выбирается произвольно. Чаще всего её называют ok или exist.
| Запрос | val |
exist |
|---|---|---|
mapp[1] |
2 |
true |
mapp[3] |
0 |
false |
If с инициализацией
Конструкция if в Go может содержать короткое выражение перед условием, отделённое точкой с запятой:
if инициализация; условие {
// ...
}
Сначала выполняется инициализация, затем проверяется условие. Переменные, объявленные в инициализации, видны в блоках if и else, но за их пределами не доступны.
mapp := map[int]int{0: 1, 1: 2}
if val, exist := mapp[3]; exist {
fmt.Println(val)
} else {
fmt.Println(val)
}
Ключа 3 в map нет, поэтому exist равно false, и выполняется else. Переменная val в этом блоке доступна и содержит 0. Программа выведет 0.
Условия: &&, ||, !
Условия объединяют логическими операторами:
&&— «и», оба условия истинны;||— «или», хотя бы одно истинно;!— отрицание, аналогnotв Python.
mapp := map[int]int{0: 1, 1: 2, 3: 1}
if val, exist := mapp[3]; exist && val == 1 {
fmt.Println("ключ 3 есть, значение 1")
}
Если значение по ключу не нужно, его заменяют пустым идентификатором _. Он отбрасывает значение:
if _, exist := mapp[5]; !exist {
fmt.Println("ключа 5 нет")
}
Ошибка как значение
В Go нет try/catch. Ошибка — обычное значение типа error, которое функция возвращает наряду с результатом, обычно последним. Если всё прошло успешно, ошибка равна nil. Поэтому её проверяют явно:
package main
import (
"errors"
"fmt"
)
func checkAmount(amount int) error {
if amount < 0 {
return errors.New("сумма не может быть отрицательной")
}
return nil
}
func main() {
if err := checkAmount(-5); err != nil {
fmt.Println("ошибка:", err)
}
}
Программа выведет ошибка: сумма не может быть отрицательной. Функция checkAmount при отрицательной сумме возвращает ошибку, а при корректной — nil, и тогда блок if пропускается.
Такой подход делает обработку видимой в коде: по строке вызова и проверке err != nil сразу понятно, где может возникнуть сбой. Если функция возвращает значение и ошибку, то при ошибке первое значение обычно нулевое, а проверять нужно именно err.
Подробно работу с ошибками разберём на следующем занятии.
Ветвление через switch
59:10 — 01:04:10Switch сравнивает значение с перечисленными вариантами и выполняет только одну подходящую ветку. Break не нужен, проваливаться в следующий case по умолчанию не нужно.
Как работает switch
switch сравнивает значение с вариантами case и выполняет только ту ветку, которая подошла. Аналог в Python 3.10 — match-case, в большинстве других языков — switch-case.
status := "paid"
switch status {
case "new":
fmt.Println("создан")
case "paid", "shipped":
fmt.Println("в работе")
default:
fmt.Println("неизвестен")
}
При status := "paid" выведется «в работе».
Несколько значений и default
- Несколько значений в одном
caseперечисляются через запятую. Ветка выполняется для любого из них. defaultвыполняется, если ни одинcaseне подошёл. Его можно не писать.
Break не нужен
Если всё же нужно выполнить следующий case, пишут fallthrough явно, последним оператором ветки:
switch status {
case "paid":
fmt.Println("оплачен")
fallthrough
case "shipped":
fmt.Println("в работе")
}
Для "paid" выведутся обе строки.
Сложные условия
Если после switch ничего не указано, каждый case содержит логическое условие. Проверка идёт сверху вниз, поэтому выполнится первая истинная ветка:
total := 25_000
switch {
case total < 10_000:
fmt.Println("small")
case total < 50_000:
fmt.Println("regular")
default:
fmt.Println("large")
}
Здесь выведется regular. Вторую часть темы, применение switch в конкурентном коде, разберём в конце курса.
Пустой срез и защита от переполнения
01:04:10 — 01:08:50Главная проблема алгоритмов — неверное предположение о данных. Пустой срез приводит к делению на ноль, а переполнение целого типа обходят проверкой границ до операции.
Пустой срез и деление на ноль
Главная проблема алгоритмов — неверное предположение о данных. Посмотрим на подсчёт среднего:
values := []int{}
sum := 0
for _, v := range values {
sum += v
}
avg := sum / len(values) // panic
Цикл по пустому срезу не выполняется, sum остаётся нулём, а len(values) равен 0. Целочисленное деление на ноль вызывает панику (panic): программа аварийно останавливается. Компилятор эту ошибку не видит, потому что заранее не знает, какие данные придут.
Защита — проверить длину до деления и вернуть ошибку:
if len(values) == 0 {
return 0, errors.New("пустой срез")
}
// ... расчёт среднего
Переполнение целого типа
Тип int8 хранит значения от -128 до 127:
var a int8 = 10
var b int8 = 127
sum := a + b // 137 не помещается в int8
Go не выдаёт ошибку: значение молча «заворачивается» и становится -119. Такой баг проявляется позже, например в балансе продавца.
Границы типа берут из пакета math: math.MaxInt8 (127), math.MaxInt64 и другие константы. Числа вручную не записывают.
Перехват паники
Встроенная функция recover перехватывает panic внутри отложенной функции (defer). Это похоже на try-catch в других языках, но работает дорого по ресурсам. Поэтому в Go ошибку обычно возвращают как значение, и вызывающий код обрабатывает её явно.
Ошибки как значения, путь алгоритма и задание
01:08:50 — 01:14:40Лектор объясняет, почему в Go ошибки возвращают явно, а не через исключения. Затем показан путь от входных данных к результату и формулируется домашнее задание.
Ошибка в Go — обычное значение. Функция, которая может не выполниться, возвращает её последним результатом, а вызывающий код проверяет её явно. Исключений и конструкции try/catch в языке нет.
На вопрос из зала, как обходиться без try/catch, ответ такой: сразу после вызова функции проверяют ошибку и решают, что делать дальше.
package main
import "fmt"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("деление на ноль")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("ошибка:", err)
return
}
fmt.Println(result)
}
Код читается сверху вниз, а возможность ошибки видна в сигнатуре (int, error).
Сравнение с Python
В Python ошибки часто не указаны в документации библиотек. О них узнают при запуске, когда программа падает. В Go возможная ошибка записана в сигнатуре функции, а её обработка — в коде.
В Go есть и аварийное завершение (panic), его можно перехватить, но это несёт накладные расходы. Поэтому предсказуемые ситуации, например деление на ноль, проверяют заранее и возвращают как значение.
Путь алгоритма
Любой алгоритм на этом занятии проходит шесть шагов:
- Выбрать тип данных.
- Объявить данные.
- Проверить условие.
- Пройти циклом.
- Разобрать
switch. - Обработать границы.
Шаг 6 часто пропускают, хотя именно на границах программы чаще всего ломаются.
Задание для практики
Задачи лежат в репозитории. Сигнатуры функций и автотесты менять нельзя, цель — добиться PASS при запуске go test ./....
- FizzBuzz через switch. Числа от 1 до 30; делимость проверяйте через остаток от деления
%. - Min, max и сумма среза. Пройдите срез циклом
range. - Среднее без потери точности. Целочисленное деление отбрасывает дробную часть, поэтому сумму приводят к
float64до деления:float64(sum) / float64(len(values)). - Пустой вход. Проверьте длину среза до обращения к элементам и до деления. Верните ошибку вместо паники.
- Автотесты до PASS. Все проверки в файле тестов должны пройти.
Разбор задачи ProcessOrder
01:14:40 — 01:23:00Задача ProcessOrder просматривает результаты попыток слева направо и останавливается на первом успехе или при достижении лимита. Разобраны ошибка в первом варианте и её исправление.
Условие задачи
Функция ProcessOrder(results []bool, maxAttempts int) (success bool, attempts int) обрабатывает повторные попытки заказа. Каждый элемент results — результат одной попытки: true означает успех, false означает ошибку. Попытки просматриваются слева направо.
Функция останавливается в одном из трёх случаев:
- встретился первый
true; - число попыток достигло
maxAttempts; - список
resultsзакончился.
Возвращаемые значения:
successравенtrueтолько в том случае, если встретилсяtrue;attempts— число просмотренных результатов, включая успешный.
Элементы после успешной попытки не учитываются. При maxAttempts <= 0 или пустом списке функция возвращает (false, 0).
| results | maxAttempts | Результат | Почему |
|---|---|---|---|
[false, false, true, false] |
3 | (true, 3) |
успех на третьей попытке, четвёртая не просматривается |
[false, false, true] |
2 | (false, 2) |
лимит достигнут раньше успеха |
[false, false, false] |
3 | (false, 3) |
список закончился |
[true, false] |
0 | (false, 0) |
лимит нулевой, ни одна попытка не выполняется |
[] |
5 | (false, 0) |
список пуст |
Тесты
Тест TestProcessOrder — таблица из шестнадцати случаев. В каждой строке заданы входные данные, лимит и ожидаемый результат. Функция считается верной, если проходят все строки. Запуск выполняется командой go test ./... в папке задачи.
Заготовка из условия содержит только комментарий TODO и возвращает false, 0.
Решение
Первая версия решения:
for _, value := range results {
if attempts >= maxAttempts {
break
}
attempts = attempts + 1
if value {
return true, attempts
}
}
return false, 0
Цикл работает так:
for _, value := range resultsперебирает результаты. Индекс здесь не нужен, поэтому стоит_.- Перед каждой попыткой проверяется
attempts >= maxAttempts. Если лимит достигнут, выполняетсяbreak. Проверка стоит до увеличения счётчика, поэтомуattemptsникогда не превышает лимит. - Счётчик увеличивается на единицу. Если результат
true, функция сразу возвращаетtrue, attempts. Счётчик уже учитывает успешную попытку, а следующие элементы не просматриваются. - Если цикл завершился без успеха, то есть по
breakили по концу списка, выполняется строка после цикла.
Отдельной проверки на пустой список и нулевой лимит не нужно. При пустом списке цикл не выполняется. При maxAttempts <= 0 условие attempts >= maxAttempts выполняется уже на первой итерации.
Имена success и attempts объявлены в сигнатуре, поэтому внутри функции их не объявляют. attempts стартует с нуля, success — с false, и строка attempts = attempts + 1 работает без :=.
Ошибка первой версии
Последняя строка return false, 0 неверна. Когда цикл завершается без успеха, функция возвращает 0 попыток, хотя попытки были выполнены. Тесты показывают ошибку:
--- FAIL: TestProcessOrder/no_success
ProcessOrder() = (false,0), want (false,3)
Падают четыре случая: no_success, limit_shorter, results_exhausted, one_allowed_failure. Во всех них успеха нет. Случаи с true проходят, поэтому ошибка незаметна, если проверять только успешные сценарии.
Исправление — вернуть накопленный счётчик:
return false, attempts
После этой замены все тесты проходят.
Вопрос для пары: break, continue и for condition
breakпрекращает цикл. Он нужен, когда дальнейшие шаги не имеют смысла, как при достижении лимита попыток.continueпропускает текущую итерацию и переходит к следующей. В этой задаче он не нужен: все просмотренные попытки учитываются, пропускать нечего.for condition— цикл только с условием, без счётчика и диапазона. Он подходит, когда число повторов заранее неизвестно, например, пока не достигнут лимит или не получен успех.
Остальные задачи, рефлексия и итоги
01:23:00 — 01:29:22Разобраны условия оставшихся задач практики: FizzBuzz, статистика по суммам и категории заказа. Подведены итоги занятия и сообщена тема следующего.
Задача 1. FizzBuzz
Для чисел от 1 до 30 нужно вывести FizzBuzz, если число делится на 3 и на 5, Fizz — если только на 3, Buzz — если только на 5, и само число в остальных случаях.
Делимость проверяют через остаток от деления: число n делится на d, если n % d == 0.
Число, которое делится на 15, делится и на 3, и на 5. Поэтому проверку на 15 ставят первой:
Решение через switch без выражения: каждый case здесь — отдельное логическое условие.
for i := 1; i <= 30; i++ {
switch {
case i%15 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
Задача 2. Статистика по суммам
Срез сумм в копейках непустой. Минимум и максимум находят одним проходом: первый элемент берут как начальное значение, каждый следующий сравнивают с ним. Сумма накапливается в той же петле.
Среднее возвращают как float64. Деление целых чисел отбрасывает дробную часть, поэтому сумму и длину приводят к float64 до деления:
avg := float64(sum) / float64(len(values))
Пустой срез обрабатывают отдельно: проверка len(values) == 0 стоит перед циклом, и функция сразу возвращает ошибку или сообщение.
Задача 3. Категория заказа
Границы категорий заданы для целых копеек, поэтому промежутков между ними нет: 49_999 и 50_000 — соседние значения. Подчёркивание в 10_000 — разделитель разрядов, на значение оно не влияет.
Отрицательную сумму проверяют первой и не продолжают расчёт. Порядок case важен: когда срабатывает sum < 50_000, значение уже не меньше 10 000.
switch {
case sum < 0:
fmt.Println("ошибка: отрицательная сумма")
case sum < 10_000:
fmt.Println("small")
case sum < 50_000:
fmt.Println("regular")
default:
fmt.Println("large")
}
Задачи 4 и 5
Задачи 4 и 5 из списка простые. Пустой вход обрабатывается проверкой длины до вычислений, как в задаче 2. Для задачи 5 запускают go test ./..., смотрят, какие кейсы падают, и правят код. Сигнатуры функций и автотесты менять нельзя.
Задания из репозитория, с более сложными условиями, выполняются по желанию.
Рефлексия
Ответы на вопросы со слайда:
var |
:= |
|
|---|---|---|
| Где писать | в функции и вне её | только внутри функции |
| Тип | можно указать явно: var n int |
выводится из значения |
| Без значения | var n int даёт 0 |
нельзя, значение обязательно |
- Тип для денег:
int64в копейках. Целая арифметика точна, аfloat64даёт погрешность:0.1 + 0.2не равно0.3. - Формы цикла: классический счётчик, условие вместо
whileиrangeдля перебора. switch: выбирает одну подходящую ветку,breakне нужен.
На следующем занятии разберём функции, указатели, slice, map и struct, а также задание.
Ключевые цитаты
«В банках или там, где проходят заказы, например в каком-нибудь маркетплейсе, проходят сотни тысяч заказов в день, и суммы скачут. Поэтому даже самая малая неточность приведёт к ошибке, так как она имеет накопительный эффект.»
«Точно так же, как мы int превратили во float, или можно было float в int, но тогда отрежется часть после запятой.»
«Из-за этого нужно тщательно выбирать тип и следить за тем, чтобы он не переполнился.»
«Ошибка — это значение, оно должно всегда быть возвращено.»
«Программа упадёт. Поэтому здесь нам нужно добавить условие и проверить, равна ли длина нулю. Если она равна нулю, то мы эту операцию не выполняем.»
Глоссарий
Вопросы для самопроверки
-
Почему для хранения цены заказа не рекомендуется использовать float64?
Показать ответ
Тип float64 не все дробные числа хранит точно. Например, сумма переменных float64 со значениями 0.1 и 0.2 не равна значению float64(0.3), и такие ошибки могут накапливаться при большом количестве операций. Поэтому деньги хранят в целых минорных единицах, например в копейках, в int64. -
Чему равна стоимость покупки 3 штук по цене 199 руб. 90 коп., если хранить сумму в копейках в int64? Ответ выразить в копейках и в рублях.
Показать ответ
Расчёт: \(19990 \cdot 3 = 59970\) копеек, то есть 599 руб. 70 коп. -
Какой диапазон значений у int8 и uint8?
Показать ответ
int8 хранит значения от -128 до 127, uint8 — от 0 до 255. Тип byte является синонимом uint8. -
Почему Go не складывает float64 и int без преобразования?
Показать ответ
Go не смешивает числовые типы автоматически. Так видно, где может потеряться точность, и приведение записывается в коде явно, например float64(b). Приведение float64 к int отбрасывает дробную часть. -
Почему конструкция type byte = uint8 полезна, хотя по сути это один и тот же тип?
Показать ответ
Имя byte показывает, что значение используется как байт данных, а не как число. Компилятор видит одно и то же, а читающему код понятнее намерение. -
Что выведет len("привет") и len([]rune("привет"))?
Показать ответ
len("привет") вернёт 12, потому что строка хранится в UTF-8, и каждая буква этого слова занимает два байта. []rune("привет") даёт срез из 6 символов, и len вернёт 6. -
Какие значения по умолчанию получат переменные, объявленные без присваивания: var a int, var c bool, var d string?
Показать ответ
a получит 0, c получит false, d получит пустую строку "". Это zero values. -
Какие значения получат A, B, C в блоке const (A = iota; B; C)? Что произойдёт, если вставить новую константу в середину блока?
Показать ответ
A = 0, B = 1, C = 2. Вставка в середину сдвигает значения следующих констант, поэтому новые статусы лучше добавлять в конец блока. -
Назовите три формы цикла for в Go.
Показать ответ
Классическая форма for i := 0; i < n; i++ { }, форма только с условием for cond { } и форма перебора for i, v := range s { }. -
Дан map m := map[int]int{0: 1, 1: 2}. Что вернут значения v, ok := m[3] и каким будет результат проверки if ok?
Показать ответ
v получит нулевое значение 0, ok получит false. Блок if ok не выполнится, так как ключа 3 в map нет. -
Почему в switch не нужен break, а в C++ или JavaScript он нужен?
Показать ответ
В Go после выполнения подходящего case выполнение сразу выходит из switch и не проваливается в следующий case. В C++ и JavaScript без break выполнение переходит к следующему case, поэтому break ставят явно. -
Функция ProcessOrder получает результаты [false, false, true] и maxAttempts = 2. Какой будет результат?
Показать ответ
Результат (false, 2). Первая попытка неудачная, вторая тоже, затем attempts достигает maxAttempts, и цикл прерывается до проверки третьего элемента. -
Почему перед вычислением среднего значения нужна проверка len(values) == 0?
Показать ответ
Для пустого среза len равно 0, и деление на него вызывает panic, то есть аварийное завершение программы. Проверка позволяет вернуть ошибку как значение и избежать краха. -
Для набора сумм [12000, 7500, 31000, 4500] найдите минимум, максимум, сумму и среднее. Как классифицировать заказ на 49 999 копеек?
Показать ответ
Минимум 4500, максимум 31000, сумма 55000, среднее \(55000 / 4 = 13750\). Сумма 49 999 копеек попадает в категорию regular, так как диапазон regular — от 10 000 до 49 999 включительно. -
Почему в Go ошибка возвращается как значение, а не через исключения try/catch?
Показать ответ
Ошибка видна в сигнатуре функции и обрабатывается явно сразу после вызова через if err != nil. Это делает поведение программы предсказуемым и упрощает чтение кода.
Что изучить дополнительно
-
Пакет math/big для чисел больше 64 битЛектор упомянул его в связи с криптовалютами, где нужна точность до 18 знаков после запятой, но подробно не раскрыл.
-
Срезы (slice), map и structСрезы и map только бегло показаны в цикле и проверке наличия ключа; лектор сказал, что подробно разберёт их на следующем занятии.
-
Указатели и функцииТема следующего занятия, которая понадобится для работы с функциями в домашнем задании.
-
Флаги сборки и анализ размещения переменных в куче и стекеЛектор показал, что флагом сборки можно узнать, куда попала переменная, но не объяснил механизм.
-
Интерфейсы и switch по типамЛектор отложил вторую часть switch, связанную с интерфейсами, на следующие занятия.
-
Горутины и конкурентность в switchЛектор сказал, что основное свойство switch для конкурентности будет рассмотрено в конце курса.
Упоминания
-
Python инструментИспользуется для сравнения: match-case, словари dict и обработка строк, которые сравниваются с поведением Go.
-
JavaScript инструментСравнивается с Go в поведении switch: в JavaScript нужен break, чтобы не провалиться в следующий case.
-
C++ инструментУпоминается в сравнении поведения switch, типов byte и rune и переполнения беззнаковых целых.
-
math (пакет стандартной библиотеки) инструментСодержит константы максимальных значений типов, например math.MaxInt8, для проверки переполнения.
-
math/big инструментУпомянут как пакет для очень больших целых чисел, которые не помещаются в 64 бита, например для криптовалют.
-
go test инструментКоманда запуска автотестов в домашнем задании, по которым проверяется правильность решения.
-
Excel инструментПриведён пример: автоматический индекс в константах iota напоминает протягивание номеров строк в Excel.
-
Репозиторий на GitHub с заданиями ссылкаМесто, где находятся домашние задания с тестами и где студенты публикуют своё решение в отдельной ветке.
-
Расширение для просмотра Markdown в редакторе инструментЛектор предложил его для удобного чтения файлов README без разметки.