HTTP-сервер на net/http: регистрация обработчиков и формирование ответа
Содержание курса
HTTP-сервис приветствия
Небольшой веб-сервис на Go — это, пожалуй, самый наглядный способ увидеть, как стандартная библиотека превращает несколько десятков строк кода в живую сетевую программу. Здесь нет ничего лишнего: маршрутизатор, обработчики, порт — и сервер готов принимать запросы.
В центре задачи стоит функция newMux, которая собирает и возвращает свежий мультиплексор с двумя маршрутами. Первый отвечает за здоровье сервиса и позволяет внешним наблюдателям убедиться, что процесс жив. Второй — дружелюбно приветствует любого, кто обратится по нужному адресу. Важно понимать, что каждый обработчик обязан вести себя предсказуемо: правильный метод — правильный ответ, неправильный метод — явный отказ, а несуществующий путь должен честно сообщать о своём отсутствии. Порядок выставления заголовков тоже имеет значение: они должны быть готовы прежде, чем клиент увидит статус или тело ответа.
Что нужно сделать
Допишите service.go. Функция newMux должна при каждом вызове возвращать новый *http.ServeMux. На нём нужны два маршрута: GET /health отвечает строкой ok, а GET /hello — строкой Hello, Go!. Для другого метода на /hello верните 405 и текст method not allowed. Неизвестный путь должен остаться обычным 404 от ServeMux.
Перед записью статуса и тела установите Content-Type: text/plain; charset=utf-8. В main запустите сервер на :18080 и обработайте ошибку ListenAndServe. Здесь ещё нет JSON API: задача только про mux, обработчик и HTTP-ответ.
О данных в ответах
Используйте учебные данные. Не вставляйте пароли, токены, ключи доступа, паспортные и банковские данные, а также персональные данные других людей. Политика обработки данных.
- Public go test ./...
- healthGET /health → 200
- helloGET /hello → 200
Сначала выполняется go test ./..., затем сервис собирается и проверяется HTTP-запросами. Внешняя сеть и скачивание модулей отключены.
service_test.go
package main
import (
"context"
"io"
"net"
"net/http"
"net/http/httptest"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
func TestNewMux(t *testing.T) {
if newMux() == newMux() { t.Fatal("newMux must return a fresh mux") }
for _, tc := range []struct{method, path string; status int; body string}{
{http.MethodGet, "/health", http.StatusOK, "ok\n"},
{http.MethodGet, "/hello", http.StatusOK, "Hello, Go!\n"},
{http.MethodPost, "/hello", http.StatusMethodNotAllowed, "method not allowed\n"},
{http.MethodGet, "/missing", http.StatusNotFound, "404 page not found\n"},
} {
t.Run(tc.method+tc.path, func(t *testing.T) {
req := httptest.NewRequest(tc.method, tc.path, nil)
rr := httptest.NewRecorder()
newMux().ServeHTTP(rr, req)
resp := rr.Result(); defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != tc.status || string(body) != tc.body { t.Fatalf("status=%d body=%q", resp.StatusCode, body) }
if tc.path != "/missing" && !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/plain") { t.Fatalf("content-type=%q", resp.Header.Get("Content-Type")) }
})
}
}
func TestMainReportsListenError(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:18080")
if err != nil { t.Skipf("cannot reserve port 18080: %v", err) }
defer listener.Close()
binary := filepath.Join(t.TempDir(), "service")
if runtime.GOOS == "windows" { binary += ".exe" }
build := exec.Command("go", "build", "-o", binary, ".")
if output, err := build.CombinedOutput(); err != nil { t.Fatalf("build: %v\n%s", err, output) }
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
run := exec.CommandContext(ctx, binary)
output, err := run.CombinedOutput()
if ctx.Err() != nil { t.Fatal("program did not stop after ListenAndServe failed") }
if err == nil { t.Fatal("program ignored the ListenAndServe error and exited successfully") }
if strings.TrimSpace(string(output)) == "" { t.Fatal("program did not report the ListenAndServe error") }
}
Отправьте решение, чтобы увидеть результаты тестов.
