Biblioteka standardowa Go jest wyjątkowa
W ekosystemie większości języków programowania budowanie aplikacji webowej bez frameworka brzmi jak szaleństwo. Kto pisałby serwer HTTP od zera w Pythonie czy Javie? Ale Go to inny przypadek. Pakiet net/http w bibliotece standardowej Go to w pełni funkcjonalny, wydajny i dobrze zaprojektowany serwer HTTP — nie prototyp, nie zabawka, lecz narzędzie produkcyjne.
Wszystkie nasze serwisy w LinWork — strony firmowe, kalkulatory, systemy proxy — działają na czystym net/http bez żadnego frameworka. To świadomy wybór, który po latach pracy się sprawdza. Oto dlaczego.
Routing w Go 1.22+
Przez lata największą słabością standardowego routera w Go był brak obsługi parametrów ścieżki i metod HTTP. Chcąc obsłużyć /users/{id}, trzeba było ręcznie parsować URL lub sięgać po zewnętrzny router typu gorilla/mux czy chi.
Go 1.22 (luty 2024) zmieniło to fundamentalnie. Nowy http.ServeMux obsługuje wzorce z parametrami i metodami HTTP:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /articles/{slug...}", getArticle)
Parametry odczytujemy przez r.PathValue("id"). Wzorzec {slug...} dopasowuje pozostałą część ścieżki. To wystarczający routing dla zdecydowanej większości aplikacji webowych.
Porównajmy z popularnymi frameworkami. Gin: r.GET("/users/:id", getUser). Chi: r.Get("/users/{id}", getUser). Echo: e.GET("/users/:id", getUser). Różnica w składni jest minimalna, a w zamian nie dodajemy żadnej zewnętrznej zależności.
Middleware bez magii
Middleware to funkcja, która opakowuje handler HTTP, dodając logikę wykonywaną przed lub po właściwym handlerze. W standardowym Go middleware to po prostu funkcja przyjmująca http.Handler i zwracająca http.Handler:
func logging(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 %v", r.Method, r.URL.Path, time.Since(start))
})
}
Łączenie middleware'ów to zwykła kompozycja funkcji: logging(auth(rateLimiter(mux))). Nie potrzebujemy specjalnych mechanizmów rejestracji, łańcuchów ani pipeline'ów — standardowa kompozycja funkcji Go wystarczy.
Typowe middleware'y, które implementujemy w naszych projektach:
- Logowanie — czas odpowiedzi, metoda, ścieżka, kod statusu
- Odzyskiwanie po panic — przechwycenie paniki i zwrócenie HTTP 500 zamiast crashu
- CORS — nagłówki Cross-Origin Resource Sharing
- Rate limiting — ograniczenie liczby żądań z jednego IP
- Kompresja gzip — transparentna kompresja odpowiedzi
Każdy z nich to 15-30 linii kodu. Proste, czytelne, łatwe do zrozumienia i modyfikacji.
Szablony HTML
Pakiet html/template w Go to silnik szablonów z automatycznym escapowaniem HTML, zabezpieczający przed atakami XSS. Obsługuje dziedziczenie layoutów, częściowe szablony (partials), funkcje niestandardowe i warunkowe renderowanie.
Typowa struktura szablonów w naszych projektach:
templates/
layout.html -- bazowy layout z nagłówkiem i stopką
home.html -- strona główna
blog/
list.html -- lista artykułów
article.html -- pojedynczy artykuł
Layout definiuje bloki, które strony dziedziczące wypełniają treścią. Mechanizm template.ParseGlob ładuje wszystkie szablony jednorazowo przy starcie aplikacji. Od Go 1.16 dyrektywa //go:embed pozwala wbudować szablony w plik binarny, eliminując zależność od systemu plików.
Czy html/template jest tak elastyczny jak Jinja2, Blade czy Twig? Nie — celowo. Go preferuje prostotę. Brak złożonej logiki w szablonach wymusza przygotowanie danych w kodzie Go, co prowadzi do czystszej architektury.
Obsługa plików statycznych
Serwowanie CSS, JavaScript, obrazów i innych zasobów statycznych to jedna linijka:
mux.Handle("GET /static/", http.StripPrefix("/static/",
http.FileServer(http.Dir("static"))))
Lub z wbudowanymi zasobami:
//go:embed static
var staticFiles embed.FS
mux.Handle("GET /static/", http.FileServerFS(staticFiles))
Standardowy file server obsługuje nagłówki ETag, Last-Modified, Content-Type, range requests i kompresję. W produkcji zazwyczaj pliki statyczne serwujemy przez Nginx (z cache'owaniem i HTTP/2 push), ale serwer Go jest w pełni wystarczający dla mniejszych projektów.
Obsługa JSON API
Budowanie API REST w czystym Go jest równie proste:
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
user, err := db.FindUser(id)
if err != nil {
http.Error(w, "Not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
Pakiet encoding/json obsługuje serializację i deserializację struktur Go. Tagi json:"name" kontrolują nazwy pól w JSON. Dla bardziej zaawansowanych przypadków dostępny jest json.Decoder z obsługą streamingu.
Dlaczego nie framework
Frameworki Go jak Gin, Echo czy Fiber są dobrymi narzędziami. Ale każda zewnętrzna zależność to kompromis:
- Aktualizacje i bezpieczeństwo — musisz śledzić CVE i aktualizować zależności
- Kompatybilność — nowe wersje mogą łamać API
- Krzywa uczenia się — nowy członek zespołu musi poznać API frameworka oprócz standardowej biblioteki Go
- Rozmiar binarki — każda zależność zwiększa wynikowy plik
Standardowa biblioteka Go jest wersjonowana razem z językiem, testowana przez tysiące kontrybutorów i gwarantuje kompatybilność wsteczną zgodnie z obietnicą Go 1 Compatibility Promise. Kod napisany w Go 1.16 kompiluje się i działa na Go 1.25 bez zmian.
Kiedy framework ma sens
Uczciwie — są sytuacje, gdy framework upraszcza pracę. Walidacja złożonych formularzy, automatyczne bindowanie parametrów, wbudowany websocket, generowanie dokumentacji OpenAPI — to funkcje, które w czystym Go wymagają więcej kodu. Jeśli budujesz duże API z dziesiątkami endpointów i złożoną walidacją, framework może zaoszczędzić czas.
Ale dla stron firmowych, blogów, landing page'ów, prostych API i narzędzi wewnętrznych — stdlib Go jest więcej niż wystarczający. Mniej kodu, mniej zależności, mniej problemów.
Chcesz zobaczyć, jak wygląda strona firmowa zbudowana w czystym Go? Właśnie na niej jesteś. Masz pytania o naszą architekturę? Skontaktuj się z nami.