Первый HTTP-сервер на Go
Приложение учёта начинается не с таблицы и не с JavaScript. Сначала нужен процесс, который принимает HTTP-запрос и возвращает предсказуемый ответ. В этой серии построим «Инвентарь»: небольшой локальный инструмент для карточек оборудования, остатков и движений. Один проект позволит увидеть, как серверные страницы постепенно получают интерактивность без отдельного клиентского приложения.
Предполагается, что вы уже знакомы с переменными, функциями и структурами Go, а также представляете назначение HTML. Специальный веб-фреймворк не понадобится: маршрутизацию, сервер, шаблоны и работу с базой предоставляет стандартная библиотека. В первом состоянии достаточно Go 1.27.2. Это зафиксированная версия курса; переключение на другой выпуск следует рассматривать как отдельное изменение окружения, а не незаметное обновление примера.
Процесс, адрес и запрос
Сервер — обычная программа, которая остаётся запущенной между обращениями браузера. Адрес 127.0.0.1:4493 состоит из интерфейса и порта. Loopback означает, что приложение доступно с вашего компьютера. Это удобно для учебного стенда: соседние устройства не должны автоматически получить доступ к будущим формам учёта. Если порт занят, процесс не сможет слушать его; изменение адреса в коде потребует согласованного изменения ссылок следующих уроков.
Создадим go.mod и main.go в одной папке. Имя модуля professorweb.local/inventory описывает наш проект и не означает, что где-то существует публичный сервер с таким доменом. На этом шаге сторонних пакетов нет. В архиве состояние lesson-01 содержит оба файла полностью. Не копируйте его поверх работающей базы из другой главы: каждое состояние серии предназначено для отдельного самостоятельного прохождения.
В main.go помещается следующая полная программа. Её важный результат — разделение маршрутизатора и параметров HTTP-сервера.
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main(){
mux:=http.NewServeMux()
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter,r *http.Request) {
w.Header().Set("Content-Type","text/plain; charset=utf-8")
fmt.Fprintln(w,"Инвентарь: сервер готов")
})
server:=&http.Server{Addr:"127.0.0.1:4493",Handler:mux,ReadHeaderTimeout:5*time.Second}
log.Println("http://127.0.0.1:4493")
log.Fatal(server.ListenAndServe())
}
http.NewServeMux создаёт таблицу правил. Обработчик получает два аргумента: через ResponseWriter формируется ответ, а Request представляет входящее обращение. Наш обработчик ничего не сохраняет между запросами. Его локальные переменные исчезают после завершения функции; длительное время жизни сервера не делает каждую переменную общим хранилищем автоматически.
Шаблон GET /{$} ограничивает правило корневым адресом и методом GET. Завершающий маркер нужен именно для точного корня. Без понимания правил сопоставления легко случайно сделать обработчик ответом на все вложенные пути. При неизвестном адресе маршрутизатор должен сообщать об отсутствии ресурса, а не выдавать красивую главную страницу с успешным статусом. Позже это различие поможет отделить отсутствующую карточку от неисправности базы.
Заголовок и тело ответа
У ответа есть статус, заголовки и тело. В данном обработчике тело — строка UTF-8, поэтому Content-Type установлен в text/plain; charset=utf-8. Браузер использует этот заголовок для интерпретации содержимого. Русский текст без согласованной кодировки может отображаться неправильно даже при корректных строках в исходном файле. Проблема тогда находится на границе HTTP, а не в типах Go.
Заголовок устанавливается до записи тела. Первый вызов записи без явного WriteHeader начинает успешный ответ со статусом 200. После этого нельзя произвольно передумать и отправить другой статус: часть ответа уже могла уйти клиенту. Пока строка короткая, ошибка формирования почти невероятна, но при работе с шаблоном мы сначала подготовим тело в буфере и только затем начнём выдачу. Это прямое следствие устройства ответа.
ReadHeaderTimeout ограничивает ожидание заголовков входящего запроса. Его назначение отличается от времени вычисления обработчика и от времени ожидания браузера. Один параметр не превращает приложение в устойчивый публичный сервис, но уже показывает, что работа сервера состоит из нескольких фаз. На последнем шаге серии добавим остальные таймауты и штатное завершение процесса, когда существующее соединение нельзя просто оборвать.
Сообщение log.Println выводится в терминал процесса. Оно не попадает в тело HTTP-ответа и не видно посетителю страницы. Это две независимые линии наблюдения. Если браузер ничего не показывает, полезно сначала установить, запущен ли процесс и удалось ли открыть порт. Если процесс работает, следующая граница — совпадение адреса, метода и маршрута. Такая последовательность экономит время по сравнению с изменением сразу всего кода.
Ожидаемое обращение браузера
Для самостоятельного прохождения откройте папку lesson-01 и выполните go run .. Эта команда приведена для читателя; при подготовке статьи программа не запускалась. Ожидается, что терминал сообщит локальный адрес, а GET-запрос к корню получит строку «Инвентарь: сервер готов». Здесь нет ни измерения скорости, ни подтверждённого результата работы конкретной машины. Описываемая выдача следует из обработчика.
Попробуйте мысленно изменить путь на /items. В таблице нет соответствующего правила, поэтому ответ должен отличаться от корневого. Затем измените метод на POST к корню: наш GET-обработчик не становится обработчиком записи только потому, что путь совпал. Метод — часть договора. Будущая форма будет отправлять POST, а просмотр списка останется GET, чтобы перезагрузка и открытие ссылки не создавали новые карточки.
Отдельный ожидаемый случай — повторный запуск второго экземпляра на том же порту. Он должен завершиться ошибкой прослушивания. Это не конфликт карточек и не проблема браузерного кэша. Один адрес обычно обслуживает один слушающий процесс. Чтобы продолжить прохождение, остановите прежний экземпляр либо выберите отдельный порт и явно зафиксируйте изменение; не переключайте случайно весь курс на разные адреса.
Пока наш сервер показывает только своё существование. Он ещё не знает ни артикула оборудования, ни количества, ни места хранения. Следующий урок добавит предметную модель, сохранив тот же маршрут и ту же границу HTTP. Благодаря этому изменение будет видно в одном месте: ответ получит содержательные строки, а запуск процесса, адрес и устройство обработчика останутся уже знакомыми.
Полезно также различать отказ соединения и HTTP-ошибку. Если процесс не слушает порт, браузер не получает никакого статуса вашего приложения. Если сервер ответил 404, соединение уже состоялось, и запрос дошёл до маршрутизатора. Эти признаки задают разные направления поиска причины. Для команды разработки это небольшой, но важный общий словарь: «сервер недоступен» и «ресурс отсутствует» не должны означать одну неопределённую проблему. Уже первый обработчик позволяет провести эту границу без базы и дополнительных библиотек.
Исходники урока
Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-01. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.
Первичные источники: документация 1, документация 2.