# ดีไซน์แพตเทิร์นใน Go: แพตเทิร์นสำคัญและคำถามสัมภาษณ์สำหรับนักพัฒนา Go > เชี่ยวชาญดีไซน์แพตเทิร์นของ Go ทั้ง Functional Options, Strategy, Factory และ Observer พร้อมตัวอย่างโค้ดใช้งานจริง แนวปฏิบัติที่ดีแบบ idiomatic และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Go - Published: 2026-05-28 - Updated: 2026-06-06 - Author: SharpSkill - Tags: go, design-patterns, best-practices, interview, software-architecture - Reading time: 10 min --- ดีไซน์แพตเทิร์นใน 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](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) และปัจจุบันเป็นมาตรฐานทั่วทั้งระบบนิเวศของ Go โดยใช้อาร์กิวเมนต์แบบ variadic เพื่อกำหนดค่าให้กับ struct ```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 } ``` ผู้เรียกใช้ระบุเฉพาะสิ่งที่ต่างจากค่าเริ่มต้นเท่านั้น การเพิ่มตัวเลือกใหม่จะไม่ทำให้จุดเรียกใช้เดิมพังเลย แพตเทิร์นนี้ปรากฏในไลบรารีระดับโปรดักชันอย่าง `google.golang.org/grpc` และ `go.uber.org/zap` ## แพตเทิร์น Strategy ผ่านการเติมเต็ม interface แพตเทิร์น Strategy ห่อหุ้มอัลกอริทึมที่สลับเปลี่ยนกันได้ไว้หลัง interface เดียวกัน ใน Go สิ่งนี้แมปตรงไปยังโพลีมอร์ฟิซึมที่อิงกับ interface โดยไม่ต้องใช้คลาสนามธรรมหรือสายโซ่การสืบทอด ระบบประมวลผลการชำระเงินเป็นตัวอย่างที่ดีของแพตเทิร์นนี้ วิธีการชำระเงินแต่ละแบบอิมพลีเมนต์ interface เดียวกัน และบริการ checkout จะเลือก strategy ในขณะ runtime ```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 } ``` 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 มีประโยชน์อยู่แล้วโดยปริยาย](https://go.dev/blog/organizing-go-code) ```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 } ``` ฟังก์ชัน factory `NewConnPool` ตรวจสอบพารามิเตอร์ `maxSize` และจองพื้นที่ slice ไว้ล่วงหน้า การกำหนดค่า struct โดยตรง (`&ConnPool{}`) จะข้ามการตรวจสอบนี้ ซึ่งอาจนำไปสู่ panic ขณะ runtime แพตเทิร์นนี้ขยายต่อได้อย่างเป็นธรรมชาติ โดยนำไปผสมกับ Functional Options สำหรับสถานการณ์การกำหนดค่าที่ซับซ้อนยิ่งขึ้น ## แพตเทิร์น Observer ด้วย channel และ goroutine แพตเทิร์น Observer แจ้งเตือนผู้ติดตามหลายรายเมื่อสถานะเปลี่ยนแปลง ใน Java หรือ C# เรื่องนี้ต้องอาศัย event listener และการลงทะเบียน callback ส่วน Go เสนอเส้นทางที่เป็น idiomatic มากกว่า นั่นคือ channel ผู้ติดตามแต่ละรายรับเหตุการณ์ผ่าน channel ของตนเอง และ goroutine จัดการการส่งมอบแบบ concurrent แนวทางนี้ใช้ประโยชน์จาก [พรีมิทิฟด้าน concurrency](/blog/go/go-concurrency-goroutines-channels) ของ Go โดยตรง ช่วยเลี่ยงความยุ่งเหยิงของ callback ```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` ป้องกันไม่ให้ผู้ติดตามที่ช้าบล็อก bus ทั้งหมด ในระบบระดับโปรดักชัน การเพิ่มกลไกการยกเลิกที่อิงกับ context และเมธอด `Unsubscribe` จะทำให้การอิมพลีเมนต์สมบูรณ์ยิ่งขึ้น ## แพตเทิร์น Middleware สำหรับไปป์ไลน์ของคำขอ HTTP สายโซ่ middleware เป็นกระดูกสันหลังของเซิร์ฟเวอร์ HTTP ใน Go แพตเทิร์นนี้ห่อ `http.Handler` ด้วยพฤติกรรมเพิ่มเติม (การบันทึกล็อก การยืนยันตัวตน การจำกัดอัตรา) โดยไม่แก้ไขตัว handler เอง interface `http.Handler` ของไลบรารีมาตรฐานทำให้การประกอบนี้ง่ายดายอย่างยิ่ง ```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 แต่ละตัวห่อตัวถัดไป จนเกิดเป็นไปป์ไลน์ แพตเทิร์นนี้เหมือนกันใน [router ยอดนิยมอย่าง chi](https://github.com/go-chi/chi) ซึ่งรับ `func(http.Handler) http.Handler` เป็น middleware ## composition แทนการสืบทอดด้วย struct embedding Go ไม่รองรับการสืบทอด แต่ struct embedding จะเลื่อนเมธอดของชนิดข้อมูลภายในขึ้นไปยังชนิดข้อมูลภายนอกแทน ทำให้นำโค้ดกลับมาใช้ซ้ำได้โดยไม่เกิด coupling ที่แน่นหนา นี่ไม่ใช่การสืบทอด ชนิดข้อมูลที่ถูก embed ไม่รับรู้ถึงชนิดข้อมูลที่ embed มันอยู่ และไม่มี virtual dispatch การเข้าใจข้อแตกต่างนี้สำคัญอย่างยิ่งต่อ [คำถามสัมภาษณ์ Go](/technologies/go/interview-questions/concurrency-patterns) ผู้สมัครที่สับสนระหว่าง 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 } ``` ตอนนี้ทั้ง `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 ที่ต่ำลงและทดสอบได้ง่ายขึ้น](/technologies/go/interview-questions/testing) > **รูปแบบในการสัมภาษณ์** > > ผู้สัมภาษณ์มักขอให้ผู้สมัครรีแฟกเตอร์ 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 ## บทสรุป - แพตเทิร์น 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 ตามตำรา --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/go/go-design-patterns-essential-patterns-interview