Go хорош тем, что первый HTTP API можно написать на стандартной библиотеке. Без фреймворка, генератора проекта и длинной настройки. В этом уроке мы сделаем два endpoint: /health, который возвращает статус сервера, и /tasks, который отдает JSON со списком задач
Цель не в том, чтобы построить идеальную архитектуру. Цель — увидеть, как в Go работают net/http, handler, status code и JSON-ответ
Что получится в конце
Сервер будет запускаться командой:
go run .
Проверка:
curl http://localhost:8080/health
Ответ:
{"status":"ok"}
И второй endpoint:
curl http://localhost:8080/tasks
вернет массив задач в JSON
Создаем проект
mkdir go-http-api
cd go-http-api
go mod init example.com/go-http-api
Создайте main.go:
package main
import (
"encoding/json"
"log"
"net/http"
)
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/tasks", tasksHandler)
log.Println("server started on http://localhost:8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
"status": "ok",
})
}
func tasksHandler(w http.ResponseWriter, r *http.Request) {
tasks := []Task{
{ID: 1, Title: "Разобрать net/http", Done: true},
{ID: 2, Title: "Вернуть JSON из handler", Done: false},
}
writeJSON(w, http.StatusOK, tasks)
}
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Println("write json:", err)
}
}
Запустите:
go run .
В другом окне терминала проверьте curl
Что такое mux
Строка:
mux := http.NewServeMux()
создает маршрутизатор из стандартной библиотеки. Он связывает URL-пути с функциями-обработчиками
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/tasks", tasksHandler)
Когда приходит запрос на /health, Go вызывает healthHandler. Когда приходит запрос на /tasks, вызывает tasksHandler
Для первого API этого достаточно. Фреймворк можно добавить позже, когда появится настоящая причина: middleware, роутинг посложнее, валидация, группы маршрутов
Как выглядит handler
Обработчик:
func healthHandler(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
"status": "ok",
})
}
получает два аргумента
http.ResponseWriter нужен, чтобы записать ответ: статус, заголовки, тело
*http.Request содержит входящий запрос: метод, путь, заголовки, body, query-параметры
В этом handler мы пока не читаем request. Но в реальном API через r вы будете проверять метод, читать query, парсить JSON из body и доставать контекст запроса
JSON-ответ без ручной сборки строк
Функция:
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Println("write json:", err)
}
}
делает три вещи:
- Ставит заголовок
Content-Type. - Записывает HTTP status code.
- Кодирует Go-значение в JSON.
Не собирайте JSON руками через строки. Пакет encoding/json умеет кодировать struct, map, slices и базовые типы
Зачем нужны json-теги
В Task есть теги:
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
Без тегов JSON получил бы имена ID, Title, Done. С тегами ответ становится привычным для API:
[
{"id":1,"title":"Разобрать net/http","done":true}
]
Это маленькая деталь, но она сразу делает ответ похожим на реальный backend
Добавляем проверку метода
Сейчас /tasks отвечает на любой HTTP-метод. Для учебного GET endpoint лучше явно ограничить:
func tasksHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{
"error": "method not allowed",
})
return
}
tasks := []Task{
{ID: 1, Title: "Разобрать net/http", Done: true},
{ID: 2, Title: "Вернуть JSON из handler", Done: false},
}
writeJSON(w, http.StatusOK, tasks)
}
Проверка:
curl -X POST http://localhost:8080/tasks
Ответ должен вернуть ошибку и статус 405
Частые ошибки
address already in use. Порт 8080 уже занят. Остановите старый сервер или поменяйте порт, например на :8081
curl не получает ответ. Проверьте, что сервер запущен в другом окне терминала и что адрес совпадает: http://localhost:8080
JSON скачет странными именами полей. Добавьте json-теги к экспортируемым полям struct
superfluous response.WriteHeader call. Статус ответа записали больше одного раза. В handler должен быть понятный путь: один ответ и затем return
Что может быть еще интересно по этой теме
Нужен ли фреймворк для первого API на Go? Нет. Стандартная библиотека закрывает базовый HTTP-сервер, handlers, заголовки и ответы. Фреймворк появляется позже по потребности
Можно ли так писать production API? Можно начать со стандартной библиотеки, но production потребует логирование, конфигурацию, graceful shutdown, таймауты, контекст, валидацию и тесты
Почему handler получает ResponseWriter и Request? Это базовый контракт net/http: один объект для ответа, второй для входящего запроса
Что добавить после GET endpoints? Следующий шаг — POST endpoint, чтение JSON body и сохранение данных в память или базу
Что открыть дальше
- Goroutines и channels: первая параллельность в Go — чтобы понимать, почему Go комфортен для серверов.
- Go modules: go mod init, tidy и зависимости — когда появятся внешние пакеты.
- Go и PostgreSQL: мини-API с базой данных — следующий практический backend-шаг.
- Ошибки в Go: error, panic и recover без суеты — для нормальной обработки ошибок в API.



