ดีไซน์แพตเทิร์นใน Go: แพตเทิร์นสำคัญและคำถามสัมภาษณ์สำหรับนักพัฒนา Go

เชี่ยวชาญดีไซน์แพตเทิร์นของ Go ทั้ง Functional Options, Strategy, Factory และ Observer พร้อมตัวอย่างโค้ดใช้งานจริง แนวปฏิบัติที่ดีแบบ idiomatic และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Go

ภาพประกอบดีไซน์แพตเทิร์นของ Go ด้วยรูปทรงเรขาคณิตนามธรรมที่สื่อถึงสถาปัตยกรรมซอฟต์แวร์

ดีไซน์แพตเทิร์นใน Go แตกต่างจากแพตเทิร์นในภาษาเชิงวัตถุอย่างสิ้นเชิง เนื่องจากไม่มีคลาสหรือการสืบทอด Go จึงอาศัย composition, interface และฟังก์ชันแบบ first-class เพื่อให้ได้ความยืดหยุ่นเชิงโครงสร้างแบบเดียวกัน โดยมักมีพิธีรีตองน้อยกว่าและชัดเจนกว่า

หลักการเชิง idiomatic ของ Go

Go ให้ความสำคัญกับ composition มากกว่าการสืบทอด ดีไซน์แพตเทิร์นคลาสสิกทุกตัวต้องถูกปรับให้ทำงานกับ interface, struct embedding และฟังก์ชันในฐานะ first-class citizen แทนที่จะเป็นลำดับชั้นของคลาส

แพตเทิร์น Functional Options สำหรับการกำหนดค่าที่ยืดหยุ่น

แพตเทิร์น Functional Options แก้ปัญหาที่พบได้บ่อยใน Go นั่นคือการสร้างอ็อบเจ็กต์ที่มีพารามิเตอร์ตัวเลือกจำนวนมาก ต่างจากภาษาที่มี method overloading หรือค่าอาร์กิวเมนต์เริ่มต้น Go ไม่มีทั้งสองอย่าง แพตเทิร์น Builder ใช้งานได้แต่ก็ดูเยิ่นเย้อ ส่วน Functional Options มอบ API ที่สะอาดและขยายได้ พร้อมทั้งยังคงความเข้ากันได้ย้อนหลังเมื่อความต้องการเพิ่มขึ้น

แพตเทิร์นนี้ ได้รับความนิยมจาก Dave Cheney และปัจจุบันเป็นมาตรฐานทั่วทั้งระบบนิเวศของ Go โดยใช้อาร์กิวเมนต์แบบ variadic เพื่อกำหนดค่าให้กับ struct

server.gogo
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
}

ผู้เรียกใช้ระบุเฉพาะสิ่งที่ต่างจากค่าเริ่มต้นเท่านั้น การเพิ่มตัวเลือกใหม่จะไม่ทำให้จุดเรียกใช้เดิมพังเลย แพตเทิร์นนี้ปรากฏในไลบรารีระดับโปรดักชันอย่าง google.golang.org/grpc และ go.uber.org/zap

แพตเทิร์น Strategy ผ่านการเติมเต็ม interface

แพตเทิร์น Strategy ห่อหุ้มอัลกอริทึมที่สลับเปลี่ยนกันได้ไว้หลัง interface เดียวกัน ใน Go สิ่งนี้แมปตรงไปยังโพลีมอร์ฟิซึมที่อิงกับ interface โดยไม่ต้องใช้คลาสนามธรรมหรือสายโซ่การสืบทอด

ระบบประมวลผลการชำระเงินเป็นตัวอย่างที่ดีของแพตเทิร์นนี้ วิธีการชำระเงินแต่ละแบบอิมพลีเมนต์ interface เดียวกัน และบริการ checkout จะเลือก strategy ในขณะ runtime

payment.gogo
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 ของ Go ถูกเติมเต็มแบบโดยปริยาย ไม่มีคีย์เวิร์ด implements ชนิดข้อมูลใดก็ตามที่มีเมธอด Pay(float64) (string, error) ถือว่าเข้าข่ายเป็น Processor แนวทางนี้ช่วยให้ coupling ต่ำและทำให้การทดสอบเป็นเรื่องตรงไปตรงมา เพียงส่ง Processor จำลองที่คืนผลลัพธ์ที่คาดเดาได้

ฟังก์ชัน factory และแพตเทิร์นการสร้างอ็อบเจ็กต์ใน Go

