Перейти к основному содержимому

Веб на стандартной библиотеке Go

Разработчику

См. также: Фреймворки и библиотеки Go · Пример микросервиса · Gin · Важные интерфейсы.

Зачем stdlib, если есть Gin и Echo

Пакет net/http — полноценный HTTP-стек: сервер, клиент, TLS, cookies, multipart. Фреймворки строятся поверх него (кроме Fiber на fasthttp). Умение писать на stdlib нужно, чтобы:

  • понимать, что делают middleware и контекст во фреймворках;
  • собирать лёгкие сервисы без лишних зависимостей;
  • встраивать сторонние http.Handler в существующий сервер.

Маршрутизация

Классический ServeMux

mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("GET /users/{id}", userByID)

log.Fatal(http.ListenAndServe(":8080", mux))

С Go 1.22+ ServeMux понимает метод HTTP и шаблоны вроде {id} в пути. Параметры читают через r.PathValue("id").

Обработчик как http.Handler

type greetHandler struct{ prefix string }

func (h greetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, %s", h.prefix, r.URL.Query().Get("name"))
}

mux.Handle("GET /hi", greetHandler{prefix: "Привет"})

Тип с методом ServeHTTP удобен, когда обработчик хранит зависимости (БД, конфиг).


Query, формы и multipart

ИсточникКак читать
Query string ?a=1r.URL.Query().Get("a")
application/x-www-form-urlencodedr.ParseForm()r.FormValue("email")
multipart/form-datar.ParseMultipartForm(maxMem)r.FormValue, r.FormFile
func contact(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
email := r.FormValue("email")
// ...
}

Для загрузки файлов — r.FormFile("avatar"), не забывать defer file.Close().


Middleware

Идиома — обёртка над http.Handler:

func withLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}

mux.Handle("GET /api/", withLogging(apiHandler))

Цепочку можно собирать вручную или маленькой функцией chain(mw ...Middleware) http.Handler. Панику в production лучше перехватывать отдельным middleware и логировать, не полагаясь на «аварийный» ответ клиенту.


HTML-шаблоны

Пакет html/template (не text/template для веб-страниц) экранирует вывод и снижает риск XSS:

var page = template.Must(template.New("home").Parse(`
<!DOCTYPE html>
<html><body>
<h1>{{.Title}}</h1>
<p>Привет, {{.Name}}</p>
</body></html>
`))

func home(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = page.Execute(w, struct {
Title, Name string
}{"Заметки", r.URL.Query().Get("name")})
}

Шаблоны компилируют один раз (template.Must при старте). Для JSON-API шаблоны не нужны — достаточно encoding/json (важные интерфейсы).


JSON-ответы

func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(v)
}

Ошибки API обычно оформляют единой структурой { "error": "..." } и фиксированными кодами HTTP.


Контекст запроса

r.Context() наследует отмену при обрыве клиента. Для таймаутов и отмены БД:

ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT ...", id)

Подробнее о context.Context — в важных интерфейсах. Фреймворки прокидывают тот же контекст в свой API.


Корректная остановка сервера

ListenAndServe блокирует навсегда. Для деплоя в Kubernetes нужен graceful shutdown:

srv := &http.Server{Addr: ":8080", Handler: mux}

go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown: %v", err)
}

Shutdown перестаёт принимать новые соединения и ждёт завершения активных запросов в пределах таймаута.


Когда переходить на фреймворк

ЗадачаStdlib достаточно
Healthcheck, webhook, внутренний APIЧасто да
Группы маршрутов, валидация, OpenAPIУдобнее Gin/Echo
Максимальный RPS на одном ядреСмотреть бенчмарки; иногда Fiber

Практика: Gin · Echo · Простые приложения.


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).