MEASUREMENT
bench/gohttp/internals.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/http-server
- How to run it
go run bench/gohttp/internals.go go run bench/gohttp/practice.go go run bench/gohttp/protocols.go
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «HTTP-сервер стандартной библиотеки»
| Файл | Что делает |
|---|---|
internals.go |
поднимает серверы net/http на loopback и печатает шесть наблюдений: горутина на соединение, нулевые таймауты по умолчанию, контекст запроса после ухода клиента, Shutdown против Close и возврат Serve, паника в обработчике, шаблоны ServeMux Go 1.22 |
protocols.go |
добавлен после аудита 07.10.2026: keep-alive HTTP/1.1 и одновременные запросы HTTP/2 по одному соединению, паника в обработчике под HTTP/1.1 и HTTP/2, Close и перехваченное соединение, MaxHeaderBytes по умолчанию. HTTP/2 — без TLS, через Server.Protocols go1.24 |
practice.go |
источник ответов двух практических задач урока |
go run bench/gohttp/internals.go
go run bench/gohttp/practice.go
go run bench/gohttp/protocols.go
Записи прогонов — в runs/, go1.24.7 linux/amd64, GOMAXPROCS=2. Все серверы
слушают только 127.0.0.1 на свободном порту.
Что получилось
Горутина на соединение. Десять запросов к обработчику, который спит 200 мс, заняли 209 мс: пик — 10 обработчиков одновременно.
Таймауты сервера по умолчанию — ноль. http.Server{} не ограничивает ни
чтение заголовков, ни запись, ни простой. Клиент, приславший начало
заголовков, через 2 с всё ещё держит соединение; с ReadHeaderTimeout 300 мс
сервер закрыл его через 301 мс.
Контекст отменяется, работа — нет. Клиент ушёл через 100 мс. Обработчик,
слушающий r.Context(), остановился через 101 мс с context canceled;
обработчик, который его не слушает, доделал всю работу — 500 мс.
Serve возвращается сразу. Shutdown дождался пяти запросов в полёте
(все 200), Close оборвал все пять. В обоих случаях Serve вернул
http: Server closed через 0 мс после вызова — запросы в этот момент ещё
работали.
Паника в обработчике не роняет процесс. Клиент получил ошибку соединения,
сервер записал в журнал http: panic serving …: boom, следующий запрос — 200.
ServeMux Go 1.22. GET /items/{id}: GET /items/42 → 200,
POST → 405 с Allow: GET, HEAD, /items/42/x → 404. В практике — обратная
сторона: при шаблоне без метода /users/ метод, которого нет в более
конкретном шаблоне, уходит туда, и 405 не будет.
После аудита 07.10.2026 (protocols.go)
Соединение — не запрос. Десять запросов подряд одним клиентом HTTP/1.1 прошли по одному соединению. Десять одновременных запросов HTTP/2 — тоже по одному, с десятью обработчиками одновременно (202 мс при сне 200 мс).
Паника зависит от протокола. HTTP/1.1: соединение закрыто, всего
соединений 3, и обработчик /boom вызван дважды — клиентский Transport
повторил идемпотентный GET. HTTP/2: сброшен только поток, соединение
переиспользовано, обработчик вызван один раз.
Close не знает о перехваченных соединениях. После srv.Close()
соединение, забранное через Hijack, продолжило отвечать.
MaxHeaderBytes по умолчанию — 1 МиБ (http.DefaultMaxHeaderBytes = 1048576): заголовок 4096 байт — 200, 2097152 байт — 431.
Script
279 lines//go:build ignore
// HTTP-сервер стандартной библиотеки: что net/http делает сам и чего не
// делает, пока его не попросят.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. «Как сделать веб-сервер на Go» — вопрос на одну строку
// http.ListenAndServe. Вопросы, на которых проверяют, начинаются после неё:
// сколько запросов сервер обслуживает одновременно, что будет с медленным
// клиентом, что происходит с работой, когда клиент ушёл, как остановить
// сервер, не оборвав запросы. Каждый ответ здесь получен запуском настоящего
// сервера на loopback.
//
// ШЕСТЬ СЮЖЕТОВ:
//
// 1. Горутина на соединение: десять запросов по 200 мс обслуживаются
// одновременно.
// 2. Таймауты сервера по умолчанию — ноль: клиент, не дославший заголовки,
// держит соединение сколько угодно, пока не задан ReadHeaderTimeout.
// 3. Контекст запроса отменяется, когда клиент ушёл, — но обработчик,
// который его не слушает, доделывает работу.
// 4. Shutdown дожидается запросов в полёте, Close — обрывает их, а Serve
// возвращается сразу после вызова Shutdown, не дожидаясь их.
// 5. Паника в обработчике не роняет процесс: сервер её перехватывает и
// закрывает соединение.
// 6. ServeMux с шаблонами Go 1.22: метод и параметр пути в шаблоне, 405 с
// заголовком Allow без единой строки кода.
//
// ЗАПУСК:
//
// go run bench/gohttp/internals.go
package main
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
// serve поднимает сервер на свободном порту loopback и возвращает его адрес.
func serve(srv *http.Server) (string, chan error) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
done := make(chan error, 1)
go func() { done <- srv.Serve(ln) }()
return "http://" + ln.Addr().String(), done
}
// client без пула соединений: каждый запрос — своё соединение.
func client(timeout time.Duration) *http.Client {
return &http.Client{Timeout: timeout, Transport: &http.Transport{DisableKeepAlives: true}}
}
func ms(d time.Duration) float64 { return float64(d.Microseconds()) / 1000 }
func main() {
fmt.Printf("%s %s/%s, GOMAXPROCS=%d | HTTP-сервер net/http\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
// ------------------------------------------------------------------ 1
show("1) ГОРУТИНА НА СОЕДИНЕНИЕ")
var inFlight, peak atomic.Int64
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n := inFlight.Add(1)
for {
p := peak.Load()
if n <= p || peak.CompareAndSwap(p, n) {
break
}
}
time.Sleep(200 * time.Millisecond)
inFlight.Add(-1)
})}
base, _ := serve(srv)
start := time.Now()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, err := client(0).Get(base)
if err == nil {
resp.Body.Close()
}
}()
}
wg.Wait()
fmt.Printf(" 10 запросов, обработчик спит 200 мс: всего %.0f мс\n", ms(time.Since(start)))
fmt.Printf(" обработчиков одновременно, пик: %d\n", peak.Load())
fmt.Println(" Каждое принятое соединение сервер обслуживает в своей горутине:")
fmt.Println(" ни очереди, ни пула обработчиков, ни предела по умолчанию.")
srv.Close()
// ------------------------------------------------------------------ 2
show("2) ТАЙМАУТЫ СЕРВЕРА ПО УМОЛЧАНИЮ")
var zero http.Server
fmt.Printf(" http.Server{}: ReadTimeout=%v ReadHeaderTimeout=%v WriteTimeout=%v IdleTimeout=%v\n",
zero.ReadTimeout, zero.ReadHeaderTimeout, zero.WriteTimeout, zero.IdleTimeout)
fmt.Println(" клиент прислал начало заголовков и замолчал:")
for _, rht := range []time.Duration{0, 300 * time.Millisecond} {
s := &http.Server{Handler: http.NotFoundHandler(), ReadHeaderTimeout: rht}
b, _ := serve(s)
c, err := net.Dial("tcp", strings.TrimPrefix(b, "http://"))
if err != nil {
panic(err)
}
fmt.Fprintf(c, "GET / HTTP/1.1\r\nHost: x\r\n")
t0 := time.Now()
c.SetReadDeadline(time.Now().Add(2 * time.Second))
_, err = bufio.NewReader(c).ReadString('\n')
state := "соединение закрыто сервером"
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
state = "соединение всё ещё открыто"
}
fmt.Printf(" ReadHeaderTimeout=%-6v через %4.0f мс: %s\n", rht, ms(time.Since(t0)), state)
c.Close()
s.Close()
}
fmt.Println(" 2 с — предел ожидания самого клиента в этом замере. Сервер без")
fmt.Println(" таймаута держал бы соединение и его горутину сколько угодно.")
// ------------------------------------------------------------------ 3
show("3) КОНТЕКСТ ЗАПРОСА, КОГДА КЛИЕНТ УШЁЛ")
type outcome struct {
label string
after time.Duration
note string
}
results := make(chan outcome, 2)
mux3 := http.NewServeMux()
mux3.HandleFunc("/listens", func(w http.ResponseWriter, r *http.Request) {
t0 := time.Now()
select {
case <-time.After(500 * time.Millisecond):
results <- outcome{"слушает r.Context()", time.Since(t0), "доделал работу"}
case <-r.Context().Done():
results <- outcome{"слушает r.Context()", time.Since(t0), "остановился: " + r.Context().Err().Error()}
}
})
mux3.HandleFunc("/ignores", func(w http.ResponseWriter, r *http.Request) {
t0 := time.Now()
time.Sleep(500 * time.Millisecond)
results <- outcome{"не слушает контекст", time.Since(t0), "доделал работу; контекст: " + fmt.Sprint(r.Context().Err())}
})
s3 := &http.Server{Handler: mux3}
b3, _ := serve(s3)
fmt.Println(" клиент ждёт ответа 100 мс и уходит; работа обработчика — 500 мс")
for _, path := range []string{"/listens", "/ignores"} {
_, err := client(100 * time.Millisecond).Get(b3 + path)
if err == nil {
panic("клиент должен был уйти по таймауту")
}
o := <-results
fmt.Printf(" %-22s через %4.0f мс: %s\n", o.label, ms(o.after), o.note)
}
s3.Close()
// ------------------------------------------------------------------ 4
show("4) ОСТАНОВКА: Shutdown, Close И ВОЗВРАТ ИЗ Serve")
slow := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(300 * time.Millisecond)
io.WriteString(w, "ok")
})
for _, how := range []string{"Shutdown", "Close"} {
s := &http.Server{Handler: slow}
b, served := serve(s)
statuses := make(chan string, 5)
for i := 0; i < 5; i++ {
go func() {
resp, err := client(0).Get(b)
if err != nil {
statuses <- "ошибка"
return
}
resp.Body.Close()
statuses <- fmt.Sprint(resp.StatusCode)
}()
}
time.Sleep(100 * time.Millisecond)
t0 := time.Now()
stopped := make(chan time.Duration, 1)
go func() {
if how == "Shutdown" {
s.Shutdown(context.Background())
} else {
s.Close()
}
stopped <- time.Since(t0)
}()
err := <-served
servedAfter := time.Since(t0)
_, newErr := client(time.Second).Get(b)
counts := map[string]int{}
for i := 0; i < 5; i++ {
counts[<-statuses]++
}
fmt.Printf(" %s через 100 мс после старта пяти запросов по 300 мс:\n", how)
fmt.Printf(" Serve вернул %v через %.0f мс после вызова\n", err, ms(servedAfter))
fmt.Printf(" %s вернул управление через %.0f мс\n", how, ms(<-stopped))
fmt.Printf(" запросы в полёте: 200 — %d, ошибка — %d\n", counts["200"], counts["ошибка"])
fmt.Printf(" новый запрос после вызова: %s\n", map[bool]string{true: "отклонён", false: "принят"}[newErr != nil])
}
fmt.Println(" Serve возвращается сразу после вызова Shutdown, не дожидаясь")
fmt.Println(" запросов в полёте: программа, которая завершается, как только")
fmt.Println(" вернулся ListenAndServe, обрывает их так же, как Close.")
// ------------------------------------------------------------------ 5
show("5) ПАНИКА В ОБРАБОТЧИКЕ")
var logBuf bytes.Buffer
mux5 := http.NewServeMux()
mux5.HandleFunc("/boom", func(w http.ResponseWriter, r *http.Request) { panic("boom") })
mux5.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "ok") })
s5 := &http.Server{Handler: mux5, ErrorLog: log.New(&logBuf, "", 0)}
b5, _ := serve(s5)
_, err := client(time.Second).Get(b5 + "/boom")
fmt.Printf(" GET /boom: ошибка у клиента: %v\n", err != nil)
resp, err := client(time.Second).Get(b5 + "/ok")
if err == nil {
resp.Body.Close()
fmt.Printf(" GET /ok после паники: %d\n", resp.StatusCode)
}
first := strings.SplitN(logBuf.String(), "\n", 2)[0]
if i := strings.Index(first, "127.0.0.1:"); i >= 0 {
if j := strings.Index(first[i:], ":"); j >= 0 {
rest := first[i+j+1:]
if k := strings.Index(rest, ":"); k >= 0 {
first = first[:i] + "127.0.0.1:…" + rest[k:]
}
}
}
fmt.Printf(" журнал сервера: %s\n", first)
fmt.Println(" Процесс жив: сервер перехватил панику этого запроса, записал её")
fmt.Println(" в журнал и закрыл соединение. Ответа клиент не получил.")
s5.Close()
// ------------------------------------------------------------------ 6
show("6) ServeMux С ШАБЛОНАМИ GO 1.22")
mux6 := http.NewServeMux()
mux6.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "id="+r.PathValue("id"))
})
s6 := &http.Server{Handler: mux6}
b6, _ := serve(s6)
for _, rq := range [][2]string{{"GET", "/items/42"}, {"POST", "/items/42"}, {"GET", "/items/42/x"}} {
req, _ := http.NewRequest(rq[0], b6+rq[1], nil)
resp, err := client(time.Second).Do(req)
if err != nil {
panic(err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
extra := strings.TrimSpace(string(body))
if allow := resp.Header.Get("Allow"); allow != "" {
extra += " | Allow: " + allow
}
fmt.Printf(" %-4s %-12s -> %d %s\n", rq[0], rq[1], resp.StatusCode, extra)
}
s6.Close()
}