Содержание урока · 15 разделов
- Первое приложение go: что мы собираем
- Структурирование проекта: три слоя одной программы
- Разбор скелета: карта, счётчик и два приёмника
- Карта id→Note: добавление, поиск, удаление
- Флаг done: вторая карта вместо правки структуры
- Меню и ввод: for, switch и fmt.Scan
- Таблица в консоли: ширины %-3d и %-16s
- Сортировка: sort.Slice и запасной путь
- Фильтр по подстроке: strings.Contains
- Витрина курса: что из 01–24 куда вошло
- Шпаргалка: инструмент → в приложении → урок
- Курс завершён: что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Первое приложение go: что мы собираем#
Двадцать четыре урока мы тренировались на упражнениях: счётчиках, игроках, прямоугольниках. Упражнение живёт один запуск и умирает вместе с Println. Сегодня — финальный проект курса: первое приложение go, которое похоже на настоящую программу. Это консольный менеджер заметок — CLI-инструмент, в котором вы храните короткие записи: «купить хлеб», «позвонить маме», «дописать отчёт». Программа печатает меню, читает команду, выполняет действие и снова печатает меню — пока вы не выберете выход. Такой цикл «спроси — пойми — сделай» лежит в основе тысячи утилит командной строки, от калькуляторов до git.
Список возможностей менеджера заметок небольшой, но полный: добавить заметку (каждой автоматически присваивается номер), напечатать все заметки ровной таблицей, найти заметку по номеру, пометить её готовой, удалить, отсортировать по заголовку и найти все записи по подстроке. Каждая возможность — это один метод или одна маленькая функция, а всё вместе — примерно сотня строк с комментариями. Обратите внимание на слово «собираем»: ни один инструмент в этом уроке не новый. Структуры, поля, композитные литералы — из урока о структурах; приёмники-значение и указатель — из урока о методах; карта, delete и проверка наличия — из урока о картах; срезы и append — из уроков семнадцатого и восемнадцатого; switch, циклы, строки — из середины курса. Финальный проект тем и ценен, что показывает: вы уже знаете весь язык, которого достаточно для настоящего инструмента.
Структурирование проекта: три слоя одной программы#
Первое, что отличает программу от упражнения, — структурирование проекта. До сих пор весь код жил в main: объявили данные, сделали дело, напечатали. Приложение так писать нельзя: main разбухает, каждая правка рискует сломать соседнюю строку. Правило, которое вы примените в этом проекте на go для начинающих и в каждом следующем: разделите программу на слои — данные, операции над данными, интерфейс с пользователем.
Слой данных — это тип Note: структура с полями ID, Title, Body и временем создания. Тип описывает, что такое заметка в вашей программе, — и только это. Слой операций — тип Store с картой заметок внутри и методами Add, List, Find, Delete: он знает, где лежат данные и как их менять, но не знает, кто и зачем их просит. Слой интерфейса — main с меню: он читает команды пользователя, вызывает методы хранилища и печатает результаты, но сам в данные не лезет. Границы между слоями — это сигнатуры методов: main говорит Add("купить хлеб", "батон"), и ему всё равно, что внутри — карта, срез или файл на диске.
Зачем границы, если программа маленькая? Затем, что настоящий проект растёт: завтра заметкам понадобится приоритет, послезавтра — сохранение в файл. При разделении слоёв каждая такая правка затрагивает один слой и не трогает остальные; при коде-в-одном-main — все сразу. Вы уже касались этой идеи в уроке о методах: сеттер прячет поле за проверкой. Здесь та же мысль в масштабе программы. И ещё один бонус: слои можно проверять по отдельности — методы Store тестируются простым вызовом, без клавиатуры, чем мы и пользуемся в песочнице этого урока.
Песочница: скелет приложения — Note и Store
Запустите: это работающее ядро менеджера заметок. NewStore() — функция-конструктор: возвращает указатель на Store с пустой картой и счётчиком next = 1. Три вызова s.Add(...) добавляют заметки: внутри Add номер берётся из счётчика, счётчик растёт, готовая Note кладётся в карту по своему номеру. List() собирает значения карты в срез и сортирует по номеру — карта не гарантирует порядка, поэтому сортировка в выводе обязательна. В main заметки печатаются циклом range из урока о range. Опыты. Первый: добавьте четвёртую заметку s.Add("полить цветы", "вечером") — она получит номер 4 автоматически. Второй: поменяйте в sort.Slice сравнение out[i].ID < out[j].ID на > — список напечатается от больших номеров к меньшим. Третий: добавьте поле Done bool в Note и в main напечатайте s.List()[0].Done — ноль false: нулевые значения из урока о переменных работают и здесь, заметка по умолчанию не готова.
Запустите код, чтобы увидеть вывод.
Разбор скелета: карта, счётчик и два приёмника#
Пройдём по скелету медленно — здесь каждое решение опирается на какой-то из предыдущих уроков. Хранилище — карта map[int]Note: ключ — номер заметки, значение — структура целиком. Почему не срез? Срез из уроков о срезах тоже мог бы хранить заметки, но тогда номер заметки и позиция в срезе слиплись бы: после удаления середины все номера после неё съехали бы, и записка «найди заметку 7» перестала бы означать одно и то же. Карта развязывает номер и порядок: delete по ключу не трогает остальные записи, проверка наличия через запятую-ok отвечает «а есть ли заметка 7?», а len считает записи. Это тот самый выбор структуры данных, о котором шла речь в уроке о картах: ключ здесь — не «какой по счёту», а «какой именно».
Счётчик next решает проблему номеров: каждая заметка получает уникальный растущий номер — 1, 2, 3. Хранился бы номер в len(notes), номера дублировались бы после удалений: две заметки с номером 3. Автонумерация — паттерн из настоящих систем, от баз данных до issue-трекеров, и целиком умещается в две строки id := s.next; s.next++.
Приёмники методов выбраны по правилам урока о методах. Add и Delete меняют состояние — поля notes и next — поэтому у них приёмник-указатель func (s *Store). List и Find только читают — им хватает приёмника-значения func (s Store). Заметьте: List копирует только заголовок Store — карту, которая под капотом ссылка на данные, копировать целиком не нужно; это вы видели в уроке о структурах, где map в структуре ведёт себя как ссылка.
И честная нота о поле CreatedAt. В настоящем приложении время создания заметки хранит тип time.Time из стандартного пакета time — так делает весь мировой Go. Песочница этого не тянет: в резервном движке time.Now() подменяется сырым числом миллисекунд, которое меняется от запуска к запуску, и вывод перестаёт быть воспроизводимым. Поэтому в ранимых примерах урока поле CreatedAt имеет тип int64 и заполнено фиксированным числом — Unix-меткой времени, как её хранят базы данных. Версия с настоящим time.Time — в следующем блоке: она для локального запуска.
Локальная версия: поле CreatedAt типа time.Time
GOpackage main
import (
"fmt"
"time"
)
// Note — заметка со временем создания
type Note struct {
ID int
Title string
CreatedAt time.Time
}
func main() {
n := Note{ID: 1, Title: "купить хлеб", CreatedAt: time.Now()}
fmt.Println(n.Title, n.CreatedAt.Format("02.01 15:04"))
}Кнопки нет: движки песочницы расходятся на этом примере. Настоящий Go и основной движок печатают «купить хлеб» и дату-время создания в формате «день.месяц часы:минуты» — time.Time живёт в пакете time из стандартной библиотеки, time.Now() возвращает текущий момент, а метод Format собирает из него строку по шаблону. Резервный движок подменяет time.Now() сырым числом миллисекунд, поле time.Time в литерале фактически превращается в это число, а метод Format у него не находит — печатает ошибку «у значения типа int нет поля или метода Format» или сырое число. Запустите файл локально командой go run main.go — там время настоящее. Справочно: метка 1714500000 из ранимых примеров — это 30 апреля 2024 года: Unix-время считается в секундах с 1 января 1970.
Карта id→Note: добавление, поиск, удаление#
Слой операций менеджера заметок — это четыре метода над картой, и каждый вы уже писали руками в уроке о картах, только без структур и приёмников. Add кладёт значение по новому ключу: s.notes[id] = Note{...} — присваивание по несуществующему ключу добавляет запись, это вы знаете. Delete удаляет по ключу: delete(s.notes, id) — и карта уменьшается на одну запись, независимо от того, где в «порядке» она жила. Find читает с проверкой: n, ok := s.notes[id] — двухзначное чтение из урока о картах отвечает и «что лежит», и «лежит ли вообще», а заодно обходит ловушку нулевого значения: чтение по отсутствующему ключу возвращает нулевую Note, которую без проверки ok не отличить от настоящей заметки с пустыми полями.
Один нюанс потребовал целого правила из урока о структурах: элементы карты неадресуемы. В настоящем Go написать s.notes[id].Done = true нельзя — компилятор откажет сообщением cannot assign по той же причине, по которой в уроке о структурах не работало m["a"].X = 99. Резервный движок песочницы, кстати, эту строку молча выполняет, и правка видна — удобная ловушка, а не фича: код в расчёте на неё не соберётся на настоящем Go. Правильный ход — трёхшаговый: прочитать заметку во временную переменную, изменить поле у переменной, положить обратно по ключу — он работает одинаково на обоих движках и в настоящем Go. Мы обернули его в метод MarkDone — и он появится в следующем ранимом блоке.
Отдельного разговора заслуживает возвращаемое значение Delete: bool. Операция «удалить заметку 7» имеет два исхода — удалили или такой нет, — и вызывающий код должен уметь их различить. Двухзначный ответ (bool) — та же идиома, что и запятая-ok при чтении: операция возвращает не только результат, но и признак успеха. Помните урок об ошибках и возвратах: пока у нас нет настоящих error-значений, bool — честный минимум для различения исходов, и в менеджере заметок его достаточно.
Проверьте себя: правка заметки в карте
В Store лежит карта notes map[int]Note. Нужно пометить заметку 3 готовой: поле Done становится true. Какая строка соберётся и сработает?
Песочница: Delete, Find и флаг done
Запустите: MarkDone возвращает true для существующей заметки 1 и false для несуществующей 7 — сначала проверка наличия, потом запись флага. Find(2) находит «позвонить маме», Find(9) с пустой веткой if отвечает «нет заметки 9». Флаги живут в отдельной карте done map[int]bool: len(s.done) — сколько заметок готово. После Delete(1) готовых ноль: Delete вычищает флаг вместе с заметкой — если бы он этого не делал, карта done засорялась бы номерами уже удалённых записей. Опыты. Первый: добавьте s.Delete(9) до печати — false, удаление несуществующего номера не ошибка. Второй: пометьте готовой заметку 2 и напечатайте len(s.done) после — 2: флаги независимы. Третий: добавьте метод IsDone(id int) bool, возвращающий s.done[id], — чтение по отсутствующему ключу даст false само: нулевое значение bool из урока о переменных работает на вас.
Запустите код, чтобы увидеть вывод.
Флаг done: вторая карта вместо правки структуры#
В предыдущем блоке спрятано дизайнерское решение, которое стоит развернуть: флаг готовности живёт НЕ в структуре Note, а в отдельной карте done map[int]bool рядом. Оба пути законны — пометить поле Note{... Done: true} через временную переменную или завести карту флагов — но карта даёт три выгоды.
Первая — простота правки: писать в карту по ключу можно всегда, а правка поля в элементе карты требует трёхшагового обхода неадресуемости. Вторая — целостность: карта done — это готовый индекс «все готовые заметки», len(s.done) считает готовые без цикла, а range по карте перебирает их номера без условий. Третья — разделение сущностей: заметка описывает содержание («что нужно сделать»), карта done описывает состояние («что уже сделано»); когда в следующем курсе заметки переедут в файл или базу, состояние и содержание будут меняться независимо.
Тип map[int]bool особенно приятен из-за нулевых значений: чтение по любому отсутствующему ключу возвращает false, то есть «не готова» — проверка s.done[n.ID] работает даже для заметки, о которой карта никогда не слышала. Это тот же трюк, что и в опыте к предыдущему блоку, и он безопасен ровно потому, что false — осмысленное значение по умолчанию. С map[int]string такой номер не прошёл бы: пустая строка «нет заголовка» и «заголовок не задан» слились бы в одну.
Пара слов о границе приёма: если бы у заметок были три состояния — открыта, в работе, готова — bool перестал бы хватать, и флаг переехал бы в поле status int внутри Note с константами из урока о переменных. Выбор «карта флагов или поле структуры» — это вопрос о числе состояний, и умение его задавать важнее любого конкретного ответа.
Меню и ввод: for, switch и fmt.Scan#
Остался слой интерфейса: меню. Скелет настоящего консольного меню — бесконечный цикл for без условия из уроков о циклах, внутри которого switch по команде пользователя: 1 — добавить, 2 — список, 3 — удалить, 0 — выход. Команду читает fmt.Scan из урока о вводе: он ждёт, пока пользователь наберёт число и нажмёт Enter, и кладёт его в переменную по адресу. Выход из бесконечного цикла — return внутри case 0: функция main завершается, программа заканчивается. Полная версия выглядит так, как в следующем блоке, — и вот здесь песочница бессильна: у неё нет stdin. Интерпретатор живёт на странице браузера, читать клавиатуру не может, а значит, меню с настоящим вводом в песочнице не запустить ни на одном из двух движков. Поэтому блок с меню неранимый — код для локального запуска, — а механику диспетчеризации switch мы проверим ранимым аналогом: список команд задан заранее, цикл их перебирает. Логика разбора команды идентична, просто источник команд другой.
Пара слов о взрослом вводе. fmt.Scan читает значения, разделяемые пробелами, и для одной команды этого достаточно; если когда-нибудь понадобится читать заголовки с пробелами целиком, на помощь придёт bufio.Scanner из стандартной библиотеки — он читает строку целиком. Пакета bufio в песочнице нет, поэтому упоминание — только для локальных экспериментов. Для менеджера заметок fmt.Scan(&cmd) покрывает всё: меню принимает числа.
Локальная версия: меню с настоящим вводом
GOpackage main
import "fmt"
func main() {
// хранилище и команда
var cmd int
for {
fmt.Print("команда (1-добавить, 2-список, 0-выход): ")
fmt.Scan(&cmd)
switch cmd {
case 1:
fmt.Println("добавляю заметку")
case 2:
fmt.Println("печатаю список")
case 0:
fmt.Println("выход")
return
default:
fmt.Println("не знаю такую команду")
}
}
}Кнопки нет: у песочницы нет stdin ни у основного, ни у резервного движка — интерпретатор живёт в браузере и клавиатуру не видит. Запустите файл локально: go run main.go. Цикл for без условия крутится вечно; каждый виток печатает приглашение, fmt.Scan ждёт число и кладёт его по адресу &cmd — амперсанд из урока об указателях, потому что Scan должен записать значение В вашу переменную. switch разбирает команду: case 0 печатает «выход» и делает return — выход из main, программа завершается; default ловит всё незнакомое. Заметьте: return внутри case завершает всю функцию, а не только switch — именно так меню заканчивает работу. Соедините этот main со Store из ранимых блоков — и получите полный менеджер заметок на своей машине.
Песочница: диспетчер команд без ввода
Запустите: commands — заранее заданный список команд, который подменяет клавиатуру. Цикл range из урока о range перебирает команды, handle разбирает каждую команду switch-ом: case 1, case 2, case 3 — известные команды, case 0 — выход, default — всё остальное: девятка попадает именно туда. Это тот же диспетчер, что в меню с fmt.Scan, только источник команд другой. Опыты. Первый: добавьте в commands число 4 и case 4 с печатью «ищу заметки» — новая команда меню появляется одной строкой в двух местах. Второй: перенесите switch прямо в main внутрь цикла — поведение то же, но функция handle исчезла: вынос обработчика в функцию делает main короче, это тот же выбор слоёв, что и Store. Третий: уберите return из case 0 и добавьте после цикла fmt.Println("до свидания") — раз цикл range конечен, программа дорабатывает до конца.
Запустите код, чтобы увидеть вывод.
Проверьте себя: выход из меню
Внутри бесконечного for пользователь вводит 0. В case 0 написано break. Что произойдёт?
Таблица в консоли: ширины %-3d и %-16s#
Печать «в столбик» — fmt.Println на каждую заметку — быстро превращается в кашу: номера разной длины, заголовки разной длины, колонки пляшут. Настоящая консольная таблица строит строки Printf-ом с ширинами из урока о fmt: глагол с числом ширины дополняет значение пробелами до заданной длины, а минус перед числом прижимает значение к левому краю. Запись %-3d означает «целое, ширина 3, слева», %-16s — «строка, ширина 16, слева»: короткие значения добиваются пробелами справа, длинные растут, сколько нужно. Соберите из трёх таких колонок строку с разделителями — и Println внутри цикла range печатает ровную таблицу, где все вертикальные палочки стоят друг под другом.
Ширины подбираются под данные: заголовки заметок длинные, им 16-20 позиций; номерам хватает 3; статус («открыта»/«готова») — 6-8. Хедер таблицы печатается той же строкой формата, только строками вместо чисел: %-3s для надписи ID. Правило одна-строка-формата-для-хедера-и-строк — маленькая, но прочная идиома: меняете структуру таблицы — меняете одну строку, а не десять Println.
Почему это важно в финальном проекте: таблица — первая вещь, которую видит пользователь CLI-инструмента, и «пляшущие» колонки мгновенно выдают учебную программу. При этом под капотом не появилось ничего нового: тот же Printf из урока о выводе, та же строка формата из урока о строках. Проверьте себя предиктом ниже — ширина прижимает и путает многих.
Что выведет код: ширина колонки
Прочитайте код и выберите, что он выведет:
package main
import "fmt"
func main() {
title := "хлеб"
fmt.Printf("%-6s|%d|\n", title, 7)
}Сортировка: sort.Slice и запасной путь#
Карта хранит заметки эффективно, но перебирает в случайном порядке — вы убедились в этом в уроке о картах, где порядок range каждый запуск разный. Для пользователя менеджера заметок это неприемлемо: список должен быть стабильным. Решение — sort.Slice из урока о функциях, где вы уже встречали функцию-значение: первым аргументом срез, вторым — функция-компаратор func(i, j int) bool, которая отвечает «элемент с индексом i должен стоять раньше j?». Для списка заметок по возрастанию номеров компаратор возвращает out[i].ID < out[j].ID, для сортировки по алфавиту — out[i].Title < out[j].Title: строки в Go сравниваются операцией < напрямую.
В List сортировка стоит после сборки среза: собрали значения карты через append, отсортировали, вернули. После этого любой вызов List печатает один и тот же порядок — детерминизм, которого карта не даёт.
Запасной путь, о котором стоит знать: если сортировать нужно карту по ключам, сначала соберите ключи в срез строк, отсортируйте его sort.Strings и затем переберите карту по отсортированным ключам — range по карте с ключом k в порядке готового среза. Паттерн «собрать ключи → sort.Strings → range по ключам» — универсальный мост между случайной картой и упорядоченным выводом, он встречается в половине реальных программ на Go. sort.Slice мощнее (сортирует что угодно по любому правилу), sort.Strings проще (одна строка, ноль замыканий) — выбирайте по задаче.
Фильтр по подстроке: strings.Contains#
Последняя возможность менеджера заметок — поиск: показать только те записи, в заголовке которых есть подстрока. Механика — strings.Contains из урока о строках: Contains(заголовок, шаблон) возвращает true, если шаблон встречается внутри заголовка. Вставьте её в if внутри range — и цикл печатает только совпавшие записи. Это и весь фильтр: три строки, которые в более взрослой программе станут параметром метода Find(query string) []Note.
Фильтрация по подстроке — самый честный интерфейс поиска для начинающего проекта: пользователь не обязан помнить номер заметки, чтобы её найти, — достаточно помнить слово. По мере роста проекта рядом появятся регистронезависимое сравнение (ToLower от обеих строк перед Contains), поиск по телу заметки, а потом и настоящие индексы — но принцип «перебери и оставь подходящие» останется тем же: в консольных утилитах это и есть поисковый движок в миниатюре.
Соединим арсенал: таблица с ширинами, сортировка через sort.Slice, фильтр через strings.Contains — всё это в следующем ранимом блоке работает на одном срезе заметок с фиксированными данными. Обратите внимание, как сортировка по заголовку и фильтр по подстроке живут в одном цикле, но решают разные задачи: сортировка меняет порядок вывода, фильтр — состав вывода. Разные оси одного вывода — разделение, которое стоит запомнить.
Песочница: таблица, сортировка и фильтр
Запустите: три заметки заданы литералом среза без имён полей — позиционная форма из урока о структурах. sort.Slice упорядочивает по заголовку: «архив фото» раньше «билеты», «билеты» раньше «звонок» — сравнение строк по алфавиту. Таблица печатается одной строкой формата: заголовок — тем же шаблоном со строками, строки — с числом и строками; статусы выбирает if. Фильтр ниже печатает только записи с «бил» внутри заголовка. Опыты. Первый: поменяйте компаратор на notes[i].ID < notes[j].ID — порядок станет по номерам: 1, 2, 3. Второй: добавьте в таблицу колонку готовости числом %-5v и передайте n.Done — true/false впишется в ширину. Третий: замените шаблон фильтра на «о» — совпадут «звонок в сервис» и «билеты на поезд»: подстрока ищется в любом месте заголовка.
Запустите код, чтобы увидеть вывод.
Витрина курса: что из 01–24 куда вошло#
Менеджер заметок собран — самое время оглянуться и увидеть, что в нём работает из каждого раздела курса. Это не повторение ради повторения: узнавание «вот здесь мой старый инструмент» — главный признак того, что знание прижилось.
Каркас программы — package main, import, func main, комментарии — самый первый урок; переменные, типы и нулевые значения держат все поля структур; строки и руны, числа и логика — материал пятого и шестого уроков — живут в заголовках, номерах и флагах. Управление потоком: if с запятой-ok проверяет наличие заметки, switch диспетчирует команды меню, циклы for и range перебирают список и ключи карты. Коллекции: срезы с append собирают вывод List, а карта id→Note — сердце хранилища; композитные литералы из урока о структурах заполняют и заметки, и хранилище. Функции из двадцатого урока — NewStore, handle, групповые параметры; возврат пары (Note, bool) в Find — прямое продолжение темы ошибок и возвратов. Методы приклеивают операции к Store, приёмники-указатели правят хранилище, приёмники-значения читают. Карта отвечает за уникальные номера, delete и проверку наличия.
Заметьте, чего в приложении НЕТ: горутин, каналов, интерфейсов, указателей на указатели — всего продвинутого. Это честная граница базового курса: на материале 01–24 уже собираются работающие инструменты, а оглавление курса — место, куда стоит вернуться, если какой-то винтик в машине ощущается шатким.
Шпаргалка: инструмент → в приложении → урок#
| Инструмент | Где работает в менеджере заметок | Урок курса |
|---|---|---|
| type struct + литералы | типы Note и Store, конструктор NewStore | 23 — структуры |
| методы, приёмники value/pointer | Add, Delete, MarkDone меняют хранилище; List, Find читают | 24 — методы |
| map + delete + запятая-ok | хранилище map[int]Note и флаги map[int]bool | 19 — карты |
| срез + append + range | сборка вывода List, перебор таблицы | 15–18 — срезы |
| sort.Slice / sort.Strings | стабильный порядок списка по номеру и по алфавиту | 20 — функции |
| strings.Contains | фильтр заметок по подстроке | 06 — строки |
| fmt.Printf с ширинами | таблица %-3d и %-16s | 09 — вывод |
| for + switch + return | меню: диспетчер команд и выход | 12–14 — циклы |
| fmt.Scan | чтение команды меню (локальный запуск) | 10 — ввод |
Вставьте пропуски: хранилище заметок
Соберите ядро менеджера: тип Store с картой, метод Add с автонумерацией и метод List со сборкой среза. Собранная программа печатает две заметки по номерам.
Соберите программу: заметка в карте
Соберите мини-версию приложения: тип Note, тип Store с картой, метод Add с автонумерацией и проверка в main. Собранный вывод — «первая».
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Каждый ранимый пример урока прогнан пробником на обоих движках песочницы, вывод совпадает: карта map[int]Note с литералами структур, присваивание по новому ключу, delete по ключу, чтение с запятой-ok; методы с приёмниками-указателями, меняющие поля хранилища, и приёмники-значения для чтения; sort.Slice по срезу структур с замыканием-компаратором (по int и по строкам); strings.Contains; Printf с ширинами %-3d и %-16s, включая кириллицу; map[int]bool с len и чтением по отсутствующему ключу; for+range+switch с default; поле CreatedAt int64. Недоступно или расходится: (1) stdin — у песочницы его нет на обоих движках, поэтому меню с fmt.Scan и версия с bufio — неранимые блоки для локального запуска, диспетчер проверен на заранее заданном списке команд; (2) время: в резервном движке time.Now() — сырое число миллисекунд, у него нет ни метода Format, ни Unix, поэтому поле CreatedAt в ранимых примерах — int64 с фиксированной меткой, а версия с time.Time — неранимый блок; в настоящем Go и основном движке время работает полноценно; (3) правка поля элемента карты s.notes[3].Done = true — настоящий Go отклоняет сообщением про cannot assign, а резерв молча выполняет, и правка видна: в ранимых примерах правка всегда через временную переменную, что работает одинаково; (4) range по литералу на месте for _, cmd := range []int{1, 9, 0} резерв не парсит — в ранимых список команд сначала кладётся в переменную (в настоящем Go работают оба варианта).
Карточки: первое приложение
Курс завершён: что дальше#
Это был последний урок курса go-basics — двадцать пятый из двадцати пяти. У вас за плечами полный базовый арсенал: синтаксис, типы, управление потоком, коллекции, функции с ошибками, указатели, структуры с методами — и теперь ещё первое собранное приложение, которое можно расширять как угодно: добавить редактирование заголовка, поиск по телу заметки, экспорт списка печатью в файл. Каждый из этих шагов потребует только уже знакомых инструментов.
Домашнее задание — довести приложение до личной версии: добавьте команду «показать готовые», метод Edit для правки заголовка через временную переменную и счётчик команд в меню. Готовую версию запускайте локально: go mod init notes, сохраните код в main.go, go run . — и меню с fmt.Scan оживёт на вашей клавиатуре.
Следующий курс — go-advanced — начинается там, где база закончилась: интерфейсы и полиморфизм, горутины и каналы, контекст и отмена. Всё, что здесь выглядело как правило («приёмники одного типа держите согласованными», «карта не гарантирует порядок»), получит там второе дно и настоящую причину. Оглавление курса останется навсегда — возвращайтесь к нему перед собеседованиями: двадцать пять уроков и одно работающее приложение — это уже не «пробую Go», это «пишу на Go».
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Финальный проект курса — консольный менеджер заметок: CLI с меню, таблицей, поиском, сортировкой и флагами готовности, собранный целиком из инструментов уроков 01–24.
- Структурирование проекта: слой данных (Note), слой операций (Store с методами), слой интерфейса (main с меню) — границы слоёв это сигнатуры методов.
- Хранилище — карта map[int]Note со счётчиком next: уникальные номера, delete без сдвигов, запятая-ok для проверки наличия; порядок карты случайный — вывод сортирует sort.Slice.
- Приёмники по правилам 24-го урока: Add/Delete/MarkDone на указателе меняют хранилище, List/Find на значении читают; Find возвращает пару (Note, bool).
- Флаг готовности — отдельная карта map[int]bool: запись в карту всегда разрешена, len считает готовые, чтение по отсутствующему ключу даёт false.
- Меню — бесконечный for + switch + fmt.Scan; выход — return внутри case 0; break завершает только switch; в песочнице stdin недоступен — интерактивная версия для локального запуска.
- Таблица — Printf с ширинами %-3d и %-16s: минус прижимает влево, хедер печатается тем же шаблоном; сортировка sort.Slice по компаратору, запасной путь — собрать ключи и sort.Strings.
- Фильтр по подстроке — strings.Contains в if внутри range; время создания в настоящем Go хранит time.Time, в песочнице честно заменено на int64-метку.
Домашнее задание
Довести менеджер заметок до личной версии. Шаг первый: добавьте команду 4 — «показать готовые»: метод Ready() []Note на Store собирает через range по s.done номера готовых и возвращает их заметки по возрастанию номеров (sort.Slice вам уже знаком). Шаг второй: метод Edit(id int, title string) bool — проверяет наличие, читает заметку во временную переменную, меняет заголовок, кладёт обратно; помните: прямой записи s.notes[id].Title = ... не бывает. Шаг третий: соберите полную версию с меню из неранимого блока: for, fmt.Scan(&cmd), switch, case 0 с return, — и запустите локально: go mod init notes, go run main.go. Шаг четвёртый: добавьте в таблицу колонку даты: переведите CreatedAt int64 в человекочитаемый вид хотя бы как число секунд, печатайте %-10d. Шаг пятый: заведите в комментарии три идеи, которые потребуют материалов следующего курса: сохранение в файл, регистронезависимый поиск, свои типы ошибок вместо bool. [Оглавление курса](/courses/go-basics/) — рядом на случай, если какой-то инструмент из 01–24 вспомнился смутно.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему хранилище заметок — карта, а не срез?
Карта map[int]Note держит номер заметки как ключ: удаление через delete не сдвигает остальные записи, проверка наличия — запятая-ok, чтение по номеру — прямое. Со срезом номер и позиция слиплись бы: после удаления середины заметки «съехали» бы, и номер перестал бы указывать на ту же запись. Недостаток карты — случайный порядок перебора — лечится сортировкой среза в List.
Почему в песочнице нельзя по-настоящему вводить команды меню?
Интерпретатор песочницы живёт на странице браузера, где поток ввода stdin недоступен — оба движка не могут ждать нажатий клавиатуры. Поэтому механика меню проверена ранимым диспетчером на заранее заданном списке команд, а полная версия с fmt.Scan показана в неранимом блоке для локального запуска: go mod init notes, go run main.go.
Почему поле CreatedAt в примерах int64, а не time.Time?
В настоящем Go время создания хранит time.Time из пакета time, и локальная версия урока показывает именно этот вариант. В песочнице резервный движок подменяет time.Now() сырым числом миллисекунд, которое меняется каждый запуск, а методы Format и Unix у него недоступны — вывод перестал бы быть воспроизводимым. Поэтому ранимые примеры используют int64 с фиксированной Unix-меткой: тип данных тот же, что и в базах данных, а честная оговорка в тексте объясняет замену.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Продвинутые возможности Go: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления