JavaRush /Курси /Go SELF /http.Server vs http.ListenAndServe

http.Server vs http.ListenAndServe

Go SELF
Рівень 59 , Лекція 0
Відкрита

1. Два способи запуску та роль http.Handler

Коли ви вперше бачите HTTP‑сервер у Go, може здатися, що тут і обговорювати нічого: є одна функція http.ListenAndServe, і цього досить. У маленькому прикладі так і є — одного рядка справді вистачає. Але щойно ви починаєте писати не «приклад на 8 рядків», а застосунок, виникає практичне запитання: де налаштовувати адресу, який обробник прийматиме запити і що робити, коли знадобиться складніша конфігурація, ніж просто «увімкнути сервер». Саме тут і з’являється розвилка: швидкий старт через ListenAndServe або явна конфігурація через http.Server.

Уявіть, що ви відкриваєте мінікафе. Можна просто написати на дверях «Працюємо» і почати продавати каву — це ListenAndServe. А можна повісити розклад, призначити відповідального, налаштувати касу, правила закриття зміни та журнал інцидентів — це http.Server. Обидва варіанти робочі. Різниця — у керованості.

Ментальна модель: сервер крутиться навколо http.Handler

Перш ніж порівнювати API, важливо зрозуміти, навколо чого взагалі будується сервер у net/http. У Go HTTP‑сервер працює за простою моделлю: приходить запит → сервер обирає обробник → обробник пише відповідь. Центральне слово тут — обробник.

У стандартній бібліотеці обробник описується інтерфейсом http.Handler, у якого є рівно один метод ServeHTTP(w, r). Саме це «контрактне місце», куди Go передає кожен вхідний запит. І якщо ви зрозумієте, що «сервер — це штука, яка викликає обробник», усе стане на свої місця: і ServeMux, і ListenAndServe, і http.Server.

Погляньмо на шлях запиту у спрощеному вигляді:

flowchart LR
    C[HTTP-клієнт] --> S[сервер net/http]
    S --> H[http.Handler]
    H --> W[ResponseWriter: статус/заголовки/тіло]

Ідея проста: сервер — це інфраструктура доставки запитів до обробника.

2. http.ListenAndServe: швидкий старт

Якщо ви хочете підняти сервер просто зараз і не розписувати зайві структури, http.ListenAndServe(addr, handler) — ваш вибір. Цей виклик робить дві речі: створює сервер «під капотом» і починає слухати порт, приймаючи запити.

Ось мінімальний приклад у стилі «є маршрут "/health", який просто каже «я живий»»:

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNoContent) // 204
	})

	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Println("помилка запуску:", err)
	}
}

Тут важливо помітити одне: ми передали mux як handler. Тобто сервер не «сам шукає функції». Він просто викликатиме mux.ServeHTTP(...), а mux уже всередині обере потрібний маршрут.

Адреса ":8080" — чому з двокрапкою?

Початківці дуже часто пишуть "8080" і отримують помилку. У Go адресу сервера зазвичай задають як host:port. Якщо ви пишете ":8080", це означає: слухай на всіх інтерфейсах на порту 8080. Якщо ви пишете "localhost:8080", ви прив’язуєтеся лише до localhost, що зручно під час локальної розробки.

Що означає handler == nil?

Іноді ви побачите ось так:

package main

import "net/http"

func main() {
	_ = http.ListenAndServe(":8080", nil)
}

nil тут означає не «сервер без обробників». Це означає: «використовуй http.DefaultServeMux». Тобто маршрути потрібно реєструвати через http.Handle/http.HandleFunc — глобально. Це працює, але для навчального проєкту, та й майже для будь-якого охайного проєкту, частіше зручніше тримати свій mux := http.NewServeMux(), щоб не залежати від глобального стану.

Глобальний стан у вебсервері — як спільна дошка на кухні офісу: спершу всім зручно, а потім ніхто не розуміє, хто написав «Йогурт їсти не можна, він мій».

3. http.Server: явна конфігурація

Тепер подивімося на http.Server. Це структура, у якій зберігаються налаштування сервера. І головна практична відмінність така: ви починаєте явно бачити, що саме налаштовано: адресу, обробник та інші параметри.

Найпростіший аналог попереднього прикладу:

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

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

	if err := srv.ListenAndServe(); err != nil {
		log.Println("помилка сервера:", err)
	}
}

Порівняйте з http.ListenAndServe(":8080", mux): за змістом це майже те саме. Різниця в тому, що http.Server дає змогу в одному місці тримати конфігурацію сервера, а не «ховати» її в аргументах функції.

І ще важливий момент: метод називається так само ListenAndServe(), але тепер він викликається на srv. Тобто «у сервера є метод запуску». Це інтуїтивно, а у великих застосунках ще й значно полегшує читання коду.

Навіщо це, якщо можна обійтися одним рядком?

Бо щойно у вас з’являється хоч щось налаштовуване — наприклад, особливий логер, обмеження за часом, особливості TLS, різні сервери на різних портах — структура http.Server перетворюється на зручну «точку складання». Навіть якщо сьогодні ви використовуєте лише Addr і Handler, ви вже пишете код так, щоб завтра не робити рефакторинг «на ходу».

Мінірамка: чому http.Server краще показує архітектуру

Є одна тонка, але важлива причина полюбити http.Server навіть тоді, коли «поки достатньо ListenAndServe».

ListenAndServe(":8080", mux) виглядає як «якась функція, яка десь щось робить». А srv := &http.Server {Addr: ":8080", Handler: mux} виглядає так, ніби ми зібрали об’єкт сервера і далі запускаємо його. Це підсилює архітектурне мислення: залежності стають полями, а не «переданими кудись аргументами».

І це безпосередньо поєднується з ідеєю «сервер крутиться навколо обробника»: ви буквально бачите Handler: mux у структурі.

4. Що де налаштовується

Щоб не перетворити тему на філософію, зафіксуймо практичну домовленість: що зазвичай налаштовують через ListenAndServe, а що зручніше або правильніше налаштовувати через http.Server.

Нижче таблиця — це не «єдино правильний закон», а орієнтир, який допоможе не плутатися.

Що ви налаштовуєте Де це найчастіше задають Чому так зручніше
Адреса/порт (":8080", "localhost:8080") В обох варіантах Це базовий параметр запуску, він у будь-якому разі потрібен
Головний обробник (mux) В обох варіантах Серверу потрібен http.Handler, без нього нікуди передати запит
Маршрути ("/health", "/tasks") У ServeMux (ваш mux) Маршрутизація — завдання mux, а не самого сервера
Додаткові налаштування сервера (таймаути, кастомний ErrorLog тощо) Через http.Server Це саме «політика сервера», а не маршрутизації
Уникнення глобального стану Свій mux + http.Server Явне складання легше тестувати й підтримувати

Ми свідомо не заглиблюємося в те, які саме бувають таймаути і як правильно вимикати сервер: зараз важливо зрозуміти архітектурну межу. http.Server — це контейнер конфігурації сервера, а ListenAndServe — швидкий спосіб створити цей контейнер неявно.

5. Мінікаркас застосунку: buildMux() і запуск

Від цього моменту в прикладах ми поступово збираємо один і той самий каркас: маленький HTTP‑застосунок для задач (умовний «todo»). Поки без CRUD і без JSON-декодування: сьогодні наша мета — правильно запустити сервер і зрозуміти, де що живе.

Добра практика для початківця — та й не лише для початківця — винести складання маршрутів в окрему функцію. Тоді main читається як «зібрали залежності — запустили».

package main

import "net/http"

func buildMux() *http.ServeMux {
	mux := http.NewServeMux()

	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNoContent) // 204
	})

	return mux
}

Тепер main стає дуже коротким і приємним:

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := buildMux()

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

	log.Println("слухаємо на", srv.Addr) // слухаємо на :8080
	if err := srv.ListenAndServe(); err != nil {
		log.Println("помилка сервера:", err)
	}
}

Чому я додав log.Println("слухаємо на", srv.Addr)? Бо це той самий «людський слід» у консолі, який рятує нерви, коли ви забули, який порт задали, або взагалі не впевнені, що програма дійшла до запуску.

6. Помилка запуску сервера — нормальна гілка

Для початківців дуже типова помилка: «Він же має запускатися, навіщо перевіряти помилку?». На жаль, сервер може не стартувати з багатьох причин, і найпоширеніша з них — зайнятий порт. Або у вас немає прав слухати цей порт. Або адресу вказано некоректно.

Тому базова гігієна проста: ListenAndServe повертає error, і його не можна ігнорувати.

У зовсім простих навчальних проєктах інколи пишуть log.Fatal(err). Це нормально, але важливо розуміти зміст: log.Fatal друкує повідомлення і викликає os.Exit(1). Тобто програма завершиться негайно. Для коду всередині бібліотек це погано, але в main невеликого застосунку — терпимо, хоча згодом ви навчитеся робити більш контрольовану обробку помилок. У будь-якому разі думка одна: помилка запуску — це частина нормального керування програмою.

Аналогія: ServeMux — секретар, Server — офіс

Щоб закріпити, тримайте аналогію — обережно, без перетворення на цирк.

http.Server — це офіс: адреса офісу (Addr), правила роботи, інфраструктура.
http.ServeMux — секретар на ресепшені: він дивиться на шлях запиту й вирішує, до якого співробітника (обробника) відправити відвідувача.

Якщо ви намагаєтеся в офісі налаштувати, хто за що відповідає, ви починаєте плутати шари. Тому маршрутизацію ми тримаємо в mux’і, а параметри «як працює офіс» — у http.Server.

7. Типові помилки

Помилка №1: переплутати DefaultServeMux і свій mux.
Це трапляється так: ви десь написали http.HandleFunc("/health", ...) (тобто зареєстрували маршрут у глобальному DefaultServeMux), а запускаєте сервер як http.ListenAndServe(":8080", myMux). У підсумку ваш обробник ніби не працює, бо ви зареєстрували маршрут не там. Ця помилка особливо підступна тим, що компілятор мовчить: усе коректно, просто логіка роз’їхалася.

Помилка №2: написати "8080" замість ":8080".
Бо мозок пам’ятає «порт 8080», а API очікує «адреса:порт». У Go це рядок формату host:port. Якщо host порожній, двокрапка все одно потрібна.

Помилка №3: сховати запуск сервера не в main, а десь усередині.
Початківці інколи запускають сервер усередині функції, яка ще й обробляє запити, або всередині пакета, який узагалі мав би займатися бізнес-логікою. Потім стає неможливо зрозуміти, хто відповідає за життєвий цикл застосунку. Домовленість проста: запуск сервера — це відповідальність main, а обробники — відповідальність HTTP‑шару.

Помилка №4: не логувати, де саме сервер слухає.
Це не «помилка компіляції», це помилка експлуатації. Ви запускаєте програму, бачите порожню консоль і не розумієте: вона зависла, впала чи працює? Один рядок log.Println("listening on", addr) робить поведінку прозорою.

Помилка №5: ігнорувати помилку ListenAndServe.
Якщо порт зайнятий, програма миттєво завершиться. А якщо помилку проігнорувати й далі вдавати, що все добре, можна довго шукати причину збою. Перевірка помилки — це не бюрократія, а спосіб швидко зрозуміти, що саме пішло не так.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