Go ไม่มี constructor สิ่งทดแทนเชิง idiomatic คือฟังก์ชัน factory ซึ่งมักตั้งชื่อว่า New หรือ NewXxx ที่คืนค่า struct ที่ถูกกำหนดค่าเรียบร้อยแล้ว ฟังก์ชัน factory บังคับใช้ค่าคงที่เชิงเงื่อนไข (invariant) ที่การกำหนดค่าด้วย zero-value ไม่อาจรับประกันได้

ข้อแตกต่างนี้สำคัญต่อการเตรียมตัวสัมภาษณ์ นักพัฒนา Go ควรแยกออกว่าเมื่อใดที่ฟังก์ชัน factory จำเป็น เทียบกับกรณีที่ zero value มีประโยชน์อยู่แล้วโดยปริยาย

connpool.gogo
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
}

ฟังก์ชัน factory NewConnPool ตรวจสอบพารามิเตอร์ maxSize และจองพื้นที่ slice ไว้ล่วงหน้า การกำหนดค่า struct โดยตรง (&ConnPool{}) จะข้ามการตรวจสอบนี้ ซึ่งอาจนำไปสู่ panic ขณะ runtime แพตเทิร์นนี้ขยายต่อได้อย่างเป็นธรรมชาติ โดยนำไปผสมกับ Functional Options สำหรับสถานการณ์การกำหนดค่าที่ซับซ้อนยิ่งขึ้น

พร้อมที่จะพิชิตการสัมภาษณ์ Go แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

แพตเทิร์น Observer ด้วย channel และ goroutine

แพตเทิร์น Observer แจ้งเตือนผู้ติดตามหลายรายเมื่อสถานะเปลี่ยนแปลง ใน Java หรือ C# เรื่องนี้ต้องอาศัย event listener และการลงทะเบียน callback ส่วน Go เสนอเส้นทางที่เป็น idiomatic มากกว่า นั่นคือ channel ผู้ติดตามแต่ละรายรับเหตุการณ์ผ่าน channel ของตนเอง และ goroutine จัดการการส่งมอบแบบ concurrent

แนวทางนี้ใช้ประโยชน์จาก พรีมิทิฟด้าน concurrency ของ Go โดยตรง ช่วยเลี่ยงความยุ่งเหยิงของ callback

eventbus.gogo
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 ป้องกันไม่ให้ผู้ติดตามที่ช้าบล็อก bus ทั้งหมด ในระบบระดับโปรดักชัน การเพิ่มกลไกการยกเลิกที่อิงกับ context และเมธอด Unsubscribe จะทำให้การอิมพลีเมนต์สมบูรณ์ยิ่งขึ้น

แพตเทิร์น Middleware สำหรับไปป์ไลน์ของคำขอ HTTP

สายโซ่ middleware เป็นกระดูกสันหลังของเซิร์ฟเวอร์ HTTP ใน Go แพตเทิร์นนี้ห่อ http.Handler ด้วยพฤติกรรมเพิ่มเติม (การบันทึกล็อก การยืนยันตัวตน การจำกัดอัตรา) โดยไม่แก้ไขตัว handler เอง interface http.Handler ของไลบรารีมาตรฐานทำให้การประกอบนี้ง่ายดายอย่างยิ่ง

middleware.gogo
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 แต่ละตัวห่อตัวถัดไป จนเกิดเป็นไปป์ไลน์ แพตเทิร์นนี้เหมือนกันใน router ยอดนิยมอย่าง chi ซึ่งรับ func(http.Handler) http.Handler เป็น middleware

composition แทนการสืบทอดด้วย struct embedding

Go ไม่รองรับการสืบทอด แต่ struct embedding จะเลื่อนเมธอดของชนิดข้อมูลภายในขึ้นไปยังชนิดข้อมูลภายนอกแทน ทำให้นำโค้ดกลับมาใช้ซ้ำได้โดยไม่เกิด coupling ที่แน่นหนา นี่ไม่ใช่การสืบทอด ชนิดข้อมูลที่ถูก embed ไม่รับรู้ถึงชนิดข้อมูลที่ embed มันอยู่ และไม่มี virtual dispatch

การเข้าใจข้อแตกต่างนี้สำคัญอย่างยิ่งต่อ คำถามสัมภาษณ์ Go ผู้สมัครที่สับสนระหว่าง embedding กับการสืบทอดจะเผยให้เห็นความเข้าใจที่ตื้นเขินเกี่ยวกับระบบชนิดข้อมูลของ Go

models.gogo
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
}

ตอนนี้ทั้ง User และ Order ต่างเปิดเผยฟิลด์ CreatedAt และ UpdatedAt โดยตรง เมธอดใด ๆ ที่นิยามไว้บน Timestamps ก็จะถูกเลื่อนขึ้นมาด้วยเช่นกัน กฎสำคัญคือ embedding ให้การมอบหมาย (delegation) ไม่ใช่การแทนที่ (substitution) User ไม่ได้เป็น Timestamps แต่มัน มี Timestamps

คำถามสัมภาษณ์เกี่ยวกับดีไซน์แพตเทิร์นของ Go

คำถามเรื่องดีไซน์แพตเทิร์นในการสัมภาษณ์ Go ทดสอบว่าผู้สมัครสามารถปรับแพตเทิร์นคลาสสิกให้เข้ากับระบบชนิดข้อมูลของ Go ได้หรือไม่ คำถามต่อไปนี้ปรากฏบ่อยทั้งในการคัดกรองเชิงเทคนิคและรอบสัมภาษณ์ที่ออฟฟิศ

ถาม: เมื่อใดฟังก์ชัน factory ควรคืนค่า error แทนที่จะ panic?

ฟังก์ชัน factory ควรคืนค่า error ทุกครั้งที่การตรวจสอบขึ้นอยู่กับอินพุตขณะ runtime (การกำหนดค่าที่ผู้ใช้ให้มา ตัวแปรสภาพแวดล้อม ข้อมูลภายนอก) ส่วน panic สงวนไว้สำหรับความผิดพลาดของโปรแกรมเมอร์ นั่นคือสถานการณ์ที่บ่งบอกถึงบั๊ก เช่น การส่งพอยน์เตอร์ nil ในจุดที่สัญญา (contract) ของ API ห้ามไว้ ไลบรารีมาตรฐานก็ยึดตามแนวทางนี้ os.Open คืนค่า error ในขณะที่ regexp.MustCompile จะ panic เพราะคาดหวังรูปแบบที่เป็นค่าคงที่ตั้งแต่ตอนคอมไพล์

ถาม: แพตเทิร์น Functional Options ช่วยให้ API วิวัฒน์ได้ดีขึ้นอย่างไร?

การเพิ่มฟังก์ชัน WithXxx ใหม่เป็นการเปลี่ยนแปลงที่ไม่ทำให้สิ่งเดิมพัง ผู้เรียกใช้ที่มีอยู่ยังคงทำงานได้โดยไม่ต้องแก้ไข ซึ่งต่างจาก struct สำหรับกำหนดค่า ที่การเพิ่มฟิลด์บังคับจะทำให้จุดเรียกใช้ทั้งหมดพัง หรือต่างจากพารามิเตอร์ตามตำแหน่ง ที่การจัดเรียงอาร์กิวเมนต์ใหม่ก่อให้เกิดบั๊ก

ถาม: อะไรทำให้ interface ของ Go ต่างจาก interface ของ Java หรือ C#?

interface ของ Go ถูกเติมเต็มแบบโดยปริยาย ชนิดข้อมูลอิมพลีเมนต์ interface เพียงแค่มีเมธอดที่ต้องการ โดยไม่จำเป็นต้องประกาศ implements สิ่งนี้เปิดทางให้หลักการ "รับ interface คืน struct" คือนิยาม interface แบบแคบ ณ จุดเรียกใช้ ไม่ใช่ ณ จุดอิมพลีเมนต์ ผลลัพธ์ที่ได้คือ coupling ที่ต่ำลงและทดสอบได้ง่ายขึ้น

รูปแบบในการสัมภาษณ์

ผู้สัมภาษณ์มักขอให้ผู้สมัครรีแฟกเตอร์ dependency ที่เป็นรูปธรรมให้กลายเป็น interface บททดสอบคือ ผู้สมัครสามารถระบุชุดเมธอดขั้นต่ำที่จำเป็น และดึงมันออกมาที่ฝั่งผู้บริโภคแทนที่จะเป็นฝั่งผู้ผลิตได้หรือไม่

ถาม: Observer ที่อิงกับ channel ต่างจาก Observer ที่อิงกับ callback อย่างไร?

channel แยก publisher ออกจาก subscriber ทั้งในมิติของเวลาและพื้นที่ publisher ไม่ได้ถือการอ้างอิงไปยังฟังก์ชันของ subscriber แต่ส่งไปยัง channel แทน subscriber สามารถประมวลผลเหตุการณ์ตามจังหวะของตนเองได้ด้วย channel แบบมีบัฟเฟอร์ คำสั่ง select เปิดทางให้จัดการ timeout การยกเลิกผ่าน context และการมัลติเพล็กซ์ระหว่างแหล่งเหตุการณ์หลายแหล่ง ซึ่งทั้งหมดนี้ callback ไม่ได้ให้มาฟรี ๆ

กับดักที่พบบ่อย

การลืมปิด channel ของ subscriber ทำให้เกิด goroutine รั่ว ทุก Subscribe ควรมี Unsubscribe ที่สอดคล้องกัน ซึ่งปิด channel และนำมันออกจาก bus

พร้อมที่จะพิชิตการสัมภาษณ์ Go แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

บทสรุป

  • แพตเทิร์น Functional Options แทนที่ builder และ struct สำหรับกำหนดค่าด้วย API ที่สะอาดและขยายได้ ซึ่งไม่เคยทำให้ผู้เรียกใช้เดิมพัง
  • Strategy ใน Go แมปไปยังโพลีมอร์ฟิซึมที่อิงกับ interface คือนิยาม interface ที่ฝั่งผู้บริโภค ไม่ใช่ฝั่งผู้ให้บริการ
  • ฟังก์ชัน factory บังคับใช้ invariant ที่การกำหนดค่าด้วย zero-value ไม่อาจรับประกัน คืนค่า error สำหรับอินพุตขณะ runtime และ panic เฉพาะกับบั๊กของโปรแกรมเมอร์เท่านั้น
  • Observer ที่อิงกับ channel ใช้ประโยชน์จาก goroutine และ select เพื่อส่งมอบเหตุการณ์แบบ concurrent และแยกส่วน โดยไม่ต้องใช้สายโซ่ callback
  • สายโซ่ middleware ของ HTTP ประกอบกันผ่าน signature func(http.Handler) http.Handler ซึ่งเหมือนกันทั้งในไลบรารีมาตรฐานและ router ของบุคคลที่สาม
  • struct embedding ให้การมอบหมาย ไม่ใช่การสืบทอด การเข้าใจข้อแตกต่างนี้แยกนักพัฒนาที่เชี่ยวชาญ Go ออกจากผู้ที่แปลแพตเทิร์นของ Java มาตรง ๆ
  • ความสำเร็จในการสัมภาษณ์ต้องอาศัยการแสดงให้เห็นถึงการปรับแพตเทิร์นให้เข้ากับระบบชนิดข้อมูลของ Go อย่างเป็น idiomatic ไม่ใช่การท่องจำคำนิยามของ Gang of Four ตามตำรา

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#go
#design-patterns
#best-practices
#interview
#software-architecture

แชร์

บทความที่เกี่ยวข้อง

Go 1.26 Green Tea GC, go fix และการเพิ่มประสิทธิภาพ Stack

Go 1.26 สัมภาษณ์งาน: Green Tea GC, go fix และการเพิ่มประสิทธิภาพ Stack สำหรับนักพัฒนา

เตรียมตัวสัมภาษณ์งาน Go 1.26 ครอบคลุม Green Tea garbage collector ลด overhead 10-40%, เครื่องมือ go fix พร้อม modernizers, การจัดสรร slice บน stack, ตรวจจับ goroutine leak และระบบรักษาความปลอดภัย post-quantum พร้อมตัวอย่างโค้ดและคำตอบที่คาดหวัง

คำถามสัมภาษณ์ Go - คู่มือเตรียมตัวฉบับสมบูรณ์

25 คำถามสัมภาษณ์ Go ยอดนิยม: คู่มือฉบับสมบูรณ์สำหรับนักพัฒนา

พิชิตการสัมภาษณ์ Go ด้วย 25 คำถามที่ถูกถามบ่อย goroutine, channel, interface และรูปแบบการทำงานพร้อมกันพร้อมตัวอย่างโค้ด

เตรียมสัมภาษณ์เทคนิค Go ครอบคลุม goroutine channel และ concurrency pattern

สัมภาษณ์เทคนิค Go: Goroutine, Channel และ Concurrency

คำถามสัมภาษณ์เทคนิค Go เกี่ยวกับ goroutine, channel และ concurrency pattern ต่างๆ ตัวอย่างโค้ด ข้อผิดพลาดที่พบบ่อย และคำตอบระดับผู้เชี่ยวชาญสำหรับเตรียมสัมภาษณ์เทคนิค Go ปี 2026