Мини-проект, чтобы руками понять клиент-серверную архитектуру и REST на чистой стандартной библиотеке Go.
-
GET /— проверка, что сервер жив: возвращаетHello, Go server!. -
POST /note— принимает JSON заметки и возвращает её же.Успех: 201 Created .
Ошибки:
- 400 Bad Request — невалидный JSON (
{"detail":"invalid JSON"}) или пустоеname({"detail":"name is required"}); - 405 Method Not Allowed — если дернуть не POST.
- 400 Bad Request — невалидный JSON (
-
GET /ping→{"message":"pong"}(быстрая проверка JSON-ответа). -
GET /notes— вернуть список заметок. -
GET /note/{id}— вернуть заметку по id. -
DELETE /note/{id}— удалить заметку по id.
"id": 1, // назначается сервером
"name": "string", // обязательно
"text": "string" // опционально
}
go run .
# сервер слушает http://localhost:8000curl http://localhost:8000/
# Hello, Go server!curl -i -X POST http://localhost:8000/note \
-H 'Content-Type: application/json' \
-d '{"name":"first","text":"hello go"}'Ожидаемо:
HTTP/1.1 201 Created
Content-Type: application/json
{"name":"first","text":"hello go"}
curl -i -X POST http://localhost:8000/note \
-H 'Content-Type: application/json' \
-d '{"name":"","text":"no name"}'Ответ:
HTTP/1.1 400 Bad Request
{"detail":"name is required"}
curl -i -X POST http://localhost:8000/note \
-H 'Content-Type: application/json' \
-d 'this is not json'Ответ:
HTTP/1.1 400 Bad Request
{"detail":"invalid JSON"}
curl -i http://localhost:8000/note/1
# HTTP/1.1 200 OK
# {"id":1,"name":"first","text":"hello"}
curl -i http://localhost:8000/note/1
# HTTP/1.1 200 OK
# {"id":1,"name":"first","text":"hello"}
curl -i -X DELETE http://localhost:8000/note/1
# HTTP/1.1 200 OK
# {"status":"ok"}
curl -i -X DELETE http://localhost:8000/note/1
# HTTP/1.1 404 Not Found
# {"detail":"Note not found"}
-i— показать статус и заголовки в ответе.-v— подробный режим (видно, что реально ушло/пришло).-H 'Content-Type: application/json'— обязательно для JSON-тел.-d '…'— тело запроса. Используй одинарные кавычки'…', а не обратные…— те запускают команду в bash.- Перенос строки
\допустим, но после слеша не должно быть пробелов
net/http — сервер и маршрутизатор (http.NewServeMux).
-
Обработчики имеют сигнатуру:
func(w http.ResponseWriter, r *http.Request)
где
w— «куда писать ответ»,r— «что пришло». -
JSON: пакет
encoding/json(json.NewDecoder(r.Body).Decode(&in)/json.NewEncoder(w).Encode(out)). -
Модель данных (ожидаемый JSON):
type Note struct { Name string `json:"name"` Text string `json:"text"` }
-
Статусы:
http.StatusCreated(201),http.StatusBadRequest(400),http.StatusMethodNotAllowed(405) и т.д.
-
Маршрутизатор —
http.NewServeMux(). Регистрируем пути черезHandleFunc. -
Хендлер — обычная функция
func(w http.ResponseWriter, r *http.Request)
где
r— всё про запрос,w— куда писать ответ. -
JSON :
-
чтение:
json.NewDecoder(r.Body).Decode(&in) -
запись:
json.NewEncoder(w).Encode(out) -
Память/БД :
type notesDB struct { mu sync.RWMutex // замок data []Note // "таблица" заметок next int // автоинкремент id }
- для чтения :
RLock()/RUnlock()— много одновременных читателей OK; - для записи :
Lock()/Unlock()— только один писатель; - в
list()отдаём копию среза, чтобы внешние изменения не портили внутреннее состояние:out := make([]Note, len(db.data)) copy(out, db.data) return out
- Парсинг
{id}из пути : отрезаем префикс"/note/", убираем хвостовой/, преобразуем строку в число (strconv.Atoi). Если не вышло — 404.
201 Created— создан ресурс (POST /note), в заголовкеLocation: /note/{id}.200 OK— обычные успешные ответы.400 Bad Request— невалидный JSON или нарушена валидация.404 Not Found— заметка не найдена (неверныйid).405 Method Not Allowed— метод не подходит для маршрута.
-
- Подняли PostgreSQL в Docker (
db), проверили соединение. - Применили миграцию
0001_init.up.sql→ таблицаpublic.notesи индексidx_notes_nameсозданы. - Запустили и проверили
check.sql:INSERT → UPDATE → EXPLAIN ANALYZE → DELETE, индекс используется (Bitmap Index Scan).
- Добавили удобные цели в
Makefile:db-up,migrate-up,migrate-down,sql-check. - Подготовили Go-сервис к работе с БД:
- Ввели абстракцию
Storeи реализацию на Postgres (pgStore), сохранили in-memory как fallback. - Обновили хендлеры (
create/list/update/delete) подStore.
- Ввели абстракцию
- Провели смоук-тесты
curlи прямые проверки черезpsql.
-
Единый источник данных:
- Либо навсегда включить Postgres-режим (требовать
DB_DSN), - Либо оставить dual-mode, но явно логировать, какой Store активен.
- Либо навсегда включить Postgres-режим (требовать
-
Корректная обработка
no rows:Использовать
errors.Is(err, pgx.ErrNoRows)вместо парсинга строки ошибок. -
UPDATE … RETURNING:Возвращать из БД фактическую запись (а не присланный JSON), чтобы клиент получал правду при частичных апдейтах.
-
Грациозное закрытие пула БД:
При остановке сервера вызывать
pgPool.Close(). -
Docker Compose:
- Удалить устаревшее поле
version:(предупреждение в логах). - Для
appявно задатьDB_DSN=postgres://notes:notes@db:5432/notesdb?sslmode=disableи проброс8000:8000. - БД наружу оставить на
5433:5432(внутри сетиappходит наdb:5432).
- Удалить устаревшее поле
-
Конфиг и запуск:
.envс переменными (DB_DSN, порт, лог-уровень).- Цели
make run,make logs,make down, healthcheck в compose.
-
Валидация и ответы:
- Свести всю проверку входных данных в
validate*, чтобы не дублировать логику. - Единый формат ошибок
{"detail":"…"}(у тебя уже почти так).
- Свести всю проверку входных данных в
-
Миграции в рантайме (опционально):
Подключить
pressly/gooseилиgolang-migrateи прогонять миграции при стартеapp. -
Наблюдаемость (опционально):
Структурные логи (
zerolog/slog), таймауты контекстов для запросов к БД,/healthendpoint. -
Документация и тесты (по мере времени):
- Мини-README со сценариями запуска.
httptestдля хендлеров и интеграционный тест к БД (dockerized).