โหมดมืด
บทที่ 6 — Interfaces
หลังจบบท คุณจะ:
- เข้าใจว่า interface คือ "สัญญา" — ถ้า type ของเราทำงานตามที่สัญญาระบุ (มี method ครบ) ก็ใช้แทน interface ได้ทันที โดยไม่ต้องประกาศว่า "implement" เรียกสิ่งนี้ว่า implicit implementation (เข้าข่ายโดยปริยาย)
- เขียน interface แบบ "เล็กและประกอบรวมกันได้" (small + composable)
- ใช้ type assertion (ดึง type จริงออกมา) + type switch (แตกเคสตาม type)
- รู้จัก empty interface (interface ว่าง รับค่าอะไรก็ได้) +
any
ก่อนอ่านบทนี้ ควรเข้าใจ: บท 3 — Functions, บท 5 — Structs + Methods (โดยเฉพาะเรื่อง method + pointer receiver)
🗺️ บทนี้ยาว — อ่านยังไงไม่ให้ท้อ
Interface เป็นหัวใจของ Go แต่บทนี้ไต่ไปถึงระดับ advanced แบ่งเป็น 2 โซน:
โซน หัวข้อ หมายเหตุ แกนหลัก (ต้องเข้าใจ) ข้อ 1–8 — interface คืออะไร, implicit, small interface, stdlib (Reader/Stringer/error), empty interface/ any, type assertion/switchจบโซนนี้ = ใช้ interface เขียนงานจริงได้ เจาะลึก (อ่านรอบสอง) ข้อ 9–21 — interface pollution, nil gotcha, embedding, design patterns, middleware, generics, reflect, iterator กลับมาอ่านเมื่อเขียน Go คล่องแล้ว — มือใหม่ ข้ามไป Checkpoint ได้ 👉 เพิ่งเริ่ม Go: อ่านถึง ข้อ 8 ให้เข้าใจจริง แล้วลองทำ Checkpoint — ของขั้นสูง (pattern, generics, reflect) ไว้ทีหลังไม่ผิด
1. Interface คืออะไร
Interface คือ "สัญญาเรื่องพฤติกรรม" — มันบอกว่า "type ใดก็ตามที่มี method เหล่านี้ ถือว่าใช้แทนกันได้" จุดพิเศษของ Go คือ implement แบบ implicit (โดยปริยาย) ไม่ต้องเขียน implements เหมือน Java — แค่ type มี method ครบตามที่ interface ต้องการ ก็ถือว่า "เข้าข่าย" อัตโนมัติ
"เข้าข่าย" แปลว่า type นั้นมี method ที่ interface ต้องการครบทุกตัว — เหมือนเช็คลิสต์ผ่านครบก็ถือว่าใช้แทนกันได้ ไม่ต้องประกาศความสัมพันธ์อะไรเพิ่ม:
text
Interface = "สัญญาเรื่องพฤติกรรม" (behavior contract)
= "ถ้า type มี method นี้ → type นั้น implement interface นี้"
Go = "implicit" implementation (ไม่ต้อง implement keyword)go
// ตัวอย่างนี้ต้อง import "fmt" ด้วย (ตัดออกเพื่อความกระชับ)
// Define interface
type Greeter interface {
Greet() string
}
// Type ที่มี Greet() method = automatic implement
type English struct{}
func (e English) Greet() string { return "Hello!" }
type Thai struct{}
func (t Thai) Greet() string { return "สวัสดี!" }
// Use
func sayHi(g Greeter) {
fmt.Println(g.Greet())
}
sayHi(English{}) // Hello!
sayHi(Thai{}) // สวัสดี!→ ไม่มี class English implements Greeter แบบ Java
→ ถ้ามี method ตรง — auto-satisfy
(ต่างจาก C#: C# interface = nominal typing — ต้องประกาศ class English : IGreeter ตอนสร้าง class ให้ชัดเจน ส่วน Go interface = structural/implicit — มี method ครบก็ implement อัตโนมัติ ไม่ต้องประกาศความสัมพันธ์ใด ๆ เลย นี่คือ false-friend ที่สำคัญที่สุดของบทนี้สำหรับคนมาจาก .NET)
2. ทำไม Implicit ดีกว่า
text
Java/C# (explicit):
class English implements Greeter {
public String greet() { ... }
}
→ Type ผูกกับ interface ตอนเขียน
→ ถ้าอยากให้ implement interface ใหม่ → ต้องแก้ class
Go (implicit):
type English struct{}
func (e English) Greet() string { ... }
→ Type ไม่รู้ด้วยซ้ำว่ามี interface "Greeter"
→ Author ของ Greeter ไม่ต้อง coordinate กับ author ของ Englishข้อดี:
- ✅ Decoupling (การลดความผูกพัน) — แต่ละ package ไม่ต้องรู้จักกัน ทำให้แก้ไข/เปลี่ยน implementation ได้ง่าย
- consumer (ฝั่งที่ใช้งาน) กำหนดสิ่งที่ตัวเองต้องการ
- producer (ฝั่งที่สร้าง) ไม่ต้องรู้ด้วยซ้ำว่า interface นี้มีอยู่
- ✅ Define interface "on consumer side" — ที่ใช้, ไม่ใช่ที่ producer
- ✅ Test ง่าย — mock (ตัวปลอมสำหรับเทสต์) เป็นแค่ struct ที่มี method ตรงตามที่ interface กำหนด
3. Interface Idiom — Small + Single
ปรัชญาของ Go คือ interface ควร "เล็ก" (1-3 method) แล้วค่อยประกอบรวมกันเมื่อจำเป็น ต่างจาก Java ที่มักทำ interface ใหญ่รวมทุกอย่าง interface เล็กมีข้อดีสามอย่าง — ยืดหยุ่นกว่า ทดสอบง่ายกว่า และนำกลับไปใช้ในที่อื่นได้หลากหลายกว่า:
go
// ✅ Small interface (Go style)
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Compose
type ReadCloser interface {
Reader
Closer
}
type ReadWriter interface {
Reader
Writer
}"The bigger the interface, the weaker the abstraction." — Rob Pike (ยิ่ง interface ใหญ่ ยิ่งพลังในการ abstract ลดลง — interface เล็กยืดหยุ่นและทรงพลังกว่า)แปลให้ชัดขึ้น: interface ที่มี method น้อยยิ่งยืดหยุ่นกว่า เพราะ type หลายชนิดทำตามได้ง่าย — interface ที่มี 20 method หาตัวที่ทำครบได้ยาก
→ Go favor small interface — 1-3 method
text
Java mindset:
interface UserService {
create(); read(); update(); delete(); search();
bulkCreate(); bulkUpdate(); ...
}
Go mindset:
type UserReader interface { Read(id) User }
type UserWriter interface { Write(u User) error }
type UserDeleter interface { Delete(id) error }
// Compose if need:
type UserStore interface {
UserReader
UserWriter
UserDeleter
}4. Standard Library Examples
วิธีเรียนรู้ interface ที่ดีที่สุดคือดูตัวจริงใน standard library ของ Go เอง — io.Reader, io.Writer, error, sort.Interface เป็นตัวอย่างของ interface เล็กที่ทรงพลัง มี type นับร้อยทั่ว ecosystem (ระบบนิเวศ = library/เครื่องมือรอบ ๆ ภาษา) ที่ implement มัน ทำให้เขียนโค้ดที่ใช้ร่วมกันได้:
io.Reader — anywhere with byte stream
go
type Reader interface {
Read(p []byte) (n int, err error)
}
// Many types implement:
- *os.File // ไฟล์บนดิสก์
- *bytes.Buffer // บัฟเฟอร์ในหน่วยความจำ
- *strings.Reader // อ่าน string เป็น stream
- *http.Request.Body // body ของ HTTP request
- *gzip.Reader // อ่านสตรีมที่บีบอัด gzip
- ... 100+ implementations
// Function accepts ANY:
func process(r io.Reader) error {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
// io.EOF (End Of File = สิ้นสุดไฟล์/สตรีม — ไม่ใช่ error จริง
// แต่เป็นสัญญาณว่าอ่านครบแล้ว)
// เทียบตรง ๆ ด้วย err == io.EOF ก็พอสำหรับตัวอย่างนี้ (stdlib ไม่ wrap EOF)
// (ในโค้ดจริงที่ซับซ้อนกว่านี้ มักใช้ errors.Is แทน — จะเรียนเต็ม ๆ ในบท 07)
if err == io.EOF { break }
if err != nil { return err }
_ = n
// ... process buf[:n]
}
return nil
}
// Use with anything
process(file)
process(strings.NewReader("hello"))
process(resp.Body)fmt.Stringer
go
type Stringer interface {
String() string
}
// fmt.Println(x) checks if x implements Stringer
type Color int
const (
Red Color = iota
Green
Blue
)
func (c Color) String() string {
switch c {
case Red: return "red"
case Green: return "green"
case Blue: return "blue"
default: return fmt.Sprintf("Color(%d)", int(c)) // กัน panic ถ้าค่าเกินช่วง
}
}
fmt.Println(Color(0)) // "red" not "0"⚠️ ระวัง infinite recursion: ในบรรทัด default ต้อง cast เป็น int(c) ก่อนแล้วค่อย fmt.Sprintf("Color(%d)", int(c)) — ถ้าใช้ fmt.Sprintf("%v", c) แทน (ส่ง c ตรง ๆ) จะเกิด infinite recursion เพราะ %v เรียก String() ของ c ซ้ำ วนไม่จบ
error
go
type error interface {
Error() string
}
type MyError struct {
Code int
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Msg)
}
func doSomething() error {
return &MyError{Code: 404, Msg: "not found"}
}5. Empty Interface — any (เดิมคือ interface{})
interface ที่ไม่มี method เลย ใน Go 1.18+ เขียนเป็น any (รูปเดิม interface{} ยังใช้ได้ — เป็นชื่อเทียบเท่าเชิงประวัติศาสตร์) ถือว่าทุก type "เข้าข่าย" — เก็บค่าอะไรก็ได้ (ถ้าเคยเขียนภาษาอื่นมาก่อน: คล้าย Object ของ Java หรือค่าใด ๆ ใน Python — ถ้าไม่รู้จักภาษาอื่นมาก่อน ข้ามส่วนเปรียบเทียบนี้ได้ แค่รู้ว่า any = เก็บอะไรก็ได้)
แต่ระวัง: การใช้ any ทำให้เสีย type safety (ตรวจชนิดตอน compile ไม่ได้) จึงควรใช้เท่าที่จำเป็น ถ้าทำได้ให้ใช้ generics หรือ type เฉพาะแทน:
go
// any (Go 1.18+) = alias ของ interface{} — รับค่าได้ทุกชนิด
var x1 any = 42
var x2 any = "hello"
var x3 any = []int{1, 2, 3}
// รูปเดิม (เทียบเท่ากันหมด):
var y1 interface{} = 42
var y2 interface{} = "hello"fmt.Println signature
go
// Go 1.18+ — signature ปัจจุบัน
func Println(a ...any) (n int, err error)
// (ก่อน Go 1.18 เขียนเป็น ...interface{} — ทั้งสองเหมือนกันทุกประการ any = alias ของ interface{})→ รับ argument อะไรก็ได้
Container
go
// Heterogeneous slice
mixed := []any{1, "two", 3.0, true}
for _, v := range mixed {
fmt.Println(v)
}⚠️ Don't Overuse
go
// ❌ Lose type safety
func process(data any) {
// need type assertion to use
}
// ✅ Use generics (Go 1.18+) or specific type
func process[T any](data T) {} // implementation omitted
func process2(data User) {} // implementation omitted6. Type Assertion
เมื่อมีค่าเก็บใน any แล้วอยากดึง type จริงออกมาใช้ ต้อง "type assertion" ด้วยรูป i.(string) — แต่ถ้า type ไม่ตรงจะ panic ดังนั้นควรใช้รูปปลอดภัย s, ok := i.(string) ที่คืน ok บอกว่าสำเร็จไหมแทนการ crash:
go
var i any = "hello"
// Assert i is string
s := i.(string) // "hello"
fmt.Println(len(s))
// Panic if wrong type
n := i.(int) // 💥 panic: interface conversion: ...
// ⚠️ ถ้ารันบรรทัดนี้จริง โปรแกรมจะ panic และหยุดทำงานทันที (บรรทัดถัดไปจะไม่ทำงานเลย)
// เขียนแบบนี้ต่อกันไว้แค่โชว์ตัวอย่าง ในโค้ดจริงต้องแยกไฟล์/ฟังก์ชันคนละที่
// Safe version with ok
s, ok := i.(string) // "hello", true
n, ok := i.(int) // 0, false (no panic)
if ok {
fmt.Println(s)
}7. Type Switch
go
func describe(i any) {
switch v := i.(type) {
case int:
fmt.Printf("int: %d\n", v)
case string:
fmt.Printf("string: %s (len %d)\n", v, len(v))
case bool:
fmt.Printf("bool: %t\n", v)
case []int:
fmt.Printf("[]int: %v\n", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("unknown: %T = %v\n", v, v)
}
}
describe(42) // int: 42
describe("hi") // string: hi (len 2)
describe(true) // bool: true
describe(3.14) // unknown: float64 = 3.14ใช้ type switch เป็น "last resort" — ส่วนใหญ่ refactor ให้ใช้ interface ดีกว่า
8. Defining Behavior Through Interface
นี่คือพลังที่แท้จริงของ interface — ทำให้โค้ด "ไม่ผูกติด" กับ implementation ตัวใดตัวหนึ่ง ตัวอย่างเปรียบเทียบให้เห็นชัด: ถ้า service พึ่ง struct ตรง ๆ จะเปลี่ยน/เทสต์ยาก แต่ถ้าพึ่ง interface เราสลับ implementation (ไฟล์ → S3 → mock) ได้แค่เปลี่ยน argument:
Without Interface — Coupled
go
type FileStorage struct{}
func (f FileStorage) Save(data []byte) error { return nil } // implementation omitted
type UserService struct {
storage FileStorage
}
// ต้อง import "encoding/json" ด้วย
func (s UserService) CreateUser(u User) error {
data, _ := json.Marshal(u) // ในงานจริงต้องเช็ค err เสมอ
return s.storage.Save(data)
}text
ปัญหา:
- ต้อง use FileStorage เสมอ
- Test ลำบาก — file I/O จริง
- เปลี่ยน S3 → rewrite UserServiceWith Interface — Decoupled
go
// Define interface
type Storage interface {
Save(data []byte) error
}
// Multiple implementations
type FileStorage struct{}
func (f FileStorage) Save(data []byte) error { return nil } // implementation omitted
type S3Storage struct{}
func (s S3Storage) Save(data []byte) error { return nil } // implementation omitted
type MockStorage struct {
saved [][]byte
}
func (m *MockStorage) Save(data []byte) error {
m.saved = append(m.saved, data)
return nil
}
// Service depends on interface
type UserService struct {
storage Storage
}
func NewUserService(s Storage) *UserService {
return &UserService{storage: s}
}
// Usage
production := NewUserService(FileStorage{})
testing := NewUserService(&MockStorage{})→ Swap implementation = swap constructor argument
โซนเจาะลึก — ข้ามได้สำหรับมือใหม่
ตั้งแต่ข้อ 9 เป็นต้นไปเป็นเรื่อง design/pattern/internal ที่จะ "อิน" ก็ต่อเมื่อเขียน Go มาสักพัก — ถ้าเพิ่งเริ่ม ข้ามไป Checkpoint (ข้อ 21) ได้เลย แล้วกลับมาอ่านโซนนี้รอบสอง
9. Interface Pollution — Anti-pattern
เจาะลึก — ข้ามได้ถ้าเพิ่งเริ่ม
ระวังอีกด้านหนึ่ง — การสร้าง interface พร่ำเพรื่อก็เป็นปัญหา ถ้ามี implementation เดียวก็ไม่จำเป็นต้องมี interface เพราะเป็นแค่ boilerplate (โค้ดสำเร็จรูปที่ต้องเขียนซ้ำ ๆ ไม่เพิ่มคุณค่า)
Go มีคติว่า "Accept interfaces, return structs" — รับ interface เป็น parameter แต่คืน struct จริงเป็นผลลัพธ์:
go
// ❌ Don't define interface for every struct
type UserService interface {
CreateUser(User) error
GetUser(int) (User, error)
}
type UserServiceImpl struct{} // implementation omittedtext
ปัญหา:
- มี implementation เดียว
- Interface = "boilerplate" (โค้ดซ้ำที่ไม่เพิ่มคุณค่า) ที่ไม่มีประโยชน์text
"Accept interfaces, return structs" — Go idiom
// Don't (คืน interface โดยไม่จำเป็น):
func NewUserService(...) UserServiceI { return &userServiceImpl{...} }
// Do (คืน struct จริง):
func NewUserService(...) *UserService { return &UserService{...} }หมายเหตุ: ตั้งชื่อต่างกัน —
UserServiceI(interface) กับUserService(struct) เพื่อไม่ให้ชนกันในบรรทัดเดียวกัน(ต่างจาก C#: C# นิยม prefix
Iนำหน้า interface เช่นIUserService— ตรงข้ามกับที่นี่ที่ต่อ suffixIท้ายชื่อ Go ไม่มีธรรมเนียมนี้ตายตัว และจริง ๆ ไม่นิยม prefix/suffix บอก type ด้วยซ้ำ เพราะปกติควรมี interface เดียวไม่ต้องแยกชื่อ — เจอปัญหาแยกชื่อแบบนี้เฉพาะตอน migrate จาก concrete type ไปเป็น interface ระหว่างทาง)
When to Use Interface
text
✅ Multiple implementations exist
✅ Mock for testing
✅ Decouple package
✅ Define behavior in consumer package
❌ Just to "be flexible"
❌ One implementation
❌ Anticipated but not actual need10. Define Interface Close to Use
หลักการที่ต่อจากข้อก่อน: ควรนิยาม interface ไว้ที่ "ฝั่งผู้ใช้" (consumer) ไม่ใช่ฝั่งผู้สร้าง (producer) — แพ็กเกจที่ใช้งานเป็นคนกำหนดว่าตัวเองต้องการ method อะไร ทำให้ลดการผูกพันระหว่างแพ็กเกจ (แพ็กเกจ producer ไม่ต้องรู้ด้วยซ้ำว่า interface นี้มีอยู่):
text
Common mistake (Java mindset):
package user/
user.go // type User struct
service.go // type UserService interface + UserServiceImpl
package handler/
handler.go // depends on user.UserService
→ user package own its interfaceGo idiom:
package user/
user.go // type User struct
service.go // type Service struct (concrete, not interface)
package handler/
handler.go
// Define interface ที่ handler ต้องใช้
type UserGetter interface { Get(id) User }
// depends on UserGetter (defined here, not in user package)
→ handler defines what it needs
→ user package doesn't even know "UserGetter" exists→ "Define interface in consumer, not producer"
11. Nil Interface — Gotcha
⛔ หัวข้อนี้เป็นระดับ intermediate+ — เนื้อหาเกี่ยวกับกลไกภายในของ interface ถ้าเพิ่งเริ่ม กด Ctrl+F แล้วค้นหา "Checkpoint" เพื่อข้ามไปได้เลย
เจาะลึก — กับดักที่เจอตอนเขียนจริง ไม่ต้องจำตอนนี้
นี่คือกับดักที่ทำให้โปรแกรมเมอร์ Go มากประสบการณ์ยังพลาด
(สำหรับคนมาจาก .NET: ใน C# null คือค่าเดียวไม่มีการแบ่ง type — bug แบบนี้เกิดไม่ได้ใน C# ดังนั้นเป็นเรื่องใหม่ทั้งหมด ไม่ใช่แค่รายละเอียดปลีกย่อยที่ต่างกันนิดหน่อย)
ทำไม: ภายใน interface value เก็บเป็น "คู่ของ (type, value)" — ไม่ใช่แค่ค่าเปล่า
ปัญหา: ถ้าคืน pointer ที่เป็น nil แต่ "ติด type มาด้วย" (typed nil = nil ที่มี type) — การเช็ค err != nil จะได้ true ทั้งที่ข้างในเป็น nil
ทางแก้: return nil ตรง ๆ อย่าคืนตัวแปร pointer ที่ตอนนี้เป็น nil ดูตัวอย่าง:
go
type MyError struct {
msg string
}
func (e *MyError) Error() string { return e.msg }
func getErr() error {
var err *MyError = nil
return err // returns "typed nil"
}
err := getErr()
if err != nil {
fmt.Println("error!") // 😱 prints — but should not
}ทำไม?
text
Interface value = (type, value) pair
- nil interface: type=nil, value=nil
- typed nil: type=*MyError, value=nil
→ interface != nil because type is set!Fix
go
func getErr() error {
var err *MyError = nil
if err == nil {
return nil // ✅ return untyped nil
}
return err
}
// Or just
func getErr() error {
if condition {
return &MyError{msg: "..."}
}
return nil // ✅ untyped nil
}12. Interface Embedding
เราประกอบ interface เล็ก ๆ เข้าด้วยกันเป็น interface ใหญ่ได้โดยการ "ฝัง" (embed) — เช่น ReadWriteCloser = Reader + Writer + Closer นี่คือวิธีที่ standard library สร้าง interface อย่าง io.ReadWriter, net.Conn สอดคล้องกับปรัชญา "interface เล็กแล้วประกอบ":
go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Embed multiple
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// = same as
type ReadWriteCloser interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
Close() error
}Standard library examples:
io.ReadWriter= Reader + Writerio.ReadCloser= Reader + Closerio.ReadWriteClosernet.Conn= Reader + Writer + lots of network methods
13. Common Patterns
เจาะลึก — design pattern ระดับ intermediate+
interface เป็นเครื่องมือหลักในการทำ design pattern คลาสสิกหลายตัวใน Go ส่วนนี้รวมตัวอย่างที่ใช้บ่อย — Strategy (สลับอัลกอริทึม), Observer (ระบบ event/subscriber) และ Plugin architecture (ต่อยอดความสามารถ) ทั้งหมดอาศัย interface เป็นจุดเชื่อม:
Strategy Pattern
go
type SortStrategy interface {
Sort([]int) []int
}
type BubbleSort struct{}
func (b BubbleSort) Sort(s []int) []int { return s } // implementation omitted
type QuickSort struct{}
func (q QuickSort) Sort(s []int) []int { return s } // implementation omitted
type Sorter struct {
strategy SortStrategy
}
func (s Sorter) Sort(data []int) []int {
return s.strategy.Sort(data)
}
// Usage
sorter := Sorter{strategy: QuickSort{}}
sorter.Sort([]int{3, 1, 2})Observer Pattern
go
type Event struct {
Type string
Data any // Go 1.18+ idiom — เทียบเท่า interface{}
}
type Handler interface {
Handle(Event)
}
type EventBus struct {
handlers []Handler
}
func (b *EventBus) Subscribe(h Handler) {
b.handlers = append(b.handlers, h)
}
func (b *EventBus) Publish(e Event) {
for _, h := range b.handlers {
h.Handle(e)
}
}
// Multiple handlers
type EmailHandler struct{}
func (h EmailHandler) Handle(e Event) { fmt.Println("email:", e.Type) } // implementation omitted
type LogHandler struct{}
func (h LogHandler) Handle(e Event) { fmt.Println("log:", e.Type) } // implementation omitted
bus := &EventBus{}
bus.Subscribe(EmailHandler{})
bus.Subscribe(LogHandler{})
bus.Publish(Event{Type: "user_created", Data: ...})Plugin Architecture
go
type Plugin interface {
Name() string
Run() error
}
type App struct {
plugins []Plugin
}
func (a *App) Register(p Plugin) {
a.plugins = append(a.plugins, p)
}
func (a *App) RunAll() {
for _, p := range a.plugins {
log.Printf("Running %s", p.Name())
p.Run()
}
}14. ตัวอย่างเต็ม — HTTP Middleware
ตัวอย่างจริงที่รวมพลังของ interface + function type — ระบบ middleware ของ HTTP server เทคนิคเด็ดคือ HandlerFunc ทำให้ฟังก์ชันธรรมดากลายเป็น type ที่ implement interface Handler ได้ แล้วห่อ handler ซ้อนกันเป็นชั้น ๆ (logging, auth) ได้อย่างยืดหยุ่น:
ตัวอย่างนี้ใช้
http.ResponseWriter/*http.Requestจากnet/httpของ stdlib — ยังไม่ต้องเข้าใจ HTTP รายละเอียด เดี๋ยวบท 11 อธิบายเต็ม ตอนนี้โฟกัสที่ "interface + function type" พอ ถ้ายังว่ายแล้วก็ข้ามไปข้อ 15 ก่อนได้Pattern นี้คือคู่ขนานของ
http.Handler+http.HandlerFuncใน stdlib ตรงตัว — เห็นบ่อยใน middleware library
go
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request)
}
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
// Middleware = function ที่รับ + return Handler
type Middleware func(Handler) Handler
// Logging middleware
func LoggingMiddleware(next Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Auth middleware
func AuthMiddleware(next Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", 401)
return
}
next.ServeHTTP(w, r)
})
}
// Chain
func Chain(middlewares ...Middleware) Middleware {
return func(final Handler) Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
final = middlewares[i](final)
}
return final
}
}
// Use
finalHandler := Chain(
LoggingMiddleware,
AuthMiddleware,
)(myHandler)HandlerFunc Trick — เข้าใจให้ลึก
นี่คือทริคที่มือใหม่ Go ดูแล้วงงสุด แต่เจอแทบทุก library — ดูทีละบรรทัด:
go
// 1) ประกาศ type ใหม่ที่ "ค่าจริง" คือ function signature
type HandlerFunc func(http.ResponseWriter, *http.Request)
// 2) ใส่ method ServeHTTP ให้ type นี้ — body แค่เรียกตัวเองในฐานะ function
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r) // ⭐ f เป็นค่าของ type HandlerFunc ซึ่งจริง ๆ เป็น function เลยเรียก f(...) ได้
}ทำไมสำคัญ:
- ตอนนี้
HandlerFuncมี methodServeHTTP→ มัน implement interfaceHandlerทันที (เพราะ Go = implicit) - แปลว่า function ธรรมดา หลังจาก cast (แปลงชนิด) เป็น
HandlerFuncแล้วจะใช้แทนHandlerได้
go
// function เปล่า ๆ
fn := func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello") // implementation omitted
}
// cast → กลายเป็น Handler ใช้ในที่ที่ต้องการ interface ได้เลย
var h Handler = HandlerFunc(fn)
h.ServeHTTP(w, r)→ ทริคนี้ทำให้เราเขียน middleware/handler เป็น "function" ที่อ่านง่าย แล้วยังเสียบเข้ากับ ecosystem (ระบบนิเวศ = library/เครื่องมือรอบ ๆ ภาษา) ที่ต้องการ interface Handler ได้ทั้งหมด
→ HandlerFunc = type ที่ implement Handler interface — common trick
15. Generics + Interface (Go 1.18+, cmp.Ordered ต้องใช้ Go 1.21+)
เจาะลึก — ควรเข้าใจ interface พื้นฐาน (ข้อ 1–8) ให้แน่นก่อน
ตั้งแต่ Go 1.18 มี generics (เจเนอริก = เขียนโค้ดใช้ได้กับหลายชนิดข้อมูลโดยไม่ต้องเขียนซ้ำ) ที่ทำงานคู่กับ interface ได้
เราใช้ interface เป็น "constraint" (ข้อจำกัด/เงื่อนไขของชนิดที่รับได้ — เช่น Ordered = type ที่เปรียบเทียบมากกว่า/น้อยกว่ากันได้) บอกว่า type ที่รับได้ต้องมีคุณสมบัติอะไร
ประโยชน์: เขียนฟังก์ชันที่ใช้ได้หลาย type โดยยังคง type safety — ไม่ต้องพึ่ง any + type assertion อีกต่อไป:
go
import "cmp" // Go 1.21+ — มี cmp.Ordered มาให้ใน stdlib
// Before generics — interface only (วิธีเก่า)
func Max(a, b interface{}) interface{} {
// need type assertion
// ❌ ตัวอย่างนี้ panic ถ้าไม่ใช่ int — นี่คือข้อจำกัดของวิธีเก่า ไม่ใช่ idiomatic code
if a.(int) > b.(int) { return a }
return b
}
// With generics (Go 1.21+ — ใช้ cmp.Ordered จาก stdlib)
func Max[T cmp.Ordered](a, b T) T {
if a > b { return a }
return b
}
// ก่อน Go 1.21 ใช้ `constraints.Ordered` จาก golang.org/x/exp/constraints แทน
Max(1, 2) // int
Max(1.5, 2.5) // float64
Max("a", "b") // string
// Interface as constraint
type Number interface {
int | float64
}
func Sum[T Number](nums []T) T {
var sum T
for _, n := range nums { sum += n }
return sum
}
Sum([]int{1, 2, 3}) // 6
Sum([]float64{1.0, 2.5, 3.5}) // 7.0→ Generics = ตัวเชื่อมระหว่างความปลอดภัยของชนิดข้อมูล (static type) กับความยืดหยุ่น
16. Common Mistakes
รวมข้อผิดพลาดเรื่อง interface ที่พบบ่อย — คืน interface โดยไม่จำเป็น, ทำ interface กว้างเกินไป, พึ่ง concrete type แทน interface (ทำให้ mock ยาก) และลืมว่า method set ของ value กับ pointer ต่างกัน (ทำให้ type ไม่ implement interface อย่างที่คิด):
Returning Concrete Type as Interface
go
// ❌
func NewUser() User { return User{} } // returns concrete (good for clients)
// Vs:
type UserCreator interface { Create() User }
func New() UserCreator { return nil } // ❌ unnecessary indirectionWide Interface
go
// ❌ 20 methods — illustrative pseudocode, ไม่ใช่ Go syntax จริง
// type Repository interface {
// Save(User) error; Update(User) error; Delete(int) error; Find(int) (User, error)
// FindBy(filter string) ([]User, error); Search(q string) ([]User, error); Count() int
// // ... อีกหลาย method
// }
// ✅ Smaller, focused
type UserSaver interface { Save(User) error }
type UserFinder interface { FindByID(int) (User, error) }
type UserDeleter interface { Delete(int) error }Embed Concrete Type in Interface
go
// ❌ Mix concrete + interface
type App struct {
db *sql.DB // concrete — hard to mock
cache *redis.Client
logger *log.Logger
}
// ✅ Depend on interface
type App struct {
db DBExecutor // interface
cache Cache // interface
logger Logger // interface
}Forget Pointer Receiver
go
type Counter struct{ count int }
func (c *Counter) Inc() { c.count++ } // ✅ pointer
type Incrementer interface { Inc() }
var i Incrementer = Counter{} // ❌ Counter doesn't implement (only *Counter)
var i Incrementer = &Counter{} // ✅→ Method set ของ value vs pointer ต่างกัน
17. Reflect — Avoid If Possible
เจาะลึกมาก — แม้แต่ Go dev มืออาชีพก็ใช้น้อย ชื่อหัวข้อบอกแล้วว่า "เลี่ยงถ้าทำได้"
go
import "reflect"
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
u := User{Name: "Anna", Age: 25}
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
tag := field.Tag.Get("json")
fmt.Printf("%s (%s) = %v\n", field.Name, tag, value)
}
// Name (name) = Anna
// Age (age) = 25⚠️ Reflection ช้า + verbose (เขียนยุ่งยาก โค้ดยาว) + lose compile-time safety (เสีย type safety ตอน compile — error จะเจอตอนรันแทน)
→ ใช้น้อยที่สุด — เฉพาะ marshalling library, ORM, generic helper ก่อน Go 1.18
18. Iterator Pattern (Go 1.23+)
Go 1.23 เพิ่มความสามารถให้ for range วนบนฟังก์ชัน iterator ที่เราเขียนเองได้ — ทำให้สร้าง sequence แบบ lazy (คล้าย generator) แล้ววนด้วยไวยากรณ์ for range ที่คุ้นเคย:
go
// Iterator function
func Range(start, end int) func(yield func(int) bool) {
return func(yield func(int) bool) {
for i := start; i < end; i++ {
if !yield(i) {
return
}
}
}
}
// Use with range
for i := range Range(0, 5) {
fmt.Println(i)
}
// 0 1 2 3 419. ⚠️ Common Pitfalls
สรุปกับดักเรื่อง interface ทั้งหมดในบทไว้ในตารางเดียว คู่กับแนวทางที่ถูก ใช้เป็นเช็คลิสต์ทบทวนก่อนออกแบบ interface จริง:
| ❌ | ✅ |
|---|---|
| Wide interface (10+ methods) | small interface (1-3 methods) |
| Interface for one implementation | concrete struct |
| Return interface from constructor | return concrete struct |
| Define interface in producer package | define in consumer package |
| Compare typed nil to nil | return untyped nil explicitly |
| Method set forget pointer vs value | consistent receiver type |
| Use reflection for generic | use generics (Go 1.18+) |
| Empty interface for everything | any only when truly heterogeneous |
20. Idiomatic Go Interface
สรุปลักษณะของ interface ที่ถือว่า "เป็น Go แท้" (idiomatic) มี 4 อย่างหลัก — เล็ก, นิยามฝั่งผู้ใช้, ตั้งชื่อลงท้าย "-er", ใช้เพื่อพฤติกรรมไม่ใช่ข้อมูล ยึดแนวทางเหล่านี้แล้วโค้ดจะเข้ากับสไตล์ของชุมชน Go:
text
✅ Small (1-3 methods)
✅ Defined in consumer package
✅ Named with "-er" suffix (Reader, Writer, Stringer, Sorter)
✅ Accept interface, return struct
✅ Use to enable testing (mock = struct with methods)
✅ Compose with embedding (ReadWriter = Reader + Writer)
✅ Used for behavior, not data21. Checkpoint
🛠️ Checkpoint 6.1 — Shape Interface (อินเทอร์เฟซรูปทรง)
go
type Shape interface {
Area() float64
Perimeter() float64
}Implement (ทำให้ type เข้าข่าย interface): Rectangle, Circle, Triangle
- function
TotalArea(shapes []Shape) float64(พื้นที่รวม) - function
LargestShape(shapes []Shape) Shape(รูปที่ใหญ่สุด)
🛠️ Checkpoint 6.2 — Storage Interface (อินเทอร์เฟซที่เก็บข้อมูล)
go
type Storage interface {
Save(key string, data []byte) error
Load(key string) ([]byte, error)
Delete(key string) error
}Implement:
- MemoryStorage (เก็บใน map ในหน่วยความจำ)
- FileStorage (เก็บลงดิสก์)
- MockStorage (ตัวปลอมสำหรับเทสต์)
ใช้ใน UserService โดยฉีด (inject = ส่งเข้ามาจากภายนอก) storage interface
🛠️ Checkpoint 6.3 — Plugin System (ระบบปลั๊กอิน)
สร้าง:
go
type Plugin interface {
Name() string
Init() error
Run(input string) (output string, err error)
}- Plugin A: แปลงเป็นตัวพิมพ์ใหญ่ (uppercase)
- Plugin B: นับคำ (word count)
- Plugin C: กลับข้อความ (reverse)
- ทะเบียนปลั๊กอิน (registry) + เรียกต่อกันเป็นทอด (chain execution)
🛠️ Checkpoint 6.4 — HTTP Middleware
ทำตัวอย่างข้อ 14 — เพิ่ม:
- RateLimitMiddleware (จำกัดอัตราการเรียก)
- CORSMiddleware (อนุญาตเรียกข้ามโดเมน)
- RecoveryMiddleware (กู้จาก panic)
🛠️ Checkpoint 6.5 — Type Switch
สร้าง function Stringify(v any) string ที่:
- int → strconv.Itoa
- float64 → fmt.Sprintf("%.2f")
- bool → "true"/"false"
- []byte → string
- struct → reflect to fields
- nil → "nil"
22. สรุปบท
ทบทวนภาพรวมของบทนี้ — ตั้งแต่ interface คืออะไร, การ implement แบบ implicit, ปรัชญา interface เล็ก, type assertion/switch, ไปจนถึงกับดัก typed nil และการใช้ generics แทน empty interface:
สรุปสั้น (ภาษาไทย): interface ของ Go = "ประกาศพฤติกรรมที่ต้องการ" — ใครทำ method ตามได้ก็ใช้แทนกันได้ทันที (ไม่ต้องประกาศว่า implements) เน้นทำ interface เล็ก ๆ แล้วประกอบรวมกัน นิยามไว้ฝั่งคนใช้ ไม่ใช่ฝั่งคนสร้าง
✅ Interface = behavior contract (สัญญาเรื่องพฤติกรรม) — define what, not how
✅ Implicit implementation — มี method ตรง = auto-satisfy
✅ Go favors small interface (1-3 methods) + composition
✅ io.Reader, io.Writer, Stringer, error = canonical examples
✅ any = interface{} (Go 1.18+) — empty interface
✅ Type assertion v.(T) + safe v, ok := x.(T)
✅ Type switch switch v := x.(type) — multi-type handling
✅ "Accept interfaces, return structs" — Go idiom
✅ Define interface at consumer — not producer
✅ ⚠️ Typed nil ≠ nil ใน interface — beware
✅ Generics (Go 1.18+) แทน interface{} for type-safe generic
✅ Use interface for: multiple impl, mocking, plugin, decouple package