HTTP server на Go: первый API без фреймворка

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)
    }
}

делает три вещи:

  1. Ставит заголовок Content-Type.
  2. Записывает HTTP status code.
  3. Кодирует 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 и сохранение данных в память или базу

Что открыть дальше

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x