# Патерни проєктування в Go: ключові патерни та питання для співбесід Go-розробників > Огляд ключових патернів проєктування в Go: Functional Options, Strategy, Factory, Observer та Middleware. Практичні приклади коду та питання для технічних співбесід Go-розробників. - Published: 2026-05-28 - Updated: 2026-05-28 - Author: SharpSkill - Tags: go, design-patterns, best-practices, interview, software-architecture - Reading time: 10 min --- Мова програмування Go відома своєю простотою та прагматичним підходом до розробки програмного забезпечення. На відміну від об'єктно-орієнтованих мов із розвинутою системою класів та ієрархіями успадкування, Go пропонує власний набір ідіоматичних рішень для типових архітектурних завдань. Патерни проєктування в Go часто виглядають простіше за класичні реалізації, але водночас зберігають усю потужність та гнучкість оригінальних концепцій. Розуміння цих патернів є критично важливим для успішного проходження технічних співбесід та ефективної роботи з production-кодом. > **Ідіоматичний підхід Go** > > Go не копіює класичні патерни з Java чи C++ буквально. Замість цього мова пропонує власні механізми — інтерфейси, функції як значення першого класу, вбудовування структур та горутини — які дозволяють реалізувати ті самі концепції елегантніше та з меншою кількістю коду. ## Патерн Functional Options: гнучка конфігурація без перевантаження конструкторів Патерн Functional Options вирішує проблему створення об'єктів із багатьма опціональними параметрами. У мовах із підтримкою перевантаження методів розробники часто створюють десятки конструкторів із різними комбінаціями параметрів. Go пропонує елегантніший підхід — використання функцій-опцій, які модифікують структуру після створення. Цей патерн широко застосовується у стандартній бібліотеці Go та популярних фреймворках. Його переваги особливо помітні під час роботи з конфігурацією серверів, клієнтів баз даних та інших компонентів інфраструктури. ```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 } ``` Реалізація демонструє ключові переваги патерну: значення за замовчуванням встановлюються в одному місці, нові опції додаються без зміни сигнатури конструктора, а код виклику залишається читабельним. Розробник може передати лише ті параметри, які відрізняються від значень за замовчуванням. ## Патерн Strategy: поліморфізм через інтерфейси Патерн Strategy дозволяє визначити сімейство алгоритмів та робити їх взаємозамінними. У Go цей патерн реалізується природно через інтерфейси — механізм неявної реалізації робить код гнучким та легким для тестування. Типовим прикладом застосування Strategy є система обробки платежів, де різні методи оплати мають однаковий інтерфейс, але різну внутрішню логіку. ```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 } ``` Функція `Checkout` не знає про конкретні реалізації процесорів платежів — вона працює лише з інтерфейсом. Це дозволяє додавати нові методи оплати без модифікації існуючого коду та спрощує написання unit-тестів із mock-об'єктами. ## Factory Functions: безпечне створення об'єктів із валідацією Фабричні функції в Go відрізняються від класичного патерну Factory тим, що вони зазвичай повертають конкретний тип разом із помилкою. Цей підхід забезпечує валідацію параметрів на етапі створення об'єкта та запобігає появі невалідних станів у системі. Пул з'єднань із базою даних є класичним прикладом, де фабрична функція перевіряє вхідні параметри та гарантує коректну ініціалізацію внутрішнього стану. ```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 } ``` Функція `NewConnPool` повертає помилку замість паніки при невалідних параметрах. Це стандартний підхід у Go, який змушує розробника явно обробляти помилкові ситуації та робить код більш надійним. ## Патерн Observer: подієва архітектура з каналами Патерн Observer у Go реалізується через канали — вбудований механізм комунікації між горутинами. Event Bus дозволяє компонентам підписуватися на події та отримувати сповіщення асинхронно, що ідеально підходить для побудови слабкозв'язаних систем. ```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 } } } ``` Реалізація використовує буферизовані канали для запобігання блокуванню видавця. Конструкція `select` із `default` гілкою дозволяє відкидати події, якщо підписник не встигає їх обробляти — це важливий патерн для побудови стійких систем. ## Патерн Middleware: ланцюжок обробників запитів Middleware є фундаментальним патерном для веб-розробки на Go. Кожен middleware приймає обробник та повертає новий обробник, що дозволяє будувати ланцюжки обробки з логуванням, автентифікацією, обмеженням швидкості та іншою cross-cutting функціональністю. ```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 } ``` Функція `Chain` застосовує middleware у зворотному порядку, щоб перший вказаний middleware був зовнішнім у ланцюжку. Структура `statusWriter` демонструє патерн декоратора для перехоплення HTTP-відповідей. ## Struct Embedding: композиція замість успадкування Go не підтримує класичне успадкування, але пропонує потужну альтернативу — вбудовування структур. Цей механізм дозволяє повторно використовувати поля та методи без створення складних ієрархій типів. ```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 } ``` Вбудована структура `Timestamps` надає полям `CreatedAt` та `UpdatedAt` усім моделям, що її вбудовують. Поля вбудованої структури доступні безпосередньо — можна писати `user.CreatedAt` замість `user.Timestamps.CreatedAt`. ## Типові питання на співбесідах щодо патернів Go Технічні співбесіди на позиції Go-розробника часто включають питання про ідіоматичне застосування патернів проєктування. Нижче наведено найбільш поширені питання та підходи до відповідей. **Питання: Чому Go використовує Functional Options замість Builder pattern?** Builder pattern вимагає створення окремого типу-будівельника з методами для кожного параметра. Functional Options досягає того самого результату з меншою кількістю коду та без додаткових типів. Крім того, опції легко комбінувати та перевикористовувати. **Питання: Як забезпечити потокобезпечність у патерні Observer?** Використання `sync.RWMutex` дозволяє безпечно читати список підписників кількома горутинами одночасно (через `RLock`), блокуючи лише операції запису. Буферизовані канали запобігають deadlock при публікації подій. **Питання: Коли варто використовувати інтерфейси з одним методом?** Інтерфейси з одним методом є ідіоматичними для Go. Вони максимально гнучкі та легко реалізуються. Стандартна бібліотека містить безліч прикладів: `io.Reader`, `io.Writer`, `http.Handler`, `sort.Interface`. > **Порада для співбесіди** > > На співбесіді важливо демонструвати розуміння компромісів кожного патерну. Functional Options збільшує кількість функцій у пакеті, Strategy вимагає визначення інтерфейсу, Observer потребує уваги до управління пам'яттю каналів. Здатність обговорювати недоліки показує глибоке розуміння теми. **Питання: Як тестувати код, що використовує Strategy pattern?** Інтерфейси дозволяють легко створювати mock-реалізації для тестів. Достатньо створити структуру, що реалізує потрібний інтерфейс, і передати її у функцію, що тестується. Це усуває залежність від зовнішніх сервісів під час unit-тестування. **Питання: Яка різниця між вбудовуванням та композицією?** Вбудовування робить поля та методи вбудованої структури доступними безпосередньо, немов вони належать зовнішній структурі. Композиція через іменоване поле вимагає явного звернення до цього поля. Вбудовування зручніше для повторного використання коду, композиція — для явного розмежування відповідальності. > **Поширена помилка** > > Вбудовування не є успадкуванням. Вбудована структура не має доступу до полів зовнішньої структури, і переозначення методів працює інакше, ніж у класичному ООП. Плутанина в цих концепціях є частою причиною помилок на співбесідах. ## Підсумок Патерни проєктування в Go відображають філософію мови — простота, читабельність та практичність. Ключові концепції, які варто запам'ятати: - **Functional Options** вирішує проблему опціональних параметрів елегантніше за Builder або телескопічні конструктори - **Strategy** природно реалізується через інтерфейси Go з неявною реалізацією - **Factory Functions** завжди повертають помилку для забезпечення валідації на етапі створення - **Observer** використовує канали як вбудований механізм асинхронної комунікації - **Middleware** є стандартним підходом для cross-cutting concerns у веб-додатках - **Struct Embedding** надає композицію як альтернативу успадкуванню Розуміння цих патернів та їхніх ідіоматичних реалізацій у Go є необхідною компетенцією для успішного проходження технічних співбесід та ефективної роботи з production-кодом. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/go/go-design-patterns-essential-patterns-interview