Design Pattern trong Go: Các pattern thiết yếu và câu hỏi phỏng vấn cho lập trình viên Go
Nắm vững các design pattern Go: Functional Options, Strategy, Factory và Observer. Ví dụ mã thực tế, thực hành tốt nhất theo phong cách idiomatic và các câu hỏi phỏng vấn thường gặp cho lập trình viên Go.

Các design pattern trong Go khác biệt về cơ bản so với những phiên bản tương ứng trong các ngôn ngữ hướng đối tượng. Không có class hay kế thừa, Go dựa vào composition, interface và hàm hạng nhất để đạt được sự linh hoạt về cấu trúc tương đương, thường với ít rườm rà hơn và rõ ràng hơn.
Go ưu tiên composition hơn kế thừa. Mỗi design pattern cổ điển đều phải được điều chỉnh để hoạt động với interface, struct embedding và hàm hạng nhất, thay vì với các hệ phân cấp class.
Pattern Functional Options cho cấu hình linh hoạt
Pattern Functional Options giải quyết một vấn đề phổ biến trong Go: khởi tạo đối tượng với nhiều tham số tùy chọn. Khác với các ngôn ngữ có method overloading hay tham số mặc định, Go không hỗ trợ cả hai. Pattern Builder có thể dùng được nhưng lại dài dòng. Functional Options cung cấp một API gọn gàng, dễ mở rộng và vẫn tương thích ngược khi yêu cầu phát triển thêm.
Pattern này, được Dave Cheney phổ biến và nay đã trở thành tiêu chuẩn trong toàn bộ hệ sinh thái Go, dùng tham số variadic để cấu hình một struct.
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
}Bên gọi chỉ cần chỉ định những gì khác với giá trị mặc định. Việc thêm một tùy chọn mới không bao giờ phá vỡ các điểm gọi đã có. Pattern này xuất hiện trong các thư viện production như google.golang.org/grpc và go.uber.org/zap.
Pattern Strategy thông qua việc thỏa mãn interface
Pattern Strategy đóng gói các thuật toán có thể thay thế cho nhau sau một interface chung. Trong Go, điều này ánh xạ trực tiếp tới tính đa hình dựa trên interface, không cần class trừu tượng hay chuỗi kế thừa.
Một hệ thống xử lý thanh toán minh họa cho pattern này. Mỗi phương thức thanh toán triển khai cùng một interface, và dịch vụ checkout chọn strategy tại thời điểm runtime.
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
}Interface trong Go được thỏa mãn một cách ngầm định, không cần từ khóa implements. Bất kỳ kiểu nào có phương thức Pay(float64) (string, error) đều đủ điều kiện là một Processor. Điều này giữ cho coupling ở mức thấp và việc kiểm thử trở nên đơn giản: chỉ cần truyền vào một Processor giả lập trả về kết quả có thể dự đoán.
Hàm factory và pattern khởi tạo trong Go
Go không có constructor. Sự thay thế idiomatic là một hàm factory, thường được đặt tên New hoặc NewXxx, trả về một struct đã được khởi tạo. Các hàm factory bắt buộc tuân thủ những bất biến mà việc khởi tạo bằng zero-value không thể bảo đảm.
Sự khác biệt này quan trọng đối với việc chuẩn bị phỏng vấn: lập trình viên Go cần nhận ra khi nào một hàm factory là cần thiết so với khi nào zero value đã hữu ích theo mặc định.
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
}Hàm factory NewConnPool kiểm tra tham số maxSize và cấp phát trước slice. Việc khởi tạo struct trực tiếp (&ConnPool{}) sẽ bỏ qua bước kiểm tra này, có khả năng dẫn tới panic lúc runtime. Pattern này mở rộng một cách tự nhiên: kết hợp nó với Functional Options cho các kịch bản cấu hình phức tạp hơn.
Sẵn sàng chinh phục phỏng vấn Go?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Pattern Observer với channel và goroutine
Pattern Observer thông báo cho nhiều subscriber khi trạng thái thay đổi. Trong Java hay C#, điều này liên quan tới event listener và việc đăng ký callback. Go đưa ra một hướng đi idiomatic hơn: channel. Mỗi subscriber nhận sự kiện qua channel riêng của nó, và goroutine xử lý việc phân phối một cách đồng thời.
Cách tiếp cận này tận dụng trực tiếp các nguyên thủy concurrency của Go, tránh được mớ callback rối rắm.
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
}
}
}Câu lệnh select với nhánh default ngăn một subscriber chậm chạp làm nghẽn toàn bộ bus. Trong các hệ thống production, việc bổ sung cơ chế hủy dựa trên context và một phương thức Unsubscribe sẽ hoàn thiện phần triển khai.
Pattern Middleware cho pipeline xử lý request HTTP
Các chuỗi middleware là xương sống của máy chủ HTTP trong Go. Pattern này bao một http.Handler bằng hành vi bổ sung (ghi log, xác thực, giới hạn tốc độ) mà không sửa đổi chính handler đó. Interface http.Handler của thư viện chuẩn khiến việc kết hợp này trở nên cực kỳ dễ dàng.
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
}Hàm Chain áp dụng middleware theo thứ tự khai báo. Mỗi middleware bao lấy middleware kế tiếp, tạo thành một pipeline. Pattern này giống hệt trong các router phổ biến như chi, vốn chấp nhận func(http.Handler) http.Handler làm middleware.
Composition thay cho kế thừa với struct embedding
Go không hỗ trợ kế thừa. Thay vào đó, struct embedding nâng các phương thức của kiểu bên trong lên kiểu bên ngoài, đạt được sự tái sử dụng mã mà không cần coupling chặt. Đây không phải là kế thừa: kiểu được embed không hề biết tới kiểu chứa nó, và không có virtual dispatch.
Hiểu rõ sự khác biệt này là điều then chốt đối với các câu hỏi phỏng vấn Go. Ứng viên nhầm lẫn embedding với kế thừa sẽ bộc lộ sự hiểu biết hời hợt về hệ thống kiểu của 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
}Giờ đây cả User và Order đều phơi bày trực tiếp các trường CreatedAt và UpdatedAt. Mọi phương thức được định nghĩa trên Timestamps cũng được nâng lên. Quy tắc cốt lõi: embedding cung cấp sự ủy quyền, không phải sự thay thế. Một User không phải là Timestamps; nó có một Timestamps.
Câu hỏi phỏng vấn về design pattern trong Go
Các câu hỏi về design pattern trong phỏng vấn Go kiểm tra xem ứng viên có thể điều chỉnh các pattern cổ điển cho phù hợp với hệ thống kiểu của Go hay không. Những câu hỏi sau đây xuất hiện thường xuyên trong các vòng sàng lọc kỹ thuật và vòng phỏng vấn trực tiếp.
H: Khi nào một hàm factory nên trả về error thay vì panic?
Một hàm factory nên trả về error mỗi khi việc kiểm tra phụ thuộc vào dữ liệu đầu vào lúc runtime (cấu hình do người dùng cung cấp, biến môi trường, dữ liệu bên ngoài). Panic được dành cho lỗi của lập trình viên, tức những tình huống cho thấy có bug, chẳng hạn truyền một con trỏ nil ở nơi mà hợp đồng API cấm. Thư viện chuẩn tuân theo quy ước này: os.Open trả về error, trong khi regexp.MustCompile panic vì nó kỳ vọng một mẫu hằng số tại thời điểm biên dịch.
H: Pattern Functional Options cải thiện sự tiến hóa của API như thế nào?
Việc thêm một hàm WithXxx mới là một thay đổi không phá vỡ. Các bên gọi hiện có vẫn tiếp tục hoạt động mà không cần sửa đổi. Điều này trái ngược với struct cấu hình, nơi thêm một trường bắt buộc sẽ phá vỡ mọi điểm gọi, hoặc với tham số theo vị trí, nơi việc sắp xếp lại các đối số gây ra bug.
H: Điều gì làm cho interface của Go khác với interface của Java hay C#?
Interface của Go được thỏa mãn một cách ngầm định. Một kiểu triển khai một interface chỉ đơn giản bằng cách có các phương thức cần thiết, không cần khai báo implements. Điều này hiện thực hóa nguyên tắc "nhận interface, trả về struct": định nghĩa interface hẹp tại điểm gọi, chứ không phải tại điểm triển khai. Kết quả là coupling thấp hơn và khả năng kiểm thử cao hơn.
Người phỏng vấn thường yêu cầu ứng viên tái cấu trúc một dependency cụ thể thành một interface. Bài kiểm tra: ứng viên có thể xác định tập phương thức tối thiểu cần thiết và trích xuất nó ở phía người tiêu dùng thay vì phía nhà sản xuất hay không?
H: Observer dựa trên channel khác với Observer dựa trên callback như thế nào?
Channel tách rời publisher và subscriber cả về thời gian lẫn không gian. Publisher không giữ tham chiếu tới các hàm subscriber; nó gửi tới channel. Subscriber có thể xử lý sự kiện theo nhịp độ riêng nhờ channel có buffer. Câu lệnh select cho phép xử lý timeout, hủy thông qua context và ghép kênh giữa nhiều nguồn sự kiện, những điều mà callback không có sẵn miễn phí.
Quên đóng channel của subscriber gây rò rỉ goroutine. Mỗi Subscribe phải có một Unsubscribe tương ứng để đóng channel và gỡ nó khỏi bus.
Sẵn sàng chinh phục phỏng vấn Go?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Kết luận
- Pattern Functional Options thay thế builder và struct cấu hình bằng một API gọn gàng, dễ mở rộng và không bao giờ phá vỡ các bên gọi hiện có
- Strategy trong Go ánh xạ tới tính đa hình dựa trên interface: định nghĩa interface ở phía người tiêu dùng, không phải phía nhà cung cấp
- Hàm factory bắt buộc tuân thủ những bất biến mà khởi tạo bằng zero-value không thể bảo đảm; trả về error cho dữ liệu đầu vào runtime, chỉ panic đối với bug của lập trình viên
- Observer dựa trên channel tận dụng goroutine và
selectđể phân phối sự kiện đồng thời, tách rời mà không cần chuỗi callback - Các chuỗi middleware HTTP được kết hợp qua signature
func(http.Handler) http.Handler, giống hệt nhau giữa thư viện chuẩn và các router bên thứ ba - Struct embedding cung cấp sự ủy quyền, không phải kế thừa: hiểu rõ sự khác biệt này phân tách các lập trình viên thông thạo Go khỏi những người dịch nguyên xi pattern Java
- Thành công khi phỏng vấn đòi hỏi chứng minh khả năng điều chỉnh pattern theo cách idiomatic cho hệ thống kiểu của Go, chứ không phải đọc thuộc lòng các định nghĩa Gang of Four theo kiểu sách giáo khoa
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Go 1.26 Phỏng Vấn Kỹ Thuật: Green Tea GC, go fix và Tối Ưu Hóa Stack
Chuẩn bị phỏng vấn Go 1.26: Green Tea garbage collector giảm 10-40% overhead GC, go fix với modernizers, tối ưu slice trên stack, phát hiện rò rỉ goroutine và mật mã hậu lượng tử. Kèm ví dụ code và câu trả lời mẫu.

Top 25 câu hỏi phỏng vấn Go: hướng dẫn dành cho nhà phát triển
Chinh phục buổi phỏng vấn Go với 25 câu hỏi được hỏi nhiều nhất. Goroutine, channel, interface và mẫu đồng thời kèm ví dụ mã.

Phỏng vấn kỹ thuật Go: Goroutine, Channel và Concurrency
Các câu hỏi phỏng vấn kỹ thuật Go về goroutine, channel và các pattern concurrency. Ví dụ code, bẫy thường gặp và câu trả lời chuyên gia để chuẩn bị phỏng vấn kỹ thuật Go năm 2026.