# Wzorce projektowe w Go: kluczowe wzorce i pytania rekrutacyjne dla programistów Go > Przegląd kluczowych wzorców projektowych w Go: Functional Options, Strategy, Factory, Observer oraz Middleware. Praktyczne przykłady kodu i pytania rekrutacyjne dla programistów Go. - Published: 2026-05-28 - Updated: 2026-05-28 - Author: SharpSkill - Tags: go, design-patterns, best-practices, interview, software-architecture - Reading time: 10 min --- Język Go, znany również jako Golang, wyróżnia się na tle innych języków programowania swoim minimalistycznym podejściem do projektowania oprogramowania. W przeciwieństwie do języków takich jak Java czy C++, Go nie posiada wbudowanego wsparcia dla klasycznych wzorców obiektowych. Zamiast tego programiści Go wypracowali własny zestaw idiomatycznych rozwiązań, które wykorzystują unikalne cechy języka: interfejsy, funkcje jako wartości pierwszoklasowe oraz kompozycję struktur. Znajomość tych wzorców stanowi kluczowy element przygotowania do rozmów kwalifikacyjnych na stanowiska związane z programowaniem w Go. > **Idiomatyczne podejście Go** > > Go preferuje prostotę i czytelność nad złożone hierarchie dziedziczenia. Wzorce projektowe w Go często przyjmują formę funkcji wyższego rzędu lub kompozycji interfejsów, co prowadzi do bardziej elastycznego i łatwiejszego w testowaniu kodu. Podczas rozmów kwalifikacyjnych rekruterzy zwracają szczególną uwagę na umiejętność stosowania tych idiomatycznych rozwiązań zamiast bezpośredniego przenoszenia wzorców z innych języków. ## Wzorzec opcji funkcyjnych (Functional Options) Jednym z najbardziej eleganckich i powszechnie stosowanych wzorców w Go jest wzorzec opcji funkcyjnych. Rozwiązuje on problem konfiguracji struktur z wieloma opcjonalnymi parametrami bez konieczności tworzenia wielu konstruktorów lub przekazywania struktur konfiguracyjnych z licznymi polami. Wzorzec ten wykorzystuje funkcje jako argumenty, gdzie każda funkcja modyfikuje określony aspekt tworzonego obiektu. Takie podejście zapewnia czytelny interfejs API, umożliwia łatwe dodawanie nowych opcji bez łamania kompatybilności wstecznej oraz pozwala na definiowanie sensownych wartości domyślnych. ```go // server.go package server import ( "time" "log/slog" ) // Server holds the HTTP server configuration. type Server struct { host string port int timeout time.Duration maxConns int logger *slog.Logger } // Option defines a functional option for Server. type Option func(*Server) // WithPort sets the server port. func WithPort(port int) Option { return func(s *Server) { s.port = port } } // WithTimeout sets the request timeout. func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } // WithMaxConns sets the maximum concurrent connections. func WithMaxConns(n int) Option { return func(s *Server) { s.maxConns = n } } // New creates a Server with sensible defaults and applies options. func New(host string, opts ...Option) *Server { srv := &Server{ host: host, port: 8080, // default port timeout: 30 * time.Second, // default timeout maxConns: 100, // default max connections logger: slog.Default(), } for _, opt := range opts { opt(srv) } return srv } ``` Powyższy przykład demonstruje tworzenie serwera HTTP z konfigurowalnymi parametrami. Funkcja konstruktora `New` przyjmuje obowiązkowy argument `host` oraz dowolną liczbę opcji. Każda opcja jest funkcją modyfikującą strukturę serwera. Dzięki temu wywołanie może wyglądać następująco: `server.New("localhost", WithPort(3000), WithTimeout(time.Minute))`. Wzorzec ten jest szeroko stosowany w popularnych bibliotekach Go, takich jak gRPC czy zap logger. ## Wzorzec strategii (Strategy Pattern) Wzorzec strategii w Go realizowany jest poprzez interfejsy, które definiują kontrakt dla wymiennych algorytmów. W przeciwieństwie do implementacji w językach obiektowych, Go nie wymaga jawnej deklaracji implementacji interfejsu. Każdy typ posiadający odpowiednie metody automatycznie spełnia wymagania interfejsu. Takie podejście, znane jako duck typing, sprawia że kod jest bardziej elastyczny i łatwiejszy do testowania. Można bez problemu podmienić rzeczywistą implementację na mock podczas testów jednostkowych. ```go // payment.go package payment import "fmt" // Processor defines the strategy interface. type Processor interface { Pay(amount float64) (string, error) } // CreditCard implements Processor for card payments. type CreditCard struct { CardNumber string Expiry string } func (c *CreditCard) Pay(amount float64) (string, error) { // Charge the card via payment gateway return fmt.Sprintf("charged %.2f to card ending %s", amount, c.CardNumber[len(c.CardNumber)-4:]), nil } // BankTransfer implements Processor for wire transfers. type BankTransfer struct { IBAN string } func (b *BankTransfer) Pay(amount float64) (string, error) { return fmt.Sprintf("initiated transfer of %.2f to %s", amount, b.IBAN), nil } // Checkout processes a payment using the given strategy. func Checkout(p Processor, amount float64) error { receipt, err := p.Pay(amount) if err != nil { return fmt.Errorf("payment failed: %w", err) } fmt.Println(receipt) return nil } ``` W przedstawionym przykładzie interfejs `Processor` definiuje metodę `Pay`, którą implementują różne strategie płatności: `CreditCard` oraz `BankTransfer`. Funkcja `Checkout` nie musi wiedzieć, z jakim konkretnym procesorem płatności pracuje. Zależy jedynie od abstrakcji, co jest zgodne z zasadą odwrócenia zależności (Dependency Inversion Principle). Dodanie nowej metody płatności, na przykład portfela kryptowalutowego, wymaga jedynie utworzenia nowej struktury implementującej interfejs `Processor`. ## Funkcje fabrykujące z walidacją W Go nie istnieją konstruktory w rozumieniu języków obiektowych. Zamiast tego stosuje się funkcje fabrykujące, zwykle nazywane `New` lub `NewXxx`, które tworzą i inicjalizują struktury. Kluczową różnicą w porównaniu z prostym tworzeniem struktur jest możliwość przeprowadzenia walidacji parametrów oraz zwrócenia błędu w przypadku nieprawidłowych danych wejściowych. Konwencja zwracania `(*Type, error)` z funkcji fabrykujących jest idiomatyczna dla Go i wymusza na wywołującym obsługę potencjalnych błędów. Takie podejście prowadzi do bardziej niezawodnego kodu produkcyjnego. ```go // connpool.go package pool import ( "errors" "sync" ) // ConnPool manages a pool of reusable connections. type ConnPool struct { mu sync.Mutex conns []Conn maxSize int } // Conn represents a database connection. type Conn struct { ID int Active bool } // NewConnPool validates parameters and returns an initialized pool. func NewConnPool(maxSize int) (*ConnPool, error) { if maxSize <= 0 { return nil, errors.New("pool: maxSize must be positive") } return &ConnPool{ conns: make([]Conn, 0, maxSize), maxSize: maxSize, }, nil } // Acquire returns a connection from the pool. func (p *ConnPool) Acquire() (*Conn, error) { p.mu.Lock() defer p.mu.Unlock() for i := range p.conns { if !p.conns[i].Active { p.conns[i].Active = true return &p.conns[i], nil } } if len(p.conns) >= p.maxSize { return nil, errors.New("pool: no available connections") } c := Conn{ID: len(p.conns) + 1, Active: true} p.conns = append(p.conns, c) return &p.conns[len(p.conns)-1], nil } ``` Przykład puli połączeń ilustruje kilka ważnych koncepcji. Funkcja `NewConnPool` waliduje parametr `maxSize` i zwraca błąd dla wartości nieprawidłowych. Struktura `ConnPool` wykorzystuje `sync.Mutex` do synchronizacji dostępu z wielu gorutyn. Metoda `Acquire` pokazuje typowy wzorzec blokowania i zwalniania muteksu za pomocą `defer`. Rekruterzy często pytają o bezpieczeństwo współbieżne w Go, dlatego zrozumienie tych mechanizmów jest niezbędne. ## Wzorzec obserwatora z kanałami Wzorzec obserwatora (Observer) w Go naturalnie mapuje się na kanały i gorutyny. Zamiast wywoływać metody callback na zarejestrowanych obserwatorach, magistrala zdarzeń (event bus) rozsyła zdarzenia przez kanały. Subskrybenci nasłuchują na swoich kanałach w osobnych gorutynach, co zapewnia asynchroniczne przetwarzanie zdarzeń. Takie podejście jest szczególnie przydatne w systemach rozproszonych, gdzie komponenty muszą reagować na zdarzenia bez ścisłego powiązania. Architektura zdarzeniowa umożliwia łatwe skalowanie i dodawanie nowych obserwatorów bez modyfikacji istniejącego kodu. ```go // eventbus.go package events import "sync" // Event carries a topic and payload. type Event struct { Topic string Payload any } // Bus manages subscriptions and event dispatch. type Bus struct { mu sync.RWMutex subscribers map[string][]chan Event } // NewBus creates an event bus. func NewBus() *Bus { return &Bus{ subscribers: make(map[string][]chan Event), } } // Subscribe returns a channel that receives events for a topic. func (b *Bus) Subscribe(topic string) <-chan Event { ch := make(chan Event, 16) // buffered to avoid blocking publisher b.mu.Lock() b.subscribers[topic] = append(b.subscribers[topic], ch) b.mu.Unlock() return ch } // Publish sends an event to all subscribers of the topic. func (b *Bus) Publish(topic string, payload any) { b.mu.RLock() defer b.mu.RUnlock() for _, ch := range b.subscribers[topic] { select { case ch <- Event{Topic: topic, Payload: payload}: default: // subscriber too slow, drop event } } } ``` Warto zwrócić uwagę na kilka szczegółów implementacyjnych. Kanały są buforowane (pojemność 16), aby uniknąć blokowania publikującego. Metoda `Publish` używa `select` z klauzulą `default`, co oznacza że zdarzenie zostanie odrzucone jeśli bufor kanału jest pełny. Jest to świadoma decyzja projektowa zapobiegająca blokowaniu całego systemu przez wolnych subskrybentów. Stosowany jest `sync.RWMutex` zamiast zwykłego `Mutex`, ponieważ operacja publikacji wymaga jedynie blokady do odczytu. ## Wzorzec middleware w aplikacjach HTTP Wzorzec middleware jest fundamentalny dla budowy aplikacji webowych w Go. Middleware to funkcja przyjmująca `http.Handler` i zwracająca nowy `http.Handler`, który opakowuje oryginał dodatkowymi funkcjonalnościami. Typowe zastosowania obejmują logowanie, uwierzytelnianie, kompresję odpowiedzi czy obsługę CORS. Standardowa biblioteka Go oraz popularne frameworki jak Chi, Gin czy Echo intensywnie wykorzystują ten wzorzec. Znajomość implementacji middleware jest praktycznie wymagana na rozmowach kwalifikacyjnych dotyczących backendowego Go. ```go // middleware.go package middleware import ( "log/slog" "net/http" "time" ) // Logging records request duration and status. func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() wrapped := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(wrapped, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "status", wrapped.status, "duration", time.Since(start), ) }) } // statusWriter captures the HTTP status code. type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } // Chain applies middleware in order: first listed = outermost. func Chain(h http.Handler, mw ...func(http.Handler) http.Handler) http.Handler { for i := len(mw) - 1; i >= 0; i-- { h = mw[i](h) } return h } ``` Middleware `Logging` demonstruje technikę opakowywania `ResponseWriter` w celu przechwycenia kodu statusu HTTP. Bez tego nie byłoby możliwe zalogowanie statusu odpowiedzi, ponieważ standardowy `ResponseWriter` nie udostępnia tej informacji po wywołaniu `WriteHeader`. Funkcja `Chain` umożliwia eleganckie składanie wielu middleware w określonej kolejności. Pierwszy middleware na liście staje się najbardziej zewnętrzną warstwą. ## Osadzanie struktur jako alternatywa dla dziedziczenia Go nie posiada dziedziczenia w tradycyjnym sensie. Zamiast tego oferuje osadzanie (embedding), które pozwala na kompozycję typów. Osadzona struktura udostępnia swoje pola i metody strukturze zewnętrznej, co przypomina dziedziczenie, ale jest bardziej elastyczne i jawne. Osadzanie jest często stosowane do współdzielenia wspólnych pól między strukturami modeli danych. Typowym przykładem są pola audytowe takie jak `CreatedAt` i `UpdatedAt`, które powinny znajdować się w wielu tabelach bazy danych. ```go // models.go package models import "time" // Timestamps provides common audit fields. type Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } // User embeds Timestamps to gain CreatedAt/UpdatedAt. type User struct { Timestamps ID int Email string } // Order also embeds Timestamps. type Order struct { Timestamps ID int UserID int Total float64 } ``` Dzięki osadzeniu `Timestamps` zarówno `User` jak i `Order` posiadają pola `CreatedAt` i `UpdatedAt`. Można odwoływać się do nich bezpośrednio: `user.CreatedAt`, bez konieczności pisania `user.Timestamps.CreatedAt`. Jeśli `Timestamps` miałoby metody, byłyby one również dostępne na strukturach osadzających. Jest to preferowany sposób realizacji ponownego użycia kodu w Go zamiast głębokich hierarchii dziedziczenia. ## Typowe pytania rekrutacyjne dotyczące wzorców w Go Podczas rozmów kwalifikacyjnych na stanowiska związane z Go rekruterzy często zadają pytania sprawdzające zrozumienie idiomatycznych wzorców języka. Poniżej przedstawiono przykładowe pytania wraz z oczekiwanymi odpowiedziami. **Pytanie: Dlaczego Go preferuje kompozycję nad dziedziczenie?** Kompozycja zapewnia większą elastyczność i unika problemów związanych z głębokimi hierarchiami klas. W Go typy nie deklarują jawnie implementowanych interfejsów, co umożliwia definiowanie interfejsów w miejscu ich użycia, a nie w miejscu implementacji. Prowadzi to do luźniejszego powiązania między komponentami. **Pytanie: Jak zrealizować wzorzec Singleton w Go?** Idiomatyczne podejście wykorzystuje pakiet `sync.Once`, który gwarantuje jednokrotne wykonanie funkcji inicjalizującej, niezależnie od liczby gorutyn próbujących ją wywołać. Alternatywnie można użyć funkcji `init()` pakietu, która jest wywoływana automatycznie przy imporcie. **Pytanie: Czym różni się przekazywanie przez wartość od przekazywania przez wskaźnik w kontekście wzorców?** Przekazywanie przez wartość tworzy kopię, co jest bezpieczne współbieżnie, ale może być nieefektywne dla dużych struktur. Przekazywanie przez wskaźnik pozwala na modyfikację oryginału i jest wydajniejsze, ale wymaga uwagi przy dostępie z wielu gorutyn. W wzorcu opcji funkcyjnych stosuje się wskaźniki, ponieważ funkcje opcji muszą modyfikować konfigurowaną strukturę. > **Interfejsy w Go** > > Interfejsy w Go powinny być małe i skoncentrowane na jednym zadaniu. Popularna zasada mówi: im mniejszy interfejs, tym większa jego użyteczność. Interfejs `io.Reader` z jedną metodą `Read` jest przykładem tego podejścia. Podczas rozmowy warto wspomnieć o zasadzie segregacji interfejsów (Interface Segregation Principle) z SOLID. > **Częste błędy kandydatów** > > Rekruterzy zwracają uwagę na próby bezpośredniego przenoszenia wzorców z Javy czy C++ do Go. Tworzenie fabryk abstrakcyjnych, używanie getterów i setterów dla wszystkich pól czy budowanie złożonych hierarchii typów jest uznawane za nieidiomatyczne. Go ceni prostotę i bezpośredniość. Jeśli rozwiązanie wymaga skomplikowanej struktury klas, prawdopodobnie istnieje prostsze podejście wykorzystujące funkcje i interfejsy. ## Podsumowanie Wzorce projektowe w Go różnią się znacząco od klasycznych wzorców Gang of Four znanych z języków obiektowych. Skuteczne programowanie w Go wymaga zrozumienia i stosowania idiomatycznych rozwiązań języka: - **Opcje funkcyjne** zapewniają elegancką konfigurację z wartościami domyślnymi i rozszerzalnością - **Interfejsy** realizują wzorzec strategii bez jawnej deklaracji implementacji - **Funkcje fabrykujące** z walidacją zastępują konstruktory i wymuszają obsługę błędów - **Kanały** naturalnie implementują wzorzec obserwatora z asynchronicznym przetwarzaniem - **Middleware** umożliwia modularną budowę aplikacji HTTP poprzez kompozycję handlerów - **Osadzanie struktur** zastępuje dziedziczenie, promując kompozycję i ponowne użycie kodu Znajomość tych wzorców nie tylko ułatwia pisanie lepszego kodu produkcyjnego, ale stanowi również istotny element przygotowania do rozmów kwalifikacyjnych. Rekruterzy oczekują od kandydatów umiejętności stosowania idiomatycznych rozwiązań Go zamiast mechanicznego przenoszenia wzorców z innych języków. Praktyczna znajomość przedstawionych wzorców pozwala wyróżnić się podczas procesu rekrutacyjnego i budować skalowalne, łatwe w utrzymaniu aplikacje. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/go/go-design-patterns-essential-patterns-interview