ข้ามไปยังเนื้อหา

Interfaces

Interface คือชุดของ method signature ที่มีชื่อ type ใดที่มี methods ทั้งหมดนั้นจะตอบสนอง interface — โดยไม่ต้องบอกอย่างชัดเจน

type Shape interface {
Area() float64
Perimeter() float64
}

type ใดที่มี method Area() float64 และ Perimeter() float64 จะตอบสนอง Shape โดยอัตโนมัติ

นี่คือคุณสมบัติที่กำหนดตัวตน interfaces ของ Go เปรียบเทียบ:

ภาษาวิธีประกาศการตอบสนอง
Java / C#class Circle implements Shape
Goแค่มี methods ที่ถูกต้อง

ผลลัพธ์คือโค้ดที่ decoupled type ใน package geometry สามารถตอบสนอง interface ใน package renderer โดยที่ไม่มี package ใดรู้จักอีก package ทำให้ Go interfaces compose ข้าม package boundaries ได้โดยไม่มี import dependency บน interface definition

standard library ของ Go สร้างบน interfaces เล็ก ๆ ที่มีจุดโฟกัส:

type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }

คำสอนของ Go: “ยิ่ง interface ใหญ่ abstraction ยิ่งอ่อนแอ” interface สอง methods ถูกตอบสนองโดย types จำนวนมากกว่า interface สิบ methods อย่างมาก ทำให้ reuse ได้มากกว่าอย่างมหาศาล

Interface ที่ไม่มี method ถูกตอบสนองโดยทุก type ตั้งแต่ Go 1.18 มี alias ว่า any:

var v any = 42
v = "hello"
v = []int{1, 2, 3}

ใช้ any อย่างประหยัด — เพราะสละ type safety กรณีใช้งานปกติ: generic containers (ก่อนที่ Go จะมี generics), JSON unmarshalling รูปร่างที่ไม่รู้จัก, และ function logging แบบ variadic

แนวทาง Go ที่ idiomatic:

  • Function parameters ควรเป็น interfaces — รับ type กว้างที่สุดที่ครอบคลุมสิ่งที่คุณเรียกจริง
  • Return values ควรเป็น concrete structs — caller ได้รับ API เต็มของ type จริง; พวกเขา narrow เองได้
// Good: accepts an interface, returns a concrete type
func NewReader(rd io.Reader) *bufio.Reader { ... }
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
type Rect struct {
W, H float64
}
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }
func describe(s Shape) {
fmt.Printf("area=%.2f perimeter=%.2f\n", s.Area(), s.Perimeter())
}
// MyErr demonstrates the typed-nil-in-interface trap.
type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }
func mayFail(fail bool) error {
var p *MyErr // nil, but typed *MyErr
if fail {
p = &MyErr{msg: "boom"}
}
return p // always a non-nil error interface, even when p is nil
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rect{W: 4, H: 3},
}
for _, s := range shapes {
describe(s)
}
err := mayFail(false)
fmt.Println(err == nil) // false — the interface holds a typed nil *MyErr, not a true nil
}

interface value คือสอง words: pointer ไปยัง itab (type + method table) และ pointer ไปยัง data

flowchart LR
  subgraph Iface["interface value (2 words)"]
    T["*itab"]
    D["data"]
  end
  T --> ITAB["itab
 type: Circle
 methods: [Area, Perimeter]"]
  D --> VAL["Circle{Radius: 5}"]
  ITAB -.dynamic dispatch.-> CALL["itab.fun[0](data) → Circle.Area(data)"]
interface value เก็บ pointer ไปยัง type/method table และ pointer ไปยัง data — การเรียก method คือ indirect call ผ่าน itab

คุณตรวจสอบว่า type หนึ่งตอบสนอง interface ได้ตั้งแต่ compile time โดยไม่ต้องสร้าง value เลย ด้วย blank assignment:

var _ Shape = (*Circle)(nil)

บรรทัดนี้ compile ผ่านก็ต่อเมื่อ *Circle มีทุก method ที่ Shape ต้องการ ไม่มีต้นทุนตอน runtime และจับ method ที่ขาดหรือสะกดผิดได้ทันทีตอนเขียนโค้ด แทนที่จะไปเจอตอนเรียกใช้ครั้งแรก

interface value คือ (type, data) มันจะ == nil ก็ต่อเมื่อ ทั้งสอง words เป็น nil function ที่ประกาศ return type เป็น *T แต่ return มันผ่าน error-typed return จะ box type descriptor ที่ไม่ nil รอบ ๆ data pointer ที่ nil — ตัว interface เองไม่เคยเป็น nil เลย แม้ pointer ข้างในจะเป็น nil ก็ตาม:

type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }
func f(fail bool) error {
var p *MyErr // nil, but typed *MyErr
if fail {
p = &MyErr{msg: "boom"}
}
return p // always returns a non-nil error interface, even when p is nil
}

รันบน Go 1.26.5:

<nil>
false
*main.MyErr

fmt.Println(err) print <nil> — การ print dereference มากพอที่จะแสดงว่า pointer เป็น nil — แต่ err == nil เป็น false และ %T ยืนยันว่า dynamic type คือ *main.MyErr ตัวแปร interface พก type descriptor ตั้งแต่วินาทีที่คุณ assign typed nil pointer เข้าไป และ type descriptor นั้นอย่างเดียวก็เพียงพอที่จะทำให้ comparison เป็น false ทางแก้คือ return literal nil เมื่อไม่มี error เลย ไม่ใช่ typed nil pointer ผ่าน interface-typed return

การเก็บ concrete value ไว้ใน interface variable เรียกว่า boxing มัน allocate บน heap หรือไม่ขึ้นกับสิ่งที่ compiler พิสูจน์ได้ Compile ตัวอย่าง shapes ด้านบนด้วย -gcflags=-m:

./main.go:11:6: can inline Circle.Area
./main.go:16:20: devirtualizing s.Area to Circle
./main.go:16:20: inlining call to Circle.Area
./main.go:15:16: c does not escape
./main.go:16:20: ~r0 escapes to heap

เมื่อมี concrete type เดียวเท่านั้นที่ไหลเข้า s Shape ใน function นั้น compiler จะ devirtualize การเรียก — มันรู้แบบ static ว่า s.Area หมายถึง Circle.Area, inline มัน, และ c เองไม่ escape เลย แต่การเก็บ concrete type สองแบบที่ต่างกันเข้า interface-typed slice เดียวกันทำลายสิ่งนั้น:

./main.go:19:20: c escapes to heap
./main.go:19:27: Rect{...} escapes to heap

เมื่อ []Shape เก็บได้ทั้ง Circle หรือ Rect compiler จะไม่รู้อีกต่อไปว่าต้องเรียก Area ตัวไหนโดยไม่ผ่าน itab ตอน runtime ดังนั้นทั้งสอง values จึงถูก box ขึ้น heap นี่คือต้นทุนจริงเบื้องหลัง “interfaces ไม่ฟรี” — ไม่ใช่ตัว interface value เอง (ยังเป็นสอง words บน stack) แต่คือ data ที่ถูก box ที่มันชี้ไปเมื่อ static devirtualization ทำไม่ได้

interface value สอง values เทียบด้วย == ได้ก็ต่อเมื่อ dynamic type ของทั้งคู่ comparable compiler ไม่รู้ dynamic type ล่วงหน้าเมื่อทั้งสองฝั่งเป็น any มันจึงยอมให้ == เสมอ แล้วเลื่อนการเช็กจริงไปที่ runtime:

var a, b any
a = []int{1, 2}
b = []int{1, 2}
fmt.Println(a == b)
panic: runtime error: comparing uncomparable type []int

slice, map, และ function ไม่ comparable — การเก็บมันไว้ใน interface ไม่ได้เปลี่ยนสิ่งนั้น การเทียบ interface value สองตัวที่ dynamic type เป็น struct ที่มี field เป็น slice หรือ map ก็ panic แบบเดียวกัน นี่คือแหล่งของ crash ที่พบบ่อยลึกอยู่ใน test-assertion helper หรือ map[any]any key ที่ดูปกติดีจนกระทั่งมี field type slice โผล่มา

Anti-pattern ที่พบบ่อยซึ่งนำเข้ามาจากโค้ด Java/C# คือ producer package ที่นิยาม interface ใหญ่ไว้ข้าง ๆ concrete implementation เดียวของมัน — type UserServiceInterface interface { ... } คู่กับ type UserService struct{ ... } โดยทุก method ของ UserService ถูก copy ขึ้นไปบน interface “เพื่อ testability” นี่คือ interface pollution: interface เพิ่ม indirection โดยไม่ decouple อะไรเลย เพราะยังมี implementation เดียวอยู่ดี และทุกครั้งที่ UserService เพิ่ม method interface ก็ต้องโตตามไปด้วย

รูปแบบที่ idiomatic ของ Go ตรงข้ามกัน: package ที่ใช้dependency คือคนที่นิยาม interface เล็กที่สุดที่มันต้องการ ตรงจุดที่มันใช้ ส่วน producer package export แค่ concrete type นี่คือสิ่งที่ io.Copy ทำเป๊ะ ๆ — signature ของมันคือ func Copy(dst Writer, src Reader) (int64, error) นิยามใน io เอง ไม่ใช่โดย type ทุกตัวที่บังเอิญ copy ได้ caller ที่ส่ง *os.File, *bytes.Buffer, หรือ net.Conn ไม่ต้องให้ types เหล่านั้น “implement” อะไรโดยตั้งใจเลย พวกมันตอบสนอง io.Reader/io.Writer เพราะมี methods ที่ถูกต้องอยู่แล้ว

สองอาการที่บอกว่า interface ควรย้ายไป consumer หรือหายไปเลย:

  • interface มี implementation เดียวในทั้ง codebase และไม่มี test ไหนสลับใส่ fake เลย
  • method set ของ interface เป็นแค่ “ทุก exported method ของ struct ตัวเดียวที่ implement มัน” — copy กลไก ไม่ใช่ contract เล็กที่ตั้งใจออกแบบ
สิ่งที่ได้ประโยชน์ต้นทุน
implicit satisfactiondecoupled, no import dependencyยากต่อการ discover ว่า type ใด implement interface ใด
small interface (1-2 methods)composable, implement ง่ายอาจต้องใช้หลาย interface ร่วมกัน
any / empty interfaceรับ type ใดก็ได้สูญเสีย type safety ทั้งหมด
dynamic dispatchflexibilityต้นทุนจริง: indirect call (~1–3 ns) + เสีย inlining + ค่าอาจ escape ไป heap เมื่อ box
  • interface ฟรี — interface เพิ่ม indirection 1 ชั้นและ dynamic dispatch overhead
  • nil interface และ interface ที่เก็บ nil pointer เป็นเหมือนกัน — nil interface คือ (nil, nil), interface ที่เก็บ nil pointer คือ (*T, nil) ซึ่งไม่เท่ากัน
  • ยิ่ง interface มี method มาก ยิ่งดี — Go idiom: “the bigger the interface, the weaker the abstraction”
  • implicit satisfaction แปลว่า duck typing — Go ตรวจสอบ interface ตอน compile ไม่ใช่ runtime

💡 ตัวอย่างจากของจริง

io.Reader (1 method): http.Body, os.File, bytes.Buffer, strings.Reader ล้วน implement — composable ทั่ว ecosystem โดยไม่มี import dependency

Kubernetes ใช้ interface runtime.Object (k8s.io/apimachinery/pkg/runtime) ให้ทุก Kubernetes resource ทำงานกับ generic controller ได้ — decoupling ผ่าน small interface

เมื่อ function ที่ generate มา return error (หรือ interface type ใด ๆ) ให้เช็กสิ่งเหล่านี้ก่อนเชื่อมัน:

  • ทุก return path ใช้ bare nil หรือมี path ที่ return typed pointer variable ที่ตัวมันเองอาจเป็น nil (var p *MyErr; ...; return p)? แบบหลังทำให้ err != nil เป็น true แม้บน path ที่ “ไม่มี error”
  • == ถูกใช้เทียบ any-typed หรือ interface-typed values สองตัวหรือเปล่า? ถ้า dynamic type ฝั่งใดฝั่งหนึ่งอาจเป็น slice, map, หรือ func สิ่งนี้ compile ผ่านและ panic ตอน runtime
  • package นิยาม interface ไว้ข้าง ๆ implementation เดียวของมัน โดย methods ของ interface mirror struct แบบกลไกหรือเปล่า? นั่นคือ interface pollution — เช็กว่ามี type หรือ test อื่นที่ต้องการ indirection นี้จริงหรือไม่
  • โค้ด assert หรือเช็ก capability เพิ่มเติม (Flusher, Hijacker, WriterTo) แทนที่จะสมมติ concrete type หรือเปล่า? การข้ามเช็กนั้นทำให้ functionality ที่ wrapped type มีให้หายไปอย่างเงียบ ๆ
  • function parameter ถูก type เป็น concrete struct ทั้งที่มีแค่หนึ่งหรือสอง methods ที่ถูกเรียกบนมันจริง ๆ หรือเปล่า? นั่นคือโอกาสที่พลาดไปในการทำ small, consumer-defined interface — ไม่ใช่ bug แต่เป็น design smell ที่ควร flag

🐛 หา bug ในโค้ดนี้

type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }
func validate(input string) error {
var problem *MyErr
if input == "" {
problem = &MyErr{msg: "empty input"}
}
return problem
}
func main() {
err := validate("ok")
if err != nil {
panic("validation failed: " + err.Error())
}
}
เฉลย

validate return problem ซึ่งเป็นตัวแปร *MyErr ผ่าน error-typed return — แม้บน success path ที่ problem เป็น nil error interface ที่ return จึงเก็บ (type: *MyErr, data: nil) ซึ่งไม่เคย == nil ดังนั้น err != nil จึงเป็น true แม้ input จะถูกต้อง main จากนั้นเรียก err.Error() บน nil *MyErr receiver นั้นเพื่ออ่าน msg ซึ่ง dereference nil pointer — panic จริงคือ panic: runtime error: invalid memory address or nil pointer dereference ไม่ใช่ข้อความที่ส่งเข้า panic() ทางแก้คือ return literal nil บน success path: if input == "" { return &MyErr{msg: "empty input"} }; return nil

type ใน Go ประกาศว่า implement interface อย่างไร?
โค้ดนี้ print อะไร?
type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }

func f(fail bool) error {
    var p *MyErr
    if fail {
        p = &MyErr{msg: "boom"}
    }
    return p
}

err := f(false)
fmt.Println(err == nil)
จะเกิดอะไรขึ้นตอนรัน?
var a, b any
a = []int{1, 2}
b = []int{1, 2}
fmt.Println(a == b)
package หนึ่ง export type UserServiceInterface interface { ... } ที่ทุก method ของ UserService struct ตัวเดียวถูก copy ขึ้นไปบนมัน และไม่มีที่ไหนใน codebase implement มันหรือสลับใส่ fake นี่เรียกว่าอะไร และควรแก้อย่างไร?
empty interface any (Go 1.18+) เป็น alias ของอะไร?
function ของคุณต้องการเขียน bytes ควรใช้ parameter type ใดที่ idiomatic ที่สุด?