Аннотации типов и документация кода · МДК.01.01 · разработка программных модулей
Глава 4.1
Аннотация типа:
что это и что она делает
В этой главе разобрано, что такое аннотация типа и как она записывается у параметра, у результата и у переменной. На функциях учебного проекта показано, как читать подпись с аннотациями, что Python делает с аннотацией при запуске программы и что меняется в редакторе, когда аннотации в коде есть.
- Категория
- Понятия
- Уровень
- Начальный
- Опирается на
- Модули и пакеты, тема 02
- Зачем это знать
- Понимать, какие значения принимает функция, не открывая её тело
§1Тип значения
Тип — вид значения, от которого зависит, что с ним можно делать. У числа 5 тип int: его можно складывать и делить. У строки "5" тип str: её можно разрезать и склеить с другой строкой, а сложить с числом нельзя. Узнать тип любого значения можно функцией type: type(5) вернёт <class 'int'>, type("5") — <class 'str'>.
Python определяет тип значения во время работы программы, в момент, когда значение уже получено. Такой подход называют динамической типизацией. Объявлять заранее, какие значения принимает функция, не требуется: в def calculate_average(grades) можно передать список чисел, список строк или одно число. Если значение не подходит, ошибка возникнет только при выполнении той строки, которая с ним не справится.
Пока функция и её вызов лежат в одном файле, это не мешает: тело функции видно рядом. Когда функций десятки и они разнесены по пакетам, как в проекте из темы 02, при каждом вызове приходится открывать чужой модуль и выяснять одно и то же: что передавать и что вернётся. Аннотации типов записывают ответ на этот вопрос прямо в первой строке функции.
§2Проект темы
Трудность из §1 видна только в проекте, где функций много и они лежат в разных модулях. Поэтому все примеры темы взяты из одного такого проекта — «Отчёт кафе». Он читает файл смены с заказами, считает выручку по каждому официанту и печатает отчёт в консоль. Десять модулей разложены по пакетам: domain — заказ и смена, services — чтение файла и расчёты, utils — имена, суммы и дата, ui — печать. Суммы в проекте хранятся в копейках, целыми числами: 125000 означает 1 250,00 ₽.
Проект выдаётся в трёх архивах с одним и тем же кодом на разных этапах темы. Первый — код без аннотаций: на нём видно, каких сведений не хватает при вызове функций. Второй — тот же код с аннотациями. В этой главе оба архива открыты рядом, и каждое свойство аннотаций показано сравнением: что видно в первом архиве и что — во втором. Третий архив, с описаниями функций, нужен во второй части темы.
Скачайте первые два архива и подготовьте каждый к работе одинаково:
- Распакуйте архив. В Проводнике щёлкните по архиву правой кнопкой и выберите Извлечь всё.В Finder дважды щёлкните по архиву: рядом появится папка.В файловом менеджере щёлкните по архиву правой кнопкой и выберите Извлечь или выполните в терминале
unzip имя-архива.zip. - Откройте получившуюся папку в VS Code: File → Open Folder. Открывать нужно папку, в которой лежат
main.pyиrequirements.txt. - Откройте терминал редактора (Ctrl + `) и создайте окружение:
python3 -m venv .venvpy -m venv .venv. - Установите зависимости:
.venv/bin/pip install -r requirements.txt.venv\Scripts\pip install -r requirements.txt. Вместе с pytest поставятся Ruff и mypy. - Запустите проект:
.venv/bin/python main.py.venv\Scripts\python main.py. В терминале появится отчёт по смене.
$ .venv/bin/python main.py> .venv\Scripts\python main.py =========== Смена 21.09.2026 =========== Петрова Анна 2 480,50 ₽ Иванов Пётр 1 685,00 ₽ Сидоров Илья 772,00 ₽ =========== Итого 4 937,50 ₽ ===========
Команды в книге показаны для одной системы. Систему выбирают переключателем Windows · macOS · Linux вверху страницы; выбор запоминается для всех глав темы.
В книге программы окружения вызываются по пути: .venv/bin/python.venv\Scripts\python, .venv/bin/mypy.venv\Scripts\mypy. Такая команда работает в любом терминале, открытом в папке проекта.
Второй способ — один раз активировать окружение: source .venv/bin/activate.venv\Scripts\Activate.ps1 в PowerShell или .venv\Scripts\activate.bat в командной строке. В начале строки терминала появится (.venv), и до закрытия терминала команды пишутся коротко и одинаково во всех системах: python main.py, mypy app, python -m pytest -q. Активация разобрана в главе 2.1.
Оба архива печатают одинаковый отчёт. В первом аннотаций нет, и программа всё равно работает: для запуска аннотации не нужны. Дата в шапке — текущая: проект берёт её из часов компьютера. Раз поведение одинаковое, разница между архивами — только в тексте кода. С неё и начинается разбор.
§3Как записывается аннотация
Откройте в обоих архивах файл app/services/calculator.py и сравните первую строку функции calculate_average:
def calculate_average(grades):
Из подписи видно только имя параметра. Список это, кортеж или одно число, можно узнать, только прочитав тело.
def calculate_average(grades: list[int]) -> float:
Параметр — список целых чисел, результат — дробное число. Тело для этого читать не нужно.
Добавленные во втором архиве : list[int] и -> float — это аннотации. Аннотация типа — запись в коде, которая указывает, значения какого типа ожидаются в параметре, в переменной или в результате функции.
У calculate_average аннотации стоят в двух местах — у параметра и у результата. Всего таких мест три: третье — переменная. Все три показаны на схеме. Для неё взята функция format_title из app/utils/formatter.py, потому что у неё есть и обычный параметр, и параметр со значением по умолчанию, и результат; пример переменной — строка из функции revenue_by_waiter.
->. У переменной — между именем и знаком равенства.| Где | Запись | Читается как |
|---|---|---|
| Параметр | text: str | параметр text принимает строку |
| Параметр с умолчанием | width: int = 40 | целое число; если его не передать, будет 40 |
| Результат | -> str | функция возвращает строку |
| Функция без результата | -> None | функция ничего не возвращает |
| Переменная | totals: dict[str, int] = {} | словарь «строка — целое число», сейчас пустой |
| Атрибут объекта | self.orders: list[Order] = [] | список заказов, сейчас пустой |
В таблице те же три места, что на схеме, и два частных случая: -> None у функции, которая ничего не возвращает, и аннотация атрибута объекта self.orders — она пишется так же, как у переменной.
Кроме места, у аннотаций есть правила оформления; они соблюдаются во всём проекте. После двоеточия ставят один пробел, перед ним — ни одного: text: str. Вокруг стрелки — по пробелу: ) -> str:. У параметра со значением по умолчанию вокруг знака равенства ставят пробелы, если есть аннотация (width: int = 40), и не ставят, если её нет (width=40).
В примерах уже встречались составные записи: list[int] — «список, в котором лежат целые числа», dict[str, int] — «словарь со строковыми ключами и целыми значениями». В квадратных скобках после имени коллекции указывают, что в ней хранится. Для чтения подписей в этой главе этого достаточно; все такие записи разобраны в главе 4.2.
§4Как читать подпись функции
Запись аннотаций разобрана. Теперь можно вернуться к трудности из §1 — что передать функции и что она вернёт — и посмотреть, как подписи с аннотациями отвечают на этот вопрос для всего проекта.
Подпись функции — первая строка её объявления: имя, параметры и результат. С аннотациями подпись отвечает на три вопроса: какие значения передать, в каком порядке и что вернётся. Таблица ниже собрана из второго архива: из каждого модуля выписаны первые строки функций, которые вызываются из других модулей, а справа записано, как эти строки читаются. Подпись читают слева направо: сначала имя, затем каждый параметр с его типом, в конце тип после стрелки.
| Подпись | Что передают | Что получают |
|---|---|---|
def today_label() -> str | ничего | строку с датой, например "21.09.2026" |
def normalize_name(name: str) -> str | строку с именем | строку: имя в виде «Фамилия Имя» |
def format_kopeks(kopeks: int) -> str | целое число копеек | строку "1 250,00 ₽" |
def format_title(text: str, width: int = 40) -> str | строку и, по желанию, ширину | строку ровно заданной ширины |
def calculate_average(grades: list[int]) -> float | список целых чисел | дробное число |
def grade_bounds(grades: list[int]) -> tuple[int, int] | список целых чисел | пару целых: минимум и максимум |
def load_orders(path: Path) -> list[Order] | путь к файлу — объект Path | список заказов |
def revenue_by_waiter(orders: list[Order], top: int = 0) -> dict[str, int] | список заказов и, по желанию, число | словарь «имя — сумма» |
def sort_orders_in_place(orders: list[Order]) -> None | список заказов | ничего: функция меняет переданный список |
def save_report(path: Path, lines: list[str]) -> int | путь и список строк | целое число — сколько строк записано |
def print_report(lines: list[str]) -> None | список строк | ничего: функция только печатает |
Все сведения в правых колонках взяты только из подписей: тела функций для этого не открывались. Значит, по таблице можно составить вызов, не открывая ни одного модуля. Например, load_orders принимает Path и возвращает list[Order], а revenue_by_waiter принимает list[Order]. Поэтому результат первой функции подходит второй без преобразований — так они и вызываются в main.py:
def build_report(path: Path) -> list[str]: orders = load_orders(path) # list[Order] revenue = revenue_by_waiter(orders) # dict[str, int] ...
В таблицу не попали методы класса Shift из app/domain/shift.py: у подписей методов есть особенность. Первый параметр метода, self, аннотации не получает: это сам объект, и его тип известен из класса. Метод __init__ всегда возвращает None, поэтому его подпись в проекте — def __init__(self, day: date) -> None. Класс целиком, без описаний:
class Shift: def __init__(self, day: date) -> None: self.day = day self.orders: list[Order] = [] def add(self, order: Order) -> None: self.orders.append(order) def total_kopeks(self) -> int: return sum(order.kopeks for order in self.orders)
Здесь видны все три места аннотаций сразу. day: date — параметр: в смену передают дату, объект date из модуля datetime. self.orders: list[Order] = [] — атрибут объекта: пустой список, в который потом попадут заказы; без аннотации по пустым скобкам нельзя понять, что в нём будет. -> int у total_kopeks — результат: сумма копеек целым числом. -> None у add означает, что метод меняет объект и ничего не возвращает.
§5Что Python делает с аннотацией
Подписи из §4 сообщают, что функция ожидает. Следующий вопрос — следит ли Python за этими ожиданиями, когда программа работает. Ответ дают опыты в интерактивном Python. Откройте терминал в папке второго архива и запустите .venv/bin/python.venv\Scripts\python. Приглашение >>> означает, что Python ждёт команду; каждая строка выполняется сразу после Enter.
Сначала — где Python держит аннотации. Импортируйте функцию и посмотрите её атрибут __annotations__:
>>> from app.services.calculator import calculate_average >>> calculate_average.__annotations__ {'grades': list[int], 'return': <class 'float'>}
Это обычный словарь: ключи — имена параметров, отдельный ключ 'return' — тип результата. Python сохранил аннотации, когда создавал функцию. Проверяет ли он по ним аргументы, показывают вызовы ниже. В первых трёх вкладках в той же сессии >>> вызвана calculate_average(grades: list[int]) -> float — сначала с подходящим аргументом, затем с двумя неподходящими. Для четвёртой вкладки нужна функция, которой в проекте нет, — сложение двух сумм. Она записана в отдельный файл proba.py рядом с main.py и запущена командой .venv/bin/python proba.py.venv\Scripts\python proba.py. Все выводы получены запуском.
Аннотация при запускепереключите вызов
>>> calculate_average([5, 4, 5, 3, 5]) 4.4
РаботаетПередан список целых чисел, как и записано в аннотации. Функция вернула среднее.
>>> calculate_average([5.5, 4.0]) 4.75
РаботаетАннотация требует целые числа, переданы дробные. Python вызов не остановил: сложение и деление для дробных чисел определены, и функция вернула результат.
>>> calculate_average(['5', '4']) File "app/services/calculator.py", line 4, in calculate_average return round(sum(grades) / len(grades), 2) ^^^^^^^^^^^ TypeError: unsupported operand type(s) for +: 'int' and 'str'
Ошибка внутри функцииВызов начал выполняться, и ошибка возникла в строке с sum: сложить число со строкой нельзя. Сообщение говорит о сложении числа со строкой. О неверном аргументе в нём ничего нет, и строка вызывающего кода, где передано не то, из него не видна.
def add_kopeks(a: int, b: int) -> int: return a + b print(add_kopeks(150, 200)) print(add_kopeks("150", "200"))
350 150200
Неверный результат без ошибкиДля строк оператор + означает склейку, поэтому вместо суммы 350 получилась строка "150200". Программа продолжит работу с неверным значением. Такой случай реален: числа из CSV-файла читаются строками, и в load_orders сумму переводят в число вызовом int(row["kopeks"]).
Из четырёх вызовов следует, что аннотация описывает ожидание, но при запуске его не обеспечивает. Неверный тип либо проходит незамеченным, либо приводит к ошибке внутри функции, далеко от места вызова. Сверку вызовов с аннотациями до запуска выполняют отдельные программы — Pylance в редакторе и mypy в терминале. Работе с ними посвящена глава 4.3.
Ассоциация: разметка на парковке
Линии на асфальте показывают, где место для легковой машины, а где для грузовика. Сами по себе они не мешают грузовику встать на легковое место. Нарушение замечает человек, который обходит парковку и сверяет машины с разметкой.
Аннотация — разметка: она показывает, какое значение ожидается. При запуске Python её не проверяет. Сверку выполняет программа проверки типов, по тексту кода.
Где не совпадаетНа парковке нарушение видно, когда машина уже стоит. Программа проверки находит неверный вызов в тексте, до того как программа запущена.
§6Подсказка редактора
Python аннотации не проверяет, и при запуске они ничего не меняют. Используют их другие программы. Первая из них уже открыта — редактор VS Code: он читает аннотации и показывает их в подсказке у места вызова, без всяких настроек.
Подсказку вызывают наведением указателя на имя функции или клавишами: курсор на имя, затем Ctrl + K и Ctrl + I (на macOS — Cmd вместо Ctrl). Повторите опыт в обоих архивах: откройте main.py, найдите строку 11 — revenue = revenue_by_waiter(orders) — и наведите указатель на revenue_by_waiter. Файл main.py в двух архивах одинаковый, отличается только модуль app/services/orders.py, где записана сама функция. На кадрах сравните строку с параметром orders и тип после ->.
top выведен из значения по умолчанию: top: int = 0. Для orders выводить не из чего, и в подсказке стоит Any — «любой тип». Результат — dict без указания содержимого: что лежит в словаре, неизвестно.
orders: list[Order], результат dict[str, int]. Из места вызова видно, что передать и что вернётся.Строка вызова в двух кадрах одна и та же. Разные сведения в подсказке взялись из подписи функции в app/services/orders.py: в первом архиве там def revenue_by_waiter(orders, top=0):, во втором — подпись с аннотациями из таблицы §4.
§7Автодополнение
Подсказка показывает типы, когда код уже написан. Те же типы редактор использует, пока код набирается, — в автодополнении.
Автодополнение — список, который редактор показывает при наборе: имена переменных, функций, атрибутов и методов, подходящих в этом месте. После точки в нём перечислены атрибуты значения слева от точки. Составить такой список редактор может, только если знает тип значения. Опыт снова в двух архивах: откройте app/services/orders.py, поставьте курсор в конец тела функции sort_orders_in_place — последней в файле, — нажмите Enter и наберите orders[0].: первый элемент списка заказов и точку. Если список не появился сам, вызовите его клавишами Ctrl + Пробел. На macOS это сочетание часто занято переключением раскладки; тогда список вызывают командой Trigger Suggest из палитры команд (Cmd + Shift + P). После опыта отмените набранное: Ctrl + Z.
def sort_orders_in_place(orders):. Тип orders неизвестен, значит, неизвестен и тип его элемента. Редактор отвечает No suggestions — «предложений нет». Имя атрибута придётся набирать по памяти, и опечатка вроде order.kopek обнаружится только при запуске.
def sort_orders_in_place(orders: list[Order]) -> None:. Элемент списка — Order, и первыми в списке стоят его поля kopeks и waiter. Справа — тип выбранного поля: kopeks: int. Ниже идут служебные атрибуты, которые есть у любого объекта, — их имена начинаются и заканчиваются двумя подчёркиваниями.Что аннотации дают при работе с кодом: итог §4–§7
- Вызов без чтения тела функции. Подсказка показывает, какие значения передавать и что вернётся.
- Имена атрибутов из списка. Автодополнение предлагает поля объекта, и
order.kopeksвыбирается из списка; набирать имя по памяти не нужно. - Проверка до запуска. Программы проверки типов сверяют каждый вызов с аннотациями и показывают строку, где передано не то.
Первые два пункта работают сразу, их показали кадры §6 и §7. Третий требует отдельных программ: они разобраны в главе 4.3. Перед этим, в главе 4.2, показано, как записать любой тип, — от int до «значение или None». В главе 4.4 собраны ошибки, которые проверка находит в каждом из этих типов.
§8Частые вопросы
add_kopeks("150", "200") выполняется и возвращает "150200", вызов со строками в calculate_average завершается ошибкой внутри функции. Несовпадения находит программа проверки типов.int(...), как в функции load_orders при чтении файла: kopeks=int(row["kopeks"]).orders не видно, список это или словарь и что в нём лежит. Редактор и программа проверки тип по имени не определяют: в первом архиве у orders тип Any, и автодополнение ничего не предлагает.§9Проверьте себя
Где пишется аннотация у параметра, у результата и у переменной?
У параметра — после имени через двоеточие: text: str. У результата — после закрывающей скобки через стрелку: -> str. У переменной — между именем и знаком равенства: totals: dict[str, int] = {}.
Прочитайте подпись: def is_big(kopeks: int, limit: int = 100_000) -> bool.
Функция принимает целое число копеек и, по желанию, порог — тоже целое число, по умолчанию 100_000. Возвращает True или False. Подчёркивание внутри числа 100_000 Python пропускает, оно только разделяет разряды.
Прочитайте подпись: def save_report(path: Path, lines: list[str]) -> int.
Функция принимает путь — объект Path — и список строк. Возвращает целое число. Что это за число — количество записанных строк, — из подписи не видно: такие сведения пишут в описании функции.
Что вернёт calculate_average([5.5, 4.0]), если в аннотации записано list[int]?
4.75. Python при запуске аннотации не проверяет, а для дробных чисел сложение и деление определены.
Почему у self в методах класса нет аннотации?
self — сам объект, и его тип известен из класса, в котором записан метод. Редактор и программа проверки выводят его сами.
Почему в первом архиве после orders[0]. редактор ничего не предлагает?
У параметра orders нет аннотации, и тип его элементов неизвестен. Перечень атрибутов редактор берёт из типа значения, а типа нет.
§10Шпаргалка по главе
text: str
width: int = 40
-> str
-> None — ничего не возвращает
totals: dict[str, int] = {}
self.orders: list[Order] = []
def add(self, order: Order) -> None
self — без аннотации
хранятся в __annotations__
не проверяются
подсказка: Ctrl+K, Ctrl+I
автодополнение: Ctrl+Пробел
Итог главы
Аннотация типа записывает в подпись функции, какие значения она принимает и что возвращает. По подписям с аннотациями можно составить вызов, не открывая модуль. При запуске Python аннотации не проверяет: неверный тип проходит незамеченным или приводит к ошибке внутри функции. Редактор использует аннотации в подсказке и в автодополнении, а сверку вызовов до запуска выполняют программы проверки типов из главы 4.3.
- Синтаксис аннотаций функций — PEP 3107, переменных — PEP 526, оформление — PEP 8, Function Annotations.
- Назначение аннотаций и то, что интерпретатор их не проверяет, — PEP 484, Type Hints.
- Атрибут
__annotations__— документация Python, Annotations Best Practices. - Выводы в терминале получены запуском архивов проекта темы на Python
3.12; кадры сняты в VS Code1.138с расширением Pylance2026.4.
Тип в подписи функции:
что Go проверяет до запуска
В этой главе разобрано, где в Go записывается тип — у параметра, у результата и у переменной — и как читать подпись функции целиком. На функциях проекта темы показано, что делает компилятор, когда тип аргумента не совпадает с подписью, и как редактор использует типы в подсказке и в автодополнении.
- Категория
- Понятия
- Уровень
- Начальный
- Опирается на
- Модули и пакеты Go, тема 02
- Зачем это знать
- Понимать, какие значения принимает функция, не открывая её тело
§1Тип значения
Тип — вид значения, от которого зависит, что с ним можно делать. У числа 5 тип int: его можно складывать и делить. У строки "5" тип string: её можно склеить с другой строкой, а сложить с числом нельзя. Узнать тип значения можно функцией fmt.Printf с указателем %T: для 125000 она напечатает int, для "Смена" — string, для 0.15 — float64.
Go определяет тип каждой переменной, параметра и результата до запуска программы, при сборке. Такой подход называют статической типизацией. У любой переменной тип один и не меняется: если в переменную price положено целое число, строку в неё положить уже нельзя. Если тип значения где-то не совпадает с ожидаемым, компилятор сообщает об ошибке и исполняемый файл не создаёт.
В этом отличие от Python, где тип у параметра можно не писать. В Go подпись функции без типов не существует: func CalculateAverage(grades) — синтаксическая ошибка. Тип параметра и тип результата пишутся всегда, и поэтому первая строка любой функции уже отвечает на вопрос, что ей передать и что она вернёт.
В Python запись типа называется аннотацией и необязательна: интерпретатор её не проверяет, сверку выполняют отдельные программы. В Go тип в подписи — часть языка: без него код не компилируется, а сверку выполняет сам компилятор. Слово «аннотация» к Go не применяют, говорят просто «тип параметра», «тип результата».
§2Проект темы
Трудность «что передать и что вернётся» видна только в проекте, где функций много и они лежат в разных пакетах. Поэтому все примеры темы взяты из одного такого проекта — «Отчёт кафе». Он читает файл смены с заказами, считает выручку по каждому официанту и печатает отчёт в консоль. Код разложен по пакетам внутри папки internal, как в теме 02: domain — заказ и смена, services — чтение файла и расчёты, utils — имена, суммы и дата, ui — печать. Суммы хранятся в копейках, целыми числами: 125000 означает 1 250,00 ₽.
Проект выдаётся в трёх архивах с одним и тем же кодом на разных этапах темы. Первый архив не собирается: в нём двенадцать ошибок типов, их исправляют в практике главы 4.5. Второй — рабочий код без комментариев документации; на нём построены главы 4.1–4.4 и вся вторая часть темы. Третий — тот же код с документацией, эталон для сверки в конце.
Для этой главы нужен второй архив. Подготовьте его к работе:
- Распакуйте архив. В Проводнике щёлкните по архиву правой кнопкой и выберите Извлечь всё.В Finder дважды щёлкните по архиву: рядом появится папка.В файловом менеджере щёлкните по архиву правой кнопкой и выберите Извлечь или выполните в терминале
unzip имя-архива.zip. - Откройте получившуюся папку в VS Code: File → Open Folder. Открывать нужно папку, в которой лежат
go.modиmain.go: по файлуgo.modGo и редактор находят корень модуля. - Откройте терминал редактора (Ctrl + `) и проверьте версию Go:
go version. Нужна версия1.23или новее. Установка Go разобрана в главе 2.1, ветка Go. - Соберите все пакеты:
go build ./.... Запись./...означает «текущая папка и все вложенные». Если команда ничего не напечатала, сборка прошла без ошибок. - Запустите проект:
go run .. В терминале появится отчёт по смене.
$ go run . =========== Смена 21.09.2026 =========== Петрова Анна 2 480,50 ₽ Иванов Пётр 1 685,00 ₽ Сидоров Илья 772,00 ₽ =========== Итого 4 937,50 ₽ ===========
Дата в шапке — текущая: проект берёт её из часов компьютера. Команды Go одинаковы в Windows, macOS и Linux. Окружение, как у Python, создавать не нужно: зависимости модуля записаны в go.mod, а у этого проекта внешних зависимостей нет.
§3Где записывается тип
Откройте файл internal/services/calculator.go и найдите первую строку функции CalculateAverage. Рядом для сравнения — та же функция из ветки Python:
def calculate_average(grades: list[int]) -> float:
Тип пишется после имени через двоеточие, результат — после стрелки. Аннотации можно не писать.
func CalculateAverage(grades []int) (float64, error)
Тип пишется после имени через пробел, без двоеточия. Результатов два: дробное число и ошибка. Без типов строка не скомпилируется.
В Go тип записывается в трёх местах: у параметра, у результата и у переменной. Все три показаны на схеме. Для неё взята функция FormatTitle из internal/utils/formatter.go: у неё два параметра разных типов и один результат. Пример переменной — строка из функции RevenueByWaiter.
var, — после имени.| Где | Запись | Читается как |
|---|---|---|
| Параметр | text string | параметр text принимает строку |
| Несколько параметров одного типа | a, b int | оба параметра — целые числа; тип пишут один раз в конце |
| Один результат | ) string | функция возвращает строку |
| Несколько результатов | ) (float64, error) | функция возвращает дробное число и ошибку; список в скобках |
| Результаты с именами | ) (low, high int, err error) | три результата, у каждого есть имя |
| Нет результата | func PrintReport(lines []string) | после скобок ничего: функция ничего не возвращает |
| Переменная с типом | var names []string | срез строк, сейчас пустой |
| Переменная без записи типа | total := 0 | тип int выведен из значения 0 |
| Поле структуры | Kopeks int | поле Kopeks хранит целое число |
В таблице кроме трёх мест есть частные случаи. Если у функции несколько результатов, их типы перечисляют в скобках через запятую. Самое частое сочетание в Go — значение и ошибка: (float64, error). Тип error означает «ошибка или её отсутствие»; когда всё прошло хорошо, на месте ошибки возвращается nil. Функция без результата ничего не пишет после скобок: аналога -> None в Go нет.
У переменной тип можно не писать, если он понятен из значения. Запись total := 0 — короткое объявление: компилятор сам выводит тип int из числа 0. Тип у такой переменной всё равно есть и не меняется, его просто не набирают руками. Запись var names []string нужна, когда значения пока нет: переменная получает нулевое значение своего типа — 0 у чисел, "" у строки, пустой срез у среза.
Оформление кода в Go задаёт программа gofmt, и VS Code с расширением Go запускает её при каждом сохранении файла. Пробелы, отступы и выравнивание полей структуры приводятся к одному виду сами.
В примерах уже встречались составные типы: []int — «срез целых чисел», то есть список; map[string]int — «словарь со строковыми ключами и целыми значениями». Для чтения подписей в этой главе этого достаточно; все такие записи разобраны в главе 4.2.
§4Как читать подпись функции
Подпись функции — первая строка её объявления: имя, параметры с типами и типы результатов. Подпись отвечает на три вопроса: какие значения передать, в каком порядке и что вернётся. Таблица ниже собрана из второго архива: из каждого пакета выписаны функции, которые вызываются из других пакетов, а справа записано, как эти строки читаются. Подпись читают слева направо: имя, затем каждый параметр с типом, в конце типы результатов.
| Подпись | Что передают | Что получают |
|---|---|---|
func TodayLabel() string | ничего | строку с датой, например "21.09.2026" |
func NormalizeName(name string) string | строку с именем | строку: имя в виде «Фамилия Имя» |
func FormatKopeks(kopeks int) string | целое число копеек | строку "1 250,00 ₽" |
func FormatTitle(text string, width int) string | строку и ширину — оба аргумента обязательны | строку заданной ширины |
func CalculateAverage(grades []int) (float64, error) | срез целых чисел | дробное число и ошибку |
func GradeBounds(grades []int) (low, high int, err error) | срез целых чисел | минимум, максимум и ошибку |
func NewOrder(waiter string, kopeks int) (Order, error) | имя и сумму | заказ и ошибку |
func LoadOrders(path string) ([]domain.Order, error) | путь к файлу строкой | срез заказов и ошибку |
func RevenueByWaiter(orders []domain.Order, top int) ([]WaiterRevenue, error) | срез заказов и число | срез пар «официант — сумма» и ошибку |
func SortOrdersInPlace(orders []domain.Order) | срез заказов | ничего: функция меняет порядок в переданном срезе |
func SaveReport(path string, lines []string) (int, error) | путь и срез строк | целое число и ошибку |
func PrintReport(lines []string) | срез строк | ничего: функция только печатает |
Все сведения в правых колонках взяты только из подписей. Значит, по таблице можно составить вызов, не открывая ни одного пакета. LoadOrders возвращает []domain.Order и ошибку, а RevenueByWaiter принимает []domain.Order. Поэтому первый результат одной функции передаётся в другую без преобразований — так они и вызываются в main.go:
func buildReport(path string) ([]string, error) { orders, err := services.LoadOrders(path) // []domain.Order, error if err != nil { return nil, err } revenue, err := services.RevenueByWaiter(orders, 0) // []services.WaiterRevenue, error ...
Из этого фрагмента видны три правила вызова, которые следуют из подписей. Первое: если функция возвращает два значения, их принимают в две переменные — orders, err := …. Второе: ошибку проверяют сразу после вызова, сравнивая с nil. Третье: у параметров Go нет значений по умолчанию, поэтому RevenueByWaiter вызывается с явным 0 на месте top. В Python этот аргумент можно было пропустить.
Имя пакета перед функцией тоже часть чтения. services.LoadOrders — функция LoadOrders из пакета services; в подписи тип domain.Order — тип Order из пакета domain. Внутри своего пакета имя пишут без приставки: в internal/domain/order.go это просто Order. Имена с заглавной буквы видны из других пакетов, со строчной — только внутри своего; это правило разобрано в главе 2.4, ветка Go.
В таблицу не попали методы типа Shift из internal/domain/shift.go: у подписи метода есть особенность. Перед именем метода в скобках стоит получатель — объект, у которого метод вызывают, с его типом. В Python это self без аннотации; в Go у получателя тип пишется всегда:
type Shift struct { Day time.Time Orders []Order } func NewShift(day time.Time) *Shift { return &Shift{Day: day} } func (s *Shift) Add(order Order) { s.Orders = append(s.Orders, order) } func (s *Shift) TotalKopeks() int { total := 0 for _, order := range s.Orders { total += order.Kopeks } return total }
Здесь видны все три места записи типа. Поля структуры: Day time.Time — дата из пакета time, Orders []Order — срез заказов. Получатель (s *Shift) читается так: «метод вызывают у указателя на смену, внутри метода смена называется s». Звёздочка нужна, чтобы Add менял ту самую смену, у которой его вызвали, а не её копию; указатели разобраны в главе 4.2. Результат int у TotalKopeks — сумма копеек. У Add после скобок ничего нет: метод меняет объект и ничего не возвращает. Функция NewShift — конструктор: обычная функция, которая создаёт и возвращает новый объект. Отдельного __init__ в Go нет.
§5Что Go делает с типом
Подписи из §4 сообщают, что функция ожидает. Следующий вопрос — что будет, если передать не то. Ответ дают опыты. Создайте в корне проекта папку proba, а в ней файл main.go: это отдельная маленькая программа внутри того же модуля, она может импортировать пакеты проекта. Запускается она командой go run ./proba. Во всех вкладках ниже меняется только содержимое proba/main.go; выводы получены запуском.
Первые три вкладки вызывают CalculateAverage(grades []int) (float64, error) — сначала с подходящим аргументом, затем с двумя неподходящими. Для четвёртой нужна функция, которой в проекте нет, — сложение двух сумм; она записана прямо в proba/main.go.
Тип при сборкепереключите вызов
package main import ( "fmt" "example.com/otchet-kafe/internal/services" ) func main() { average, err := services.CalculateAverage([]int{5, 4, 5, 3, 5}) fmt.Println(average, err) }
4.4 <nil>
Собралось и работаетПередан срез целых чисел, как записано в подписи. Программа собрана и запущена: среднее 4.4, ошибки нет — на её месте <nil>.
package main import ( "fmt" "example.com/otchet-kafe/internal/services" ) func main() { average, err := services.CalculateAverage([]int{5.5, 4.0}) fmt.Println(average, err) }
# example.com/otchet-kafe/proba proba/main.go:10:50: cannot use 5.5 (untyped float constant) as int value in array or slice literal (truncated)
Не собралосьКомпилятор отказался собирать программу: 5.5 нельзя положить в срез целых чисел без потери дробной части — «(truncated)», «было бы обрезано». Число 4.0 ошибки не вызвало: оно равно целому 4, и компилятор переводит его без потерь. В Python тот же вызов выполнился и вернул 4.75.
package main import ( "fmt" "example.com/otchet-kafe/internal/services" ) func main() { average, err := services.CalculateAverage([]string{"5", "4"}) fmt.Println(average, err) }
# example.com/otchet-kafe/proba
proba/main.go:10:44: cannot use []string{…} (value of type []string) as []int value in argument to services.CalculateAverage
Не собралосьСообщение указывает на строку вызова — proba/main.go, строка 10 — и называет оба типа: передан []string, ожидался []int. В Python ошибка возникла внутри функции, в строке с sum, и место неверного вызова из неё видно не было.
package main import "fmt" func addKopeks(a int, b int) int { return a + b } func main() { fmt.Println(addKopeks(150, 200)) fmt.Println(addKopeks("150", "200")) }
# example.com/otchet-kafe/proba proba/main.go:11:24: cannot use "150" (untyped string constant) as int value in argument to addKopeks proba/main.go:11:31: cannot use "200" (untyped string constant) as int value in argument to addKopeks
Не собралось, ни одна строка не выполненаОшибочна только строка 11, но не выполнилась и строка 10 с верным вызовом: программа с ошибкой типа не собирается целиком. В Python этот вызов тихо вернул строку "150200" вместо суммы 350. В Go такая подмена невозможна: строку из CSV-файла нужно явно перевести в число вызовом strconv.Atoi, как в LoadOrders.
Из четырёх опытов видно, как Go обращается с типом в подписи. Компилятор сверяет каждый вызов с подписью до запуска и не собирает программу, пока есть хоть одно несовпадение. Ошибка указывает на строку вызова и называет оба типа — переданный и ожидаемый.
Компилятор проверяет и количество результатов. Если забыть, что CalculateAverage возвращает два значения, и принять одно, программа тоже не соберётся:
package main import ( "fmt" "example.com/otchet-kafe/internal/services" ) func main() { average := services.CalculateAverage([]int{5, 4, 5, 3, 5}) fmt.Println(average) }
# example.com/otchet-kafe/proba proba/main.go:10:13: assignment mismatch: 1 variable but services.CalculateAverage returns 2 values
Сообщение читается так: «несовпадение при присваивании: одна переменная, а функция возвращает два значения». Ошибку из результата нельзя случайно потерять: её нужно принять в переменную. Если ошибка точно не нужна, её место явно занимают знаком подчёркивания: average, _ := ….
Ассоциация: турникет в метро
Турникет сверяет билет на входе. С неподходящим билетом человек на станцию не проходит и в поезд не попадает.
Компилятор сверяет каждый вызов с подписью при сборке. Программа с неверным типом аргумента не собирается и до запуска не доходит.
Где не совпадаетТурникет пропускает людей по одному, а компилятор проверяет всю программу целиком: одна неверная строка останавливает сборку всего пакета, даже верные строки не выполняются.
§6Подсказка редактора
Компилятор проверяет типы при сборке. Редактор использует те же типы раньше — пока код читают и пишут. В VS Code этим занимается расширение Go: оно запускает программу gopls (произносится «гоу-плиз»; ls в имени — от language server) — языковой сервер Go. Языковой сервер — программа, которая постоянно разбирает код проекта и отвечает редактору: какой тип у имени под курсором, где объявлена функция, какие ошибки в файле. Установка расширения и gopls показана по шагам в главе 4.3; если расширение уже стоит, опыт можно повторить сразу.
Подсказку вызывают наведением указателя на имя или клавишами: курсор на имя, затем CtrlCmd + K и CtrlCmd + I. Откройте main.go, найдите строку 17 — revenue, err := services.RevenueByWaiter(orders, 0) — и наведите указатель на RevenueByWaiter.
services: параметры orders []domain.Order и top int, результаты []services.WaiterRevenue и error. Из места вызова видно, что передать и что вернётся. Нижняя строка — ссылка на общий сайт документации pkg.go.dev; проект курса там не опубликован, поэтому по ней ничего не откроется. Во втором архиве комментариев документации нет, поэтому между подписью и ссылкой текста нет.Наведите указатель на переменную revenue слева от :=. Тип у неё не записан, но он есть: компилятор вывел его из результата функции, и gopls показывает его так же, как записанный.
var revenue []services.WaiterRevenue: переменная — срез значений типа WaiterRevenue из пакета services. Так проверяют тип любой переменной, объявленной через :=.§7Автодополнение
Подсказка показывает типы в готовом коде. Те же типы редактор использует, пока код набирается, — в автодополнении.
Автодополнение — список, который редактор показывает при наборе: имена переменных, функций, полей и методов, подходящих в этом месте. После точки в нём перечислены поля и методы значения слева от точки. Составить такой список редактор может, только если знает тип значения, а в Go тип известен всегда. Откройте internal/services/orders.go, поставьте курсор в конец строки func SortOrdersInPlace(orders []domain.Order) {, нажмите Enter и наберите orders[0]. — первый элемент среза и точку. Если список не появился сам, вызовите его клавишами Ctrl + Пробел. На macOS это сочетание часто занято переключением раскладки; тогда список вызывают командой Trigger Suggest из палитры команд (CtrlCmd + Shift + P). После опыта отмените набранное: CtrlCmd + Z.
domain.Order, и в списке его поля Kopeks и Waiter с типами int и string. Слева — панель подробностей выбранного пункта: тип поля int. Служебных имён, как у объектов Python, в списке нет: у структуры Go есть только объявленные поля и методы.Что дают типы в подписи при работе с кодом: итог §4–§7
- Вызов без чтения тела функции. Подсказка показывает, какие значения передавать и что вернётся.
- Имена полей из списка. Автодополнение предлагает поля структуры, и
order.Kopeksвыбирается из списка; набирать имя по памяти не нужно. - Проверка до запуска. Компилятор сверяет каждый вызов с подписью и не собирает программу с неверным типом; gopls показывает ту же ошибку в редакторе ещё до сборки.
Как увидеть ошибки в редакторе и в терминале, по шагам показано в главе 4.3. Перед этим, в главе 4.2, разобрано, как записать любой тип Go — от int до «значение или ничего». В главе 4.4 собраны ошибки, которые компилятор находит в каждом из этих типов.
§8Частые вопросы
:=, тип не записан, у неё нет типа, как в Python.total := 0 — переменная типа int, положить в неё строку нельзя.4.0 в int можно, 5.5 — нельзя. Строку в число переводят явно: strconv.Atoi(row["kopeks"]), целое в дробное — float64(sum).§9Проверьте себя
Где пишется тип у параметра, у результата и у переменной?
У параметра — после имени через пробел: text string. У результата — после закрывающей скобки параметров: ) string, а если результатов несколько, — списком в скобках: ) (float64, error). У переменной — после имени в объявлении var: var names []string; в записи := тип выводится из значения.
Прочитайте подпись: func IsBig(kopeks, limit int) bool.
Функция принимает два целых числа — сумму и порог; тип int записан один раз и относится к обоим параметрам. Возвращает true или false. Значения по умолчанию у порога нет: передавать нужно оба аргумента.
Прочитайте подпись: func SaveReport(path string, lines []string) (int, error).
Функция принимает путь строкой и срез строк. Возвращает целое число и ошибку. Что это за число — количество записанных строк, — из подписи не видно: такие сведения пишут в комментарии документации.
Что будет при сборке вызова CalculateAverage([]int{5.5, 4.0})?
Программа не соберётся: 5.5 нельзя положить в срез []int без потери дробной части. Компилятор сообщит cannot use 5.5 (untyped float constant) as int value.
Что означает (s *Shift) перед именем метода?
Это получатель: метод вызывают у указателя на Shift, внутри метода объект называется s. Через указатель метод меняет ту самую смену, у которой его вызвали.
Почему RevenueByWaiter(orders) без второго аргумента не собирается, хотя в Python так было можно?
У параметров Go нет значений по умолчанию. Подпись требует два аргумента — orders и top, — и вызов с одним компилятор отклоняет. Значение «все официанты» передают явно: RevenueByWaiter(orders, 0).
§10Шпаргалка по главе
text string
a, b int — общий тип
) string
) (float64, error)
var names []string
total := 0 — тип int выведен
func (s *Shift) Add(order Order)
получатель — с типом
несовпадение — не собирается
%T — напечатать тип
подсказка: Ctrl+K, Ctrl+I
автодополнение: Ctrl+Пробел
Итог главы
В Go тип записывается у каждого параметра и результата, а у переменной выводится из значения, если не записан явно. По подписям можно составить вызов, не открывая пакет. Компилятор сверяет вызовы с подписями до запуска и не собирает программу, пока есть несовпадение; сообщение называет строку вызова и оба типа. Редактор через gopls показывает те же типы в подсказке и в автодополнении.
- Объявления функций, параметров и результатов — The Go Programming Language Specification, Function declarations; короткое объявление переменной и нулевые значения — разделы Short variable declarations и The zero value.
- Методы и получатели — спецификация, Method declarations; несколько результатов и ошибки — Effective Go, Multiple return values.
- Языковой сервер — документация gopls.
- Выводы в терминале получены запуском второго архива на Go
1.27; кадры сняты в VS Code1.138с расширением Go0.56и gopls0.24.