Аннотации типов и документация кода · МДК.01.01 · разработка программных модулей
Глава 4.2
Типы
в аннотациях
В этой главе показано, как записать тип в аннотации: простые значения, списки, словари и кортежи, собственные классы, вариант «значение или None», итераторы и функции, псевдонимы типов и тип Any. Для каждой записи сказано, как она читается и какие значения под неё подходят. Программы, которые сверяют вызовы с этими записями, разобраны в главе 4.3, а их ответы на неверный код по каждому типу — в главе 4.4.
- Категория
- Синтаксис
- Уровень
- Начальный
- Опирается на
- Глава 4.1
- Зачем это знать
- Записать и прочитать тип любого значения из проекта
§1Простые типы
Простые типы записываются своими именами — теми же, что используются в коде для преобразований: int(...), str(...), float(...), bool(...).
| Тип | Какие значения | Примеры аннотаций из проекта |
|---|---|---|
int | целые числа: 0, 125000, -3 | kopeks: int — сумма заказа, width: int = 40 |
float | дробные числа: 4.4, 0.5 | -> float у calculate_average |
str | строки: "Иванов Пётр", "" | name: str у normalize_name, -> str у format_kopeks |
bool | True и False | флаги вида strict: bool = False |
None | отсутствие значения | -> None у print_report |
Запись -> None означает, что функция ничего не возвращает. Её ставят у функций, которые только печатают, записывают файл или меняют переданный объект. В проекте так записаны print_report(lines: list[str]) -> None, sort_orders_in_place(orders: list[Order]) -> None и Shift.add(self, order: Order) -> None.
def is_big(kopeks: int, limit: int = 100_000) -> bool: return kopeks >= limit def share(part: int, total: int) -> float: return part / total def show(name: str) -> None: print(name)
Первая подпись читается так: функция принимает два целых числа и возвращает True или False. Вторая принимает два целых и возвращает дробное число: деление / в Python всегда даёт float. Третья принимает строку и ничего не возвращает.
§2Списки, словари, кортежи, множества
Для коллекции указывают, что в ней лежит. Тип содержимого пишется в квадратных скобках после имени коллекции.
| Запись | Что означает | Пример значения |
|---|---|---|
list[int] | список целых чисел любой длины | [5, 4, 5] — оценки в calculate_average |
list[str] | список строк | строки отчёта в print_report |
list[Order] | список заказов | результат load_orders |
list[list[int]] | список списков целых чисел | [[5, 4], [3, 5, 5]] — оценки по группам |
dict[str, int] | словарь: ключ — строка, значение — целое | {"Иванов Пётр": 168500} — выручка по официантам |
dict[str, str] | строка → строка | {"waiter": "Сидоров Илья", "kopeks": "77200"} — строка CSV-файла |
dict[str, list[int]] | строка → список целых | {"Иванова": [5, 4]} — журнал оценок |
tuple[int, int] | ровно два целых числа | (3, 5) — результат grade_bounds |
tuple[str, int] | ровно два элемента: строка, затем целое | ("Петрова Анна", 248050) |
tuple[int, ...] | сколько угодно целых чисел, в том числе ни одного | (5, 4, 5, 3), () |
set[str] | множество строк без повторов | {"Иванов Пётр", "Петрова Анна"} — официанты смены |
У трёх видов коллекций скобки устроены по-разному:
- Список и множество — в скобках один тип. Он относится ко всем элементам: в
list[int]каждый элемент — целое число. - Словарь — в скобках два типа через запятую: сначала тип ключей, затем тип значений.
- Кортеж — тип указан для каждого места по порядку.
tuple[int, int]— ровно два элемента. Многоточие вtuple[int, ...]означает «любое количество элементов этого типа».
Составную запись читают изнутри наружу. В dict[str, list[int]] значение — список целых чисел, а весь тип — словарь, где по строке лежит такой список.
В таблице ниже для каждой записи дано значение, которое под неё подходит, и значение, которое не подходит.
| Запись | Подходит | Не подходит | Почему |
|---|---|---|---|
list[int] | [5, 4, 5] | [5, "4", 5] | второй элемент — строка |
dict[str, int] | {"Иванов Пётр": 100} | {1: 100} | ключ — число, а записана строка |
dict[str, list[int]] | {"Сидоров": [5]} | {"Сидоров": 5} | значение — число, а записан список |
tuple[int, int] | (3, 5) | (3, 5, 4) | три элемента вместо двух |
tuple[str, int] | ("Петрова Анна", 248050) | (248050, "Петрова Анна") | элементы переставлены |
set[str] | {"Иванов Пётр"} | {"Иванов Пётр", 125000} | один из элементов — число |
from typing import List, Dictdef f(x: List[int]) -> Dict[str, int]:
def f(x: list[int]) -> dict[str, int]:
Импорт не нужен: встроенные коллекции принимают тип в скобках.
Запись с заглавной буквы и импортом из typing встречается в старом коде и в старых статьях. В проектах курса используется запись со встроенными именами.
§3Собственные классы
Класс — тоже тип. Его имя пишется в аннотации так же, как int. Это относится и к классам проекта — Order, Shift, — и к классам стандартной библиотеки: Path из pathlib, date из datetime. Чтобы имя было доступно в файле, класс импортируют.
from pathlib import Path from app.domain.order import Order def load_orders(path: Path) -> list[Order]: ... def revenue_by_waiter(orders: list[Order], top: int = 0) -> dict[str, int]: ...
| Аннотация | Откуда класс | Где в проекте |
|---|---|---|
order: Order | from app.domain.order import Order | Shift.add |
-> list[Order] | то же | load_orders |
path: Path | from pathlib import Path | load_orders, save_report |
day: date | from datetime import date | Shift.__init__ |
Когда тип параметра — класс, редактору доступны его поля: после order. в автодополнении появятся waiter и kopeks. Словарь с такими же ключами под запись Order не подходит: {"waiter": "Петрова Анна", "kopeks": 98050} — это dict, полей .waiter и .kopeks у него нет.
В архиве без аннотаций остались строки вроде from datetime import date в app/domain/shift.py, хотя date в коде не используется. Импорт стоял там ради аннотации day: date. Когда аннотации возвращаются на место, эти импорты снова нужны.
§4Значение или None
Часто функция возвращает значение, а если его нет — None. Например, поиск заказа по имени официанта: заказ найден или не найден. Такой результат записывают через вертикальную черту: Order | None — «заказ или None».
def find_order(orders: list[Order], waiter: str) -> Order | None: for order in orders: if order.waiter == waiter: return order return None
Так же записывают параметр, который можно не передавать. Значение по умолчанию None означает «не передан», и тип параметра — «строка или None»:
def shift_title(day: str | None = None) -> str: if day is None: day = today_label() return format_title(f" Смена {day} ")
Запись Order | None обязывает вызывающий код учесть оба исхода. У None нет поля kopeks, поэтому перед обращением к полю результат сравнивают с None:
found = find_order(orders, "Петрова Анна") if found is not None: print(found.kopeks)
Что ответит проверка, если условие пропустить, показано в главе 4.4, §9.
В старом коде тот же тип записывали как Optional[Order] с импортом из typing. Запись Order | None доступна с Python 3.10 и означает то же самое.
§5Несколько возможных типов
Та же вертикальная черта перечисляет любые варианты: int | str — «целое число или строка». Такой тип называют объединением. Передать можно значение любого из перечисленных типов. Внутри функции обращаться к нему как к строке или как к числу можно только после того, как выяснено, какой вариант пришёл. Обычно это делают через isinstance.
def to_kopeks(value: int | str) -> int: if isinstance(value, str): return int(value) # здесь value — строка return value # сюда доходит только целое число to_kopeks(125) # подходит: int to_kopeks("125") # подходит: str to_kopeks(1.5) # не подходит: float в перечне нет
Объединения используют там, где данные приходят в разном виде. Когда вариант один, объединение только усложняет вызов.
§6Итераторы и функции
Генератор возвращает итератор — объект, из которого значения берут по одному; список целиком при этом не создаётся. Его тип записывают как Iterator[...] с импортом из collections.abc:
from collections.abc import Iterator def read_rows(path: Path) -> Iterator[dict[str, str]]: with open(path, encoding="utf-8", newline="") as file: yield from csv.DictReader(file)
Iterator[dict[str, str]] читается изнутри наружу: элемент — словарь «строка — строка», а функция отдаёт такие элементы по одному.
Функцию, которую передают как аргумент, записывают через Callable. В скобках сначала список типов параметров, затем тип результата: Callable[[Order], int] — «функция, которая принимает заказ и возвращает целое число». Так описывают, например, ключ сортировки.
from collections.abc import Callable def sort_orders(orders: list[Order], key: Callable[[Order], int]) -> list[Order]: return sorted(orders, key=key) def by_sum(order: Order) -> int: return order.kopeks sort_orders(orders, by_sum) # подходит: Order -> int sort_orders(orders, len) # не подходит
Подпись by_sum совпадает с записью: один параметр типа Order, результат — int. Функция len тоже возвращает целое число, но принимает объект, у которого есть длина, а у заказа длины нет.
§7Псевдонимы типов
Когда один и тот же составной тип повторяется в нескольких местах, ему дают имя. Такое имя называют псевдонимом типа. В Python 3.12 для этого есть инструкция type:
type Kopeks = int type Revenue = dict[str, Kopeks] def total(revenue: Revenue) -> Kopeks: return sum(revenue.values())
Kopeks и int — один и тот же тип под двумя именами. Псевдоним меняет только чтение кода: -> Kopeks сообщает единицы измерения, которых не видно в -> int, а Revenue короче, чем dict[str, int]. Копейки от рублей псевдоним не отличает: это одно и то же целое число.
§8Тип Any
Any из модуля typing означает «любой тип». Под такую запись подходит любое значение, и со значением типа Any разрешено всё: передать его куда угодно, вызвать любой метод.
from typing import Any def parse(value: Any) -> int: return value.to_kopeks() parse("125")
У строки нет метода to_kopeks, и при запуске будет AttributeError. Запись Any об этом не предупреждает: она ничего не сообщает о значении. Поэтому Any ставят только там, где тип неизвестен заранее. В работах курса Any не используется.
§9Аннотации переменных и полей
Локальным переменным аннотация обычно не нужна: тип виден из значения. count = 0 — это int, names = ["Иванов Пётр"] — list[str]. Аннотацию ставят, когда из значения тип не ясен. Пустой словарь {} ничего не говорит о будущих ключах и значениях, поэтому в revenue_by_waiter записано totals: dict[str, int] = {}.
В теле класса аннотации работают иначе. У dataclass строка kopeks: int — это объявление поля: по ней создаётся параметр конструктора Order(waiter, kopeks). Без аннотации поля не будет.
@dataclass(frozen=True) class Order: waiter: str # поле заказа kopeks: int # поле заказа
Поэтому в архиве без аннотаций поля Order оставлены как были: это единственное место в проекте, где аннотация меняет поведение программы.
§10Проверьте себя
Как записать «словарь, где ключ — имя официанта, значение — сумма в копейках»?
dict[str, int]. Первым в скобках идёт тип ключа, вторым — тип значения.
Как записать «словарь, где ключ — фамилия, значение — список оценок»?
dict[str, list[int]]. Значение само составное: список целых чисел.
Чем tuple[int, int] отличается от tuple[int, ...]?
Первое — ровно два целых числа, кортеж из трёх под эту запись не подходит. Второе — любое количество целых чисел.
Функция возвращает Order | None. Что нужно сделать перед обращением к result.kopeks?
Сравнить результат с None: if result is not None. В результате может оказаться None, у которого поля kopeks нет.
Что означает Callable[[Order], int]?
Функцию, которая принимает один аргумент типа Order и возвращает целое число. В первых скобках — типы параметров, после запятой — тип результата.
Почему Any не стоит ставить вместо настоящего типа?
Any ничего не сообщает о значении: под него подходит всё, и вызов несуществующего метода обнаружится только при запуске.
Почему у полей dataclass аннотации нельзя убрать?
В dataclass аннотация в теле класса — объявление поля: по ней создаётся параметр конструктора. Без аннотации поле не появится.
§11Шпаргалка по главе
int float str bool
-> None
list[int]
dict[str, int]
tuple[int, int] tuple[int, ...]
set[str]
order: Order
path: Path day: date
класс нужно импортировать
Order | None
int | str
if x is not None: — перед полем
Callable[[Order], int]
Iterator[dict[str, str]]
from collections.abc import …
type Kopeks = int — псевдоним
Any — любой тип
totals: dict[str, int] = {}
Итог главы
Простые типы записываются своими именами: int, float, str, bool, а отсутствие результата — None. У коллекций в квадратных скобках указывают содержимое: list[int], dict[str, int], tuple[int, int] для пары и tuple[int, ...] для набора любой длины. Классы проекта и стандартной библиотеки пишутся так же, как встроенные типы, после импорта. Вертикальная черта перечисляет варианты: с Order | None вызывающий код сначала сравнивает результат с None. Генератор описывают через Iterator, функцию-аргумент — через Callable. Псевдоним типа даёт составному типу имя. Any означает любой тип и ничего не сообщает о значении. В теле dataclass аннотация объявляет поле. В главе 4.3 разобрано, какие программы сверяют код с этими записями, в главе 4.4 — что они отвечают на ошибку в каждом из типов.
- Составные типы и их запись — документация модуля typing.
- Коллекции с типом в скобках — PEP 585, запись через вертикальную черту — PEP 604, инструкция
type— PEP 695.
Типы Go:
как записать тип
В этой главе показано, как в Go записать тип параметра, результата и переменной: простые значения, срезы, словари и массивы, собственные структуры, указатели и вариант «значение или ничего», интерфейсы, функции как значения, именованные типы и тип any. Для каждой записи сказано, как она читается и какие значения под неё подходят. Как компилятор и редактор сверяют код с этими записями, разобрано в главе 4.3, а их ответы на неверный код по каждому типу — в главе 4.4.
- Категория
- Синтаксис
- Уровень
- Начальный
- Опирается на
- Глава 4.1
- Зачем это знать
- Записать и прочитать тип любого значения из проекта
§1Простые типы
Простые типы записываются своими именами. Те же имена служат для преобразования значения: float64(sum) переводит целое число в дробное, string(runes) собирает строку из знаков.
| Тип | Какие значения | Примеры из проекта |
|---|---|---|
int | целые числа: 0, 125000, -3 | kopeks int — сумма заказа, width int |
float64 | дробные числа: 4.4, 0.5 | первый результат CalculateAverage |
string | строки: "Иванов Пётр", "" | name string у NormalizeName |
bool | true и false | второе значение в _, seen := totals[name] |
rune | один знак Unicode: 'П' | срез []rune(word) в NormalizeName |
byte | один байт: 0–255 | срез []byte при записи файла |
error | ошибка или nil, если ошибки нет | последний результат LoadOrders, SaveReport |
У целых и дробных чисел в Go есть варианты по размеру: int8, int32, int64, uint, float32. В прикладном коде обычно пишут int и float64; остальные нужны, когда размер числа задан извне — форматом файла или протоколом.
В отличие от Python, числа разных типов в Go не смешиваются. Сложить int и float64 нельзя, даже если оба значения — числа: одно из них сначала явно переводят в тип другого. Отсюда и поведение деления. Деление двух int даёт int, дробная часть отбрасывается: 7 / 2 равно 3. Чтобы получить 3.5, делят float64(7) / 2. Поэтому в CalculateAverage сумма переводится в float64 до деления.
func isBig(kopeks, limit int) bool { return kopeks >= limit } func share(part, total int) float64 { return float64(part) / float64(total) } func show(name string) { fmt.Println(name) }
Первая подпись читается так: функция принимает два целых числа и возвращает true или false. Вторая принимает два целых и возвращает дробное число; оба целых переводятся в float64, иначе доля 1 от 3 получилась бы 0. Третья принимает строку и ничего не возвращает: после скобок ничего не стоит.
§2Срезы, словари, массивы
Для коллекции указывают, что в ней лежит. Тип содержимого пишется сразу после обозначения коллекции, без отдельных скобок вокруг него.
| Запись | Что означает | Пример значения |
|---|---|---|
[]int | срез целых чисел любой длины | []int{5, 4, 5} — оценки в CalculateAverage |
[]string | срез строк | строки отчёта в PrintReport |
[]domain.Order | срез заказов | результат LoadOrders |
[][]int | срез срезов целых чисел | [][]int{{5, 4}, {3, 5, 5}} — оценки по группам |
map[string]int | словарь: ключ — строка, значение — целое | totals в RevenueByWaiter |
map[string]string | строка → строка | строка CSV-файла в ReadRows |
map[string][]int | строка → срез целых | {"Иванова": {5, 4}} — журнал оценок |
[2]int | массив ровно из двух целых | [2]int{3, 5} |
map[string]bool | множество строк: ключи без повторов | официанты смены: {"Иванов Пётр": true} |
Как устроены эти записи:
- Срез
[]T— последовательность элементов одного типа, длина которой меняется:appendдобавляет элемент в конец. Это основная коллекция Go, аналог списка Python. - Массив
[N]T— последовательность ровно изNэлементов; числоNвходит в тип, и[2]intи[3]int— разные типы. В прикладном коде массивы встречаются редко. - Словарь
map[K]V— тип ключей в квадратных скобках, тип значений сразу за ними. Порядок ключей при переборе не задан, поэтомуRevenueByWaiterвозвращает не словарь, а срез: в срезе порядок «по убыванию суммы» сохраняется. - Множества отдельным типом в Go нет: его заменяет словарь, у которого значения не нужны, —
map[string]boolилиmap[string]struct{}. - Кортежей в Go нет. Пару значений возвращают двумя результатами функции, как в
GradeBounds, или собирают в структуру с именованными полями, какWaiterRevenue.
Составную запись читают слева направо. map[string][]int — «словарь, в котором по строке лежит срез целых чисел».
| Запись | Подходит | Не подходит | Почему |
|---|---|---|---|
[]int | []int{5, 4, 5} | []int{5, "4", 5} | второй элемент — строка |
[]int | []int{5, 4} | []float64{5, 4} | срез другого типа, даже если числа целые |
map[string]int | {"Иванов Пётр": 100} | {1: 100} | ключ — число, а нужна строка |
map[string][]int | {"Сидоров": {5}} | {"Сидоров": 5} | значение — число, а нужен срез |
[2]int | [2]int{3, 5} | [2]int{3, 5, 4} | три элемента вместо двух |
Вторая строка таблицы показывает отличие от Python. Срез []float64 не передаётся туда, где ждут []int, даже если в нём одни целые числа: тип среза определяется записью, а не содержимым.
§3Структуры и типы других пакетов
Структура — тип из нескольких именованных полей, у каждого свой тип. Её объявляют словами type … struct, и после объявления имя структуры пишется в подписи так же, как int. Типы стандартной библиотеки записываются с именем пакета: time.Time из пакета time. Типы своего модуля из другого пакета — тоже с именем пакета: domain.Order. Пакет при этом нужно импортировать.
import ( "example.com/otchet-kafe/internal/domain" ) type WaiterRevenue struct { Waiter string Kopeks int } func LoadOrders(path string) ([]domain.Order, error) { ... } func RevenueByWaiter(orders []domain.Order, top int) ([]WaiterRevenue, error) { ... }
| Запись | Откуда тип | Где в проекте |
|---|---|---|
order Order | тот же пакет domain, импорт не нужен | Shift.Add |
[]domain.Order | import "example.com/otchet-kafe/internal/domain" | LoadOrders, RevenueByWaiter |
[]WaiterRevenue | объявлен в том же пакете services | результат RevenueByWaiter |
day time.Time | import "time" | NewShift, поле Shift.Day |
Путь к файлу в проекте передаётся строкой string, отдельного типа пути, как Path в Python, для этого не заводят. Когда тип параметра — структура, редактору доступны её поля: после order. в автодополнении появятся Waiter и Kopeks. Словарь с такими же ключами под тип domain.Order не подходит: map[string]string{"waiter": "Петрова Анна"} — это словарь, полей .Waiter и .Kopeks у него нет.
Если пакет импортирован, но ни одно его имя в файле не используется, Go файл не соберёт: "time" imported and not used. В Python лишний импорт просто лежит в файле. Поэтому, удаляя последнее упоминание типа time.Time, удаляют и импорт; в VS Code с расширением Go это делается само при сохранении.
§4Значение или ничего: указатели, nil и ok
Часто функция возвращает значение, а если его нет — ничего. Например, поиск заказа по имени официанта: заказ найден или не найден. В Python такой результат записывают Order | None. В Go значения None для структуры нет: у переменной типа Order всегда есть значение, в худшем случае нулевое — пустое имя и сумма 0. Поэтому «ничего» в Go записывают одним из трёх способов.
Способ 1. Второй результат bool. Функция возвращает значение и признак, найдено ли оно. Так устроено чтение из словаря: value, ok := totals[name].
func findOrder(orders []domain.Order, waiter string) (domain.Order, bool) { for _, order := range orders { if order.Waiter == waiter { return order, true } } return domain.Order{}, false } order, ok := findOrder(orders, "Петрова Анна") if ok { fmt.Println(order.Kopeks) }
Способ 2. Указатель. Указатель — значение, которое хранит адрес другого значения в памяти. Тип «указатель на заказ» записывается *domain.Order, адрес переменной берут знаком &: &order. У указателя, который никуда не указывает, значение nil — это и есть «ничего».
func findOrderPtr(orders []domain.Order, waiter string) *domain.Order { for i := range orders { if orders[i].Waiter == waiter { return &orders[i] } } return nil } found := findOrderPtr(orders, "Петрова Анна") if found != nil { fmt.Println(found.Kopeks) }
Перед обращением к полю указатель сравнивают с nil, как в Python результат сравнивают с None. Если этого не сделать, компилятор не возразит: обращение found.Kopeks при found == nil остановит программу при работе. Этот случай разобран в главе 4.4. Указатели нужны и для другого — чтобы функция или метод меняли исходный объект, а не копию. Поэтому методы Shift из главы 4.1 объявлены с получателем *Shift, а NewShift возвращает *Shift.
Способ 3. Ошибка. Если отсутствие значения — сбой, о котором вызывающий код должен знать, возвращают значение и error, как LoadOrders: при ошибке значение не используют.
| Запись результата | Когда выбирают | Как проверяют |
|---|---|---|
(domain.Order, bool) | значения может не быть, и это обычный случай | if ok |
*domain.Order | то же, плюс вызывающий код должен менять найденный объект | if found != nil |
(domain.Order, error) | отсутствие значения — сбой с причиной | if err != nil |
Необязательных параметров в Go нет. Если аргумент можно не задавать, вызывающий код передаёт нулевое значение (0, ""), и функция договаривается считать его «не задано», как top = 0 в RevenueByWaiter. Договорённость записывают в комментарии документации — это разобрано во второй части темы.
§5Несколько возможных типов: интерфейсы
В Python объединение int | str разрешает передать значение любого из перечисленных типов. В Go объединений в подписи обычной функции нет. Его место занимает интерфейс — тип, который задан не перечнем вариантов, а набором методов. Под интерфейс подходит значение любого типа, у которого эти методы есть.
type Priced interface { TotalKopeks() int } func printTotal(item Priced) { fmt.Println(utils.FormatKopeks(item.TotalKopeks())) } printTotal(shift) // подходит: у *domain.Shift есть метод TotalKopeks() int printTotal(125) // не подходит: у int методов нет
Интерфейс Priced читается так: «любое значение, у которого есть метод TotalKopeks без параметров, возвращающий int». Смена *domain.Shift под него подходит, хотя нигде не объявлено, что она реализует Priced: достаточно, чтобы метод был. Внутри функции доступны только методы интерфейса: поле item.Day не прочитать, потому что у произвольного Priced такого поля может не быть.
Самый частый интерфейс в Go — error: под него подходит любой тип с методом Error() string. Поэтому LoadOrders может вернуть и ошибку открытия файла, и ошибку strconv.Atoi, и ErrNoOrders — у всех есть этот метод.
Когда значения действительно приходят в разном виде, например сумма числом или строкой, параметр делают типом any, а внутри выясняют, какой вариант пришёл. Этот приём разобран в §8.
§6Функции и перебор
Функция в Go — тоже значение, и у неё есть тип. Его записывают словом func с типами параметров и результата, без имён: func(domain.Order) int — «функция, которая принимает заказ и возвращает целое число». Так описывают, например, ключ сортировки.
func sortOrders(orders []domain.Order, key func(domain.Order) int) { slices.SortFunc(orders, func(a, b domain.Order) int { return key(a) - key(b) }) } func bySum(order domain.Order) int { return order.Kopeks } sortOrders(orders, bySum) // подходит: func(domain.Order) int sortOrders(orders, utils.NormalizeName) // не подходит: func(string) string
Подпись bySum совпадает с записью: один параметр типа domain.Order, результат — int. У utils.NormalizeName и параметр, и результат — строки. Внутри sortOrders тоже передаётся функция — безымянная: func(a, b domain.Order) int { … }. Её тип func(domain.Order, domain.Order) int требует slices.SortFunc: функция сравнения возвращает отрицательное число, нуль или положительное.
Перебор по одному значению — аналог генератора Python — в Go записывают типом iter.Seq или iter.Seq2 из пакета iter:
import "iter" func ReadRows(path string) iter.Seq2[map[string]string, error] { ... } for row, err := range services.ReadRows("data/smena.csv") { ... }
iter.Seq2[map[string]string, error] читается так: «последовательность пар, в каждой паре словарь "строка — строка" и ошибка». Перебирают её циклом for … range, значения приходят по одному, и весь файл в память не загружается. Квадратные скобки после Seq2 — параметры типа: в них указано, что лежит в последовательности, как в Iterator[dict[str, str]] у Python.
§7Именованные типы и псевдонимы
Составному или часто повторяющемуся типу можно дать имя. В Go для этого две записи, и они ведут себя по-разному:
type Kopeks int // новый тип на основе int type Revenue = map[string]int // псевдоним: другое имя того же типа func total(revenue Revenue) Kopeks { sum := Kopeks(0) for _, kopeks := range revenue { sum += Kopeks(kopeks) } return sum }
Запись без знака равенства — type Kopeks int — создаёт новый тип. Он хранит целое число, но для компилятора это другой тип, чем int: сложить Kopeks и int без преобразования нельзя. Поэтому в total каждое значение словаря переводится вызовом Kopeks(kopeks). Так защищаются от смешивания единиц: если завести ещё и type Rubles int, сложить копейки с рублями по ошибке не получится. У нового типа можно объявить свои методы.
Запись со знаком равенства — type Revenue = map[string]int — псевдоним. Revenue и map[string]int — один и тот же тип под двумя именами, они взаимозаменяемы. Псевдоним меняет только чтение кода, как type Kopeks = int в Python.
Чаще в коде на Go встречается первая запись. Структуры тоже объявляются ею: type Order struct { … } — новый тип на основе структуры.
§8Тип any
any — интерфейс без методов. Под него подходит значение любого типа: число, строка, структура. Старая запись того же типа — interface{}; она встречается в старом коде и в сообщениях компилятора.
В отличие от Any в Python, со значением типа any почти ничего сделать нельзя: у него нет ни полей, ни методов, его нельзя сложить с числом. Чтобы работать с ним, сначала выясняют, что внутри. Это делают утверждением типа — записью value.(int) — или переключателем switch v := value.(type).
func toKopeks(value any) (int, error) { switch v := value.(type) { case int: return v, nil // здесь v — int case string: return strconv.Atoi(v) // здесь v — string default: return 0, fmt.Errorf("неизвестный тип суммы: %T", v) } }
Внутри каждой ветки case переменная v получает тип этой ветки, и компилятор снова проверяет код по нему. Опасна короткая запись value.(int) без проверки: если внутри строка, программа при работе остановится с сообщением interface conversion: interface {} is string, not int. Компилятор такую ошибку заранее не находит, потому что тип значения внутри any известен только при работе. Безопасная форма — с признаком: n, ok := value.(int); при несовпадении ok будет false, а n — нулём.
Поэтому any ставят только там, где тип действительно неизвестен заранее, например при разборе произвольного JSON. В работах курса any не используется.
§9Переменные и нулевые значения
Локальной переменной тип обычно не пишут: его выводит компилятор из значения в записи :=. count := 0 — это int, names := []string{"Иванов Пётр"} — []string. Тип пишут явно в объявлении var, когда значения пока нет или когда нужен не тот тип, что выводится: var share float64 = 1 вместо int.
Переменная, объявленная через var без значения, получает нулевое значение своего типа:
| Тип | Нулевое значение | Что с ним можно делать |
|---|---|---|
int, float64 | 0 | всё, что с числом |
string | "" | всё, что со строкой |
bool | false | всё |
[]T | nil — пустой срез | читать длину, перебирать, добавлять через append |
map[K]V | nil — пустой словарь | читать; записывать нельзя |
*T, error, any | nil | сравнивать с nil |
| структура | все поля нулевые | всё; domain.Order{} — заказ с пустым именем и суммой 0 |
Строка про словарь — частая ловушка. После var totals map[string]int чтение totals["Иванов Пётр"] вернёт 0, а запись totals["Иванов Пётр"] = 100 остановит программу при работе с сообщением assignment to entry in nil map. Поэтому в RevenueByWaiter словарь создаётся сразу непустым значением: totals := map[string]int{}. Срез такой ловушки не имеет: в LoadOrders запись var orders []domain.Order безопасна, потому что append работает и с нулевым срезом.
У полей структуры тип пишется всегда, как у параметров функции: Waiter string, Kopeks int. Выводить его не из чего — в объявлении структуры значений нет.
§10Проверьте себя
Как записать «словарь, где ключ — имя официанта, значение — сумма в копейках»?
map[string]int. В квадратных скобках тип ключа, сразу за ними — тип значения.
Как записать «словарь, где ключ — фамилия, значение — список оценок»?
map[string][]int. Значение само составное: срез целых чисел.
Чем [2]int отличается от []int?
Первое — массив ровно из двух целых чисел, длина входит в тип. Второе — срез любой длины, к нему можно добавлять элементы через append.
Чему равно 7 / 2 в Go и как получить 3.5?
3: деление двух целых даёт целое, дробная часть отбрасывается. Для 3.5 одно из чисел переводят в дробное: float64(7) / 2.
Функция возвращает *domain.Order. Что сделать перед обращением к found.Kopeks?
Сравнить указатель с nil: if found != nil. Если заказ не найден, указатель пустой, и обращение к полю остановит программу.
Что означает func(domain.Order) int?
Тип функции, которая принимает один аргумент типа domain.Order и возвращает целое число.
Чем type Kopeks int отличается от type Kopeks = int?
Первое создаёт новый тип: сложить Kopeks с int без преобразования нельзя. Второе — псевдоним: Kopeks и int один и тот же тип под двумя именами.
Почему после var totals map[string]int нельзя сразу записать totals["x"] = 1?
Нулевое значение словаря — nil, в него нельзя записывать: программа остановится с сообщением assignment to entry in nil map. Словарь создают значением: totals := map[string]int{} или make(map[string]int).
§11Шпаргалка по главе
int float64 string bool
rune byte error
float64(n) — перевод
[]int — срез
map[string]int — словарь
[2]int — массив
type Order struct { … }
[]domain.Order time.Time
(Order, bool) → if ok
*Order → if p != nil
(Order, error) → if err != nil
func(domain.Order) int
iter.Seq2[K, V]
interface { Метод() int }
type Kopeks int — новый тип
type R = map[string]int — псевдоним
any + switch v := x.(type)
Итог главы
Простые типы записываются своими именами: int, float64, string, bool; числа разных типов не смешиваются, и деление целых даёт целое. Срез []T — основная коллекция, словарь — map[K]V, кортежей и множеств отдельными типами нет. Структуры своего и чужих пакетов пишутся с именем пакета. «Значение или ничего» записывают вторым результатом bool, указателем с nil или результатом с ошибкой. Интерфейс задаёт тип набором методов, тип функции — словом func, перебор — iter.Seq2. type Kopeks int создаёт новый тип, type R = … — псевдоним. any принимает всё, но работать с ним можно только после утверждения типа. У нулевого словаря нельзя записывать ключи. В главе 4.3 разобрано, как компилятор и редактор сверяют код с этими записями, в главе 4.4 — что они отвечают на ошибку в каждом из типов.
- Типы, нулевые значения, указатели, интерфейсы, утверждение типа и объявления типов — The Go Programming Language Specification, разделы Types, The zero value, Type assertions, Type declarations.
- Срезы и словари — Go Slices: usage and internals, Go maps in action.
- Перебор функциями-итераторами — документация пакета iter.
- Сообщения
assignment to entry in nil mapиinterface conversionполучены запуском на Go1.27.