Тема 04 · Аннотации типов и документация кода

Аннотации типов и документация кода · МДК.01.01 · разработка программных модулей

Глава 4.2

4.2

Типы
в аннотациях

В этой главе показано, как записать тип в аннотации: простые значения, списки, словари и кортежи, собственные классы, вариант «значение или None», итераторы и функции, псевдонимы типов и тип Any. Для каждой записи сказано, как она читается и какие значения под неё подходят. Программы, которые сверяют вызовы с этими записями, разобраны в главе 4.3, а их ответы на неверный код по каждому типу — в главе 4.4.

Категория
Синтаксис
Уровень
Начальный
Опирается на
Глава 4.1
Зачем это знать
Записать и прочитать тип любого значения из проекта

§1Простые типы

Простые типы записываются своими именами — теми же, что используются в коде для преобразований: int(...), str(...), float(...), bool(...).

ТипКакие значенияПримеры аннотаций из проекта
intцелые числа: 0, 125000, -3kopeks: int — сумма заказа, width: int = 40
floatдробные числа: 4.4, 0.5-> float у calculate_average
strстроки: "Иванов Пётр", ""name: str у normalize_name, -> str у format_kopeks
boolTrue и 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.

proba.pyподписи с простыми типами
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, Dict
def f(x: List[int]) -> Dict[str, int]:

→
Запись в Python 3.9 и новее

def f(x: list[int]) -> dict[str, int]:

Импорт не нужен: встроенные коллекции принимают тип в скобках.

Запись с заглавной буквы и импортом из typing встречается в старом коде и в старых статьях. В проектах курса используется запись со встроенными именами.

§3Собственные классы

Класс — тоже тип. Его имя пишется в аннотации так же, как int. Это относится и к классам проекта — Order, Shift, — и к классам стандартной библиотеки: Path из pathlib, date из datetime. Чтобы имя было доступно в файле, класс импортируют.

app/services/orders.pyкласс Order в аннотациях
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: Orderfrom app.domain.order import OrderShift.add
-> list[Order]то жеload_orders
path: Pathfrom pathlib import Pathload_orders, save_report
day: datefrom datetime import dateShift.__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».

proba.pyфункция, которая может ничего не найти
def find_order(orders: list[Order], waiter: str) -> Order | None:
    for order in orders:
        if order.waiter == waiter:
            return order
    return None

Так же записывают параметр, который можно не передавать. Значение по умолчанию None означает «не передан», и тип параметра — «строка или None»:

proba.pyнеобязательный параметр
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:

proba.pyсначала сравнение с 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.

proba.pyсумма пришла числом или строкой
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:

app/services/importer.pyгенератор
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] — «функция, которая принимает заказ и возвращает целое число». Так описывают, например, ключ сортировки.

proba.pyфункция как аргумент
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:

proba.pyпсевдонимы для сумм и выручки
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 разрешено всё: передать его куда угодно, вызвать любой метод.

proba.pyпараметр типа 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). Без аннотации поля не будет.

app/domain/order.pyполя dataclass
@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 — что они отвечают на ошибку в каждом из типов.

Откуда взяты факты
  1. Составные типы и их запись — документация модуля typing.
  2. Коллекции с типом в скобках — PEP 585, запись через вертикальную черту — PEP 604, инструкция type — PEP 695.
4.2

Типы Go:
как записать тип

В этой главе показано, как в Go записать тип параметра, результата и переменной: простые значения, срезы, словари и массивы, собственные структуры, указатели и вариант «значение или ничего», интерфейсы, функции как значения, именованные типы и тип any. Для каждой записи сказано, как она читается и какие значения под неё подходят. Как компилятор и редактор сверяют код с этими записями, разобрано в главе 4.3, а их ответы на неверный код по каждому типу — в главе 4.4.

Категория
Синтаксис
Уровень
Начальный
Опирается на
Глава 4.1
Зачем это знать
Записать и прочитать тип любого значения из проекта

§1Простые типы

Простые типы записываются своими именами. Те же имена служат для преобразования значения: float64(sum) переводит целое число в дробное, string(runes) собирает строку из знаков.

ТипКакие значенияПримеры из проекта
intцелые числа: 0, 125000, -3kopeks int — сумма заказа, width int
float64дробные числа: 4.4, 0.5первый результат CalculateAverage
stringстроки: "Иванов Пётр", ""name string у NormalizeName
booltrue и 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. Пакет при этом нужно импортировать.

internal/services/orders.goтип 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.Orderimport "example.com/otchet-kafe/internal/domain"LoadOrders, RevenueByWaiter
[]WaiterRevenueобъявлен в том же пакете servicesрезультат RevenueByWaiter
day time.Timeimport "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].

примеррезультат и признак ok
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 — это и есть «ничего».

примеруказатель или 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:

internal/services/importer.goпоследовательность пар
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, float640всё, что с числом
string""всё, что со строкой
boolfalseвсё
[]Tnil — пустой срезчитать длину, перебирать, добавлять через append
map[K]Vnil — пустой словарьчитать; записывать нельзя
*T, error, anynilсравнивать с 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 — что они отвечают на ошибку в каждом из типов.

Откуда взяты факты
  1. Типы, нулевые значения, указатели, интерфейсы, утверждение типа и объявления типов — The Go Programming Language Specification, разделы Types, The zero value, Type assertions, Type declarations.
  2. Срезы и словари — Go Slices: usage and internals, Go maps in action.
  3. Перебор функциями-итераторами — документация пакета iter.
  4. Сообщения assignment to entry in nil map и interface conversion получены запуском на Go 1.27.
Глава 4.2 · Типы в аннотациях · МДК.01.01 Автор: Макаров Максим Николаевич