Day 10: Implicit Interfaces & Internals
Quiz: 0/16 | Score: 0 pts

Day 10: Implicit Interfaces, Runtime Internals & Dynamic Dispatch

Dissecting iface, eface, Compile-Time Duck Typing, Method Sets, and The True Cost of Interface Boxing

Section 0: The "Why" & Core Vocabulary

ในฐานะวิศวกรที่ย้ายมาจาก Kotlin / Spring Boot เราคุ้นเคยกับ Nominal Subtyping: หากเรามี Interface PaymentProcessor เราต้องประกาศ class SwiftProcessor : PaymentProcessor หากไม่ได้เขียนชื่อสัญญานี้อย่างชัดเจน แม้คลาสจะมีฟังก์ชัน process(tx: Transaction) เหมือนกันทุกประการ JVM ก็จะไม่ยอมให้ใช้งานข้ามกัน

The Spring Boot Anti-Pattern: Interface Bloat & Premature Abstraction

ใน Spring Boot วัฒนธรรมองค์กรมักผลักดันให้สร้างไฟล์คู่เสมอ: AccountService.java และ AccountServiceImpl.java โดยอ้างเรื่อง Dependency Injection แม้ว่าจะมี Implementation เพียงตัวเดียวตลอดชีวิตของ Service นั้น ส่งผลให้เกิด Classloader overhead, ขยายขนาด Jar file และสร้างความซับซ้อนในการอ่านโค้ดโดยไม่จำเป็น

นอกจากนี้ การเรียกเมธอดผ่าน Interface ใน JVM ต้องพึ่งพาการทำงานของ JIT Inline Caching หากมีคลาสลูกถูกโหลดเกิน 2 แบบ (Megamorphic Call Site) ประสิทธิภาพจะตกลงสู่การค้นหาตาราง vtable เสมอ

Go พลิกวิธีคิดทั้งหมดด้วย Structural Typing (Duck Typing ในระดับ Compile-Time): "If it walks like a duck and quacks like a duck, it's a duck." ผู้สร้าง Struct ไม่จำเป็นต้องรู้ว่าในอนาคต Struct ของตนจะถูกนำไปใช้ใน Interface อะไร ส่วนผู้บริโภค (Consumer) จะเป็นผู้ประกาศ Interface ขนาดจิ๋ว (1-2 เมธอด) ณ จุดที่ต้องการใช้งานเอง (Consumer-driven Interfaces)

คำศัพท์ทางเทคนิค (Go) นิยามและพฤติกรรมระดับ Runtime / Hardware แนวคิดเทียบเท่าใน Kotlin / Spring
Implicit Satisfaction Struct ถือว่า Implement Interface ทันทีที่มี Method Set ครบตามที่กำหนด โดยไม่ต้องใช้คีย์เวิร์ดใดๆ คอมไพเลอร์ตอกบัตรตรวจสอบ ณ เวลา Compile Duck Typing (แต่ Go ปลอดภัย 100% เพราะ Type-Check ตอน Compile ไม่ใช่ Dynamic Script)
Non-Empty Interface (iface) Interface ที่มีอย่างน้อย 1 เมธอด เก็บข้อมูล 16 ไบต์: 8B สำหรับ *itab (Metadata + Function Pointers) และ 8B สำหรับ Data Pointer Standard JVM Interface Reference พร้อม vtable pointer
Empty Interface (eface หรือ any) Interface ที่ไม่มีเมธอดเลย (interface{} หรือ any ใน Go 1.18+) เก็บ 16 ไบต์: 8B สำหรับ *_type และ 8B สำหรับ Data Pointer java.lang.Object หรือ Kotlin Any?
Method Set ชุดของเมธอดที่ผูกอยู่กับ Type นั้นๆ โดยกฎกำหนดว่า T (Value) จะมีเฉพาะ Value Receiver เท่านั้น ส่วน *T (Pointer) จะมีทั้ง Value และ Pointer Receivers Class Instance Methods (ใน Kotlin ไม่มีแยก Value vs Pointer Method Set)
Type Assertion คำสั่งคลี่ Interface เพื่อดึง Concrete Value หรือตรวจสอบ Type ภายใน เช่น v, ok := i.(MyStruct) ตรวจสอบผ่าน Type Hash ภายใน itab val v = obj as? MyClass (Safe Cast ใน Kotlin)
Type Switch Control Structure ที่แยกสายการทำงานตาม Concrete Type ที่ซ่อนอยู่ใน Interface ผ่านการเปรียบเทียบ Type Metadata when (obj) { is A -> ... is B -> ... }

Section 1: Go Syntax & Command Blueprint

การนิยาม Interface ใน Go ใช้คีย์เวิร์ด type [ชื่อ] interface โดยระบุเฉพาะ Method Signatures (ชื่อ, พารามิเตอร์, ค่าที่คืน) เท่านั้น:

1.1 พื้นฐานการประกาศ Interface และ Implicit Satisfaction

package main

import "fmt"

// 1. ประกาศ Interface ขนาดเล็ก (แนะนำ 1-2 เมธอดตามปรัชญา Go)
type PaymentGateway interface {
    Authorize(amount int64) (string, error)
}

// 2. ประกาศ Concrete Struct ธรรมดา (สังเกต: ไม่มีการอ้างอิงถึง PaymentGateway เลย!)
type PromptPayService struct {
    MerchantID string
}

// 3. ผูก Method เข้ากับ Struct
// ทันทีที่บรรทัดนี้ถูกเขียน PromptPayService จะกลายเป็น PaymentGateway โดยอัตโนมัติ!
func (p *PromptPayService) Authorize(amount int64) (string, error) {
    txID := fmt.Sprintf("PP-%s-%d", p.MerchantID, amount)
    return txID, nil
}

// 4. ฝั่ง Consumer รับ Interface เข้ามาทำงาน
func ExecuteCheckout(gateway PaymentGateway, amount int64) {
    txID, err := gateway.Authorize(amount)
    if err != nil {
        fmt.Println("Payment failed:", err)
        return
    }
    fmt.Println("Payment authorized successfully. TXID:", txID)
}

func main() {
    service := &PromptPayService{MerchantID: "M-10928"}
    // ส่ง service เข้าไปได้ทันทีเพราะ Implement เมธอด Authorize ครบถ้วน
    ExecuteCheckout(service, 45000)
}

1.2 กฎเหล็ก Method Sets: Value Receiver vs Pointer Receiver

นี่คือจุดที่ทำให้วิศวกร JVM สับสนที่สุด: ทำไมบางครั้งคอมไพเลอร์จึงฟ้องว่า Struct ของเราไม่ได้ Implement Interface ทั้งๆ ที่เขียนเมธอดชื่อเดียวกันเป๊ะ?

The Method Set Compatibility Matrix

- Value Receiver func (t T): เมธอดนี้จะอยู่ใน Method Set ของทั้ง T (Value) และ *T (Pointer)
- Pointer Receiver func (t *T): เมธอดนี้จะอยู่ใน Method Set ของ *T เท่านั้น! (ไม่อยู่ใน T เพราะ Go ไม่สามารถหา Address ของ Value ชั่วคราวได้เสมอไป)

type RFIDReader interface {
    ResetCounter()
}

type ImpinjGate struct {
    Count int
}

// Implement ด้วย Pointer Receiver เพราะต้องการแก้ไขค่าภายใน Struct
func (g *ImpinjGate) ResetCounter() {
    g.Count = 0
}

func StartRaceGate() {
    // ถูกต้อง: ส่ง Pointer (*ImpinjGate) ซึ่งมี ResetCounter() อยู่ใน Method Set
    var r1 RFIDReader = &ImpinjGate{Count: 10}
    r1.ResetCounter()

    // คอมไพล์พังทันที! (Compile Error)
    // cannot use ImpinjGate{} (value of type ImpinjGate) as RFIDReader value:
    // ImpinjGate does not implement RFIDReader (method ResetCounter has pointer receiver)
    // var r2 RFIDReader = ImpinjGate{Count: 10} 
}

1.3 Type Assertion (Safe vs Unsafe)

เมื่อเราถือตัวแปร Interface แล้วต้องการดึงค่า Concrete Type ดั้งเดิมออกมาใช้งาน เราใช้ไวยากรณ์ i.(ConcreteType):

func InspectGateway(gw PaymentGateway) {
    // 1. Unsafe Assertion: หาก gw ไม่ใช่ *PromptPayService จะเกิด RUNTIME PANIC ทันที!
    // pp := gw.(*PromptPayService) 

    // 2. Safe Assertion (Idiomatic Go): ใช้รูปแบบ Value, Ok Idiom
    if pp, ok := gw.(*PromptPayService); ok {
        // ดึงสำเร็จ! pp มี Type เป็น *PromptPayService เข้าถึงฟิลด์ MerchantID ได้
        fmt.Println("Connected to PromptPay Merchant:", pp.MerchantID)
        return
    }

    fmt.Println("Unknown payment gateway provider")
}

1.4 Type Switch (Polymorphic Branching)

หากต้องการตรวจสอบหลายๆ Type พร้อมกัน ให้ใช้ switch v := i.(type) ซึ่งทำหน้าที่เหมือน pattern matching บน Types:

func RouteTransaction(gw PaymentGateway, amount int64) {
    switch concrete := gw.(type) {
    case *PromptPayService:
        fmt.Println("Routing via Thai QR / PromptPay system:", concrete.MerchantID)
    default:
        fmt.Printf("Generic gateway execution for type: %T\n", concrete)
    }
}

Section 2: Memory Architecture & Full Struct Definitions

ในภาษา Go ค่า Interface ไม่ใช่ Pointer ธรรมดาแบบ void* ใน C หรือ Object Reference บน JVM แต่ Interface ถูกนิยามไว้ใน Go Runtime Source Code (src/runtime/runtime2.go) ออกเป็น 2 ชนิดที่ชัดเจน:

2.1 โครงสร้าง iface (Non-Empty Interface) = 16 ไบต์

ใช้สำหรับ Interface ที่มี Method กำกับ เช่น io.Reader, error, PaymentGateway

// runtime/runtime2.go
type iface struct {
    tab  *itab          // 8 ไบต์: ชี้ไปยัง Interface Table (Type metadata + Method table)
    data unsafe.Pointer // 8 ไบต์: ชี้ไปยัง Concrete Value จริงบน Heap หรือ Memory Block
}

type itab struct {
    inter *interfacetype // 8 ไบต์: นิยามฝั่ง Interface Type (เช่น PaymentGateway)
    _type *_type         // 8 ไบต์: นิยามฝั่ง Concrete Type จริง (เช่น *PromptPayService)
    hash  uint32         // 4 ไบต์: สำเนา Hash ของ _type เพื่อให้ Type Assertion เปรียบเทียบได้เร็วใน O(1)
    _     [4]byte        // 4 ไบต์: Padding เพื่อ Align ให้ครบ 8 ไบต์ Boundary
    fun   [1]uintptr     // ขนาดแปรผัน: Array ของ Function Pointers ชี้ไปยัง Machine Code ของ Methods
}

2.2 โครงสร้าง eface (Empty Interface / any) = 16 ไบต์

ใช้สำหรับ Interface ที่ไม่มี Method ใดๆ (interface{} หรือ any) ไม่จำเป็นต้องมี Method Table (fun) จึงใช้ _type โดยตรง:

// runtime/runtime2.go
type eface struct {
    _type *_type         // 8 ไบต์: ชี้ไปยัง Type Metadata ของค่าที่ใส่เข้ามา
    data  unsafe.Pointer // 8 ไบต์: ชี้ไปยัง Concrete Data Pointer
}

2.3 ASCII Memory Layout: การชี้เป้าของ iface ใน RAM 64-bit

+---------------------------------------------------------------------------------------+ | Variable: var gw PaymentGateway (iface: 16 Bytes) | +-------------------------------------------+-------------------------------------------+ | Offset 0x00: tab (*runtime.itab) [8 Bytes]| Offset 0x08: data (unsafe.Pointer) [8 B] | +---------------------+---------------------+---------------------+---------------------+ | | v v +-------------------------------+ +-------------------------------+ | runtime.itab | | Concrete Data Allocation | +-------------------------------+ | (PromptPayService on Heap) | | inter: *interfacetype | +-------------------------------+ | _type: *_type (PromptPay) | | MerchantID: "M-10928" (16 B) | | hash : 0x8F31C42B (uint32) | +-------------------------------+ | pad : 4 Bytes | | fun[0]: &PromptPay.Authorize | --------> [Direct Assembly Machine Code] +-------------------------------+
Architecture Insight: The itab Cache Table

โครงสร้าง itab ไม่ได้ถูกสร้างใหม่ทุกครั้งที่มีการกำหนดตัวแปร! ตัว Go Runtime มี Global itab Hash Table เมื่อโปรแกรมรันคู่ Interface และ Concrete Type ใดเป็นครั้งแรก Runtime จะคำนวณและ Cache ค่า itab เอาไว้ และครั้งต่อไปจะดึง Pointer เดิมมาใช้ในเวลา $O(1)$ ทันที

Section 3: Visual Workflow & Assembly/RAM Mechanics

อะไรเกิดขึ้นเมื่อเราแปลง Struct ธรรมดาไปเป็น Interface? ทำไมประสิทธิภาพจึงเปลี่ยนไป? มาเจาะลึกกลไกเบื้องหลังระดับ CPU และ Escape Analysis

3.1 ขั้นตอนการแปลง Struct เข้าสู่ Interface (Interface Boxing)

Step 1: โค้ดประกาศ `var gw PaymentGateway = &PromptPayService{...}` Step 2: Compiler ตรวจสอบตั้งแต่ตอน Compile ว่า Method Set ของ *PromptPayService ครบตาม PaymentGateway หรือไม่ Step 3: หากครบ Compiler จะแทรกคำสั่งเรียก Runtime `runtime.convT2I` หรือจัดเตรียม Pointer Step 4: ตัวแปร Concrete Type จะถูก **บังคับ Escape ลง Heap เสมอ** (Escape Analysis) เพราะ Interface เก็บค่าผ่าน `data unsafe.Pointer` ซึ่งตัวแปรบน Stack ไม่สามารถรับประกันอายุขัย (Lifetime) ได้ Step 5: สร้าง struct `iface` ขนาด 16 ไบต์บน Stack ของฟังก์ชันผู้เรียก

3.2 กลไกการเรียกเมธอด: Direct Call vs Dynamic itab Dispatch

ลองเปรียบเทียบคำสั่งระดับ Assembly เมื่อเรียกเมธอดตรงๆ เทียบกับการเรียกผ่าน Interface:

// 1. Direct Concrete Call: s.Authorize(100)
// Compiler รู้ตำแหน่งแน่นอนตั้งแต่ต้น (Compile-Time Known Address)
CALL  main.(*PromptPayService).Authorize(SB) // คำสั่งตรงๆ 1 สเต็ป, ทำ Inlining ได้ทันที!

// 2. Dynamic Interface Call: gw.Authorize(100)
MOVQ  0(SP), AX       // โหลด *itab เข้าสู่ CPU Register AX
MOVQ  24(AX), AX      // อ่าน fun[0] (Function Pointer ของ Authorize) จาก itab
MOVQ  8(SP), DX       // โหลด data pointer (Receiver) เข้าสู่ Register DX
CALL  AX              // Indirect Call ข้าม Pointer! (ขัดขวาง Inlining และเสี่ยง Branch Misprediction)

สรุปผลกระทบต่อ Latency ในงาน High-Frequency Banking: การเรียกผ่าน Interface มีต้นทุน Indirect Call เพิ่มขึ้นเล็กน้อย (ราว 1-3 นาโนวินาที) แต่สิ่งที่สร้าง Overhead แท้จริงคือ Escape to Heap ซึ่งสร้างงานให้ Garbage Collector ต้องตามล้างขยะในภายหลัง

Section 4: Edge Cases, Hazards & Idiomatic Patterns

Hazard #1: Nil Interface vs Interface Holding Nil Value

นี่คือข้อผิดพลาดที่ร้ายแรงที่สุดของนักพัฒนาที่ย้ายมาจาก Spring Boot! ใน Go ตัวแปร Interface จะมีค่าเป็น nil ก็ต่อเมื่อ ทั้ง tab และ data เป็น nil ทั้งคู่!

type SecurityAuditor interface {
    Audit()
}

type BankAuditor struct{}
func (b *BankAuditor) Audit() {}

func GetAuditor() SecurityAuditor {
    var auditor *BankAuditor = nil // Concrete pointer มีค่าเป็น nil
    return auditor                 // ส่งกลับเป็น SecurityAuditor (iface)
}

func RunSystemCheck() {
    auditor := GetAuditor()

    // หายนะระดับ Production: เงื่อนไขนี้ได้ผลลัพธ์เป็น FALSE!
    // auditor != nil จะเป็น TRUE เสมอ เพราะ iface.tab มีค่าชี้ไปยัง Metadata ของ *BankAuditor!
    if auditor != nil {
        fmt.Println("Auditor exists, running audit...")
        // หากเมธอด Audit() พยายามเข้าถึงฟิลด์ภายใน จะเกิด NIL POINTER DEREFERENCE PANIC ทันที!
        auditor.Audit() 
    }
}

// วิธีแก้ที่ถูกต้อง:
func GetAuditorSafe() SecurityAuditor {
    // อย่าประกาศ Typed Pointer หากไม่มีค่า ให้คืน nil ตรงๆ!
    return nil // คืน Untyped nil ทำให้ iface.tab = nil และ iface.data = nil
}
Hazard #2: Compile-Time Interface Verification Trap

เนื่องจาก Go เป็น Implicit Interface หากเราเผลอแก้ Signature เมธอดใน Struct คอมไพเลอร์จะไม่เตือนตรงจุดที่ประกาศ Struct แต่จะไปพัง ณ บรรทัดที่ส่ง Struct เข้า Interface ซึ่งอาจอยู่ห่างออกไปหลายไฟล์

// Idiomatic Go Pattern: Guard การันตี Interface ตั้งแต่ขั้นตอน Compile-Time
// หาก PromptPayService ขาดเมธอดของ PaymentGateway บรรทัดนี้จะคอมไพล์ไม่ผ่านทันที!
var _ PaymentGateway = (*PromptPayService)(nil)

Idiomatic Go Interface Principles:

Section 5: Knowledge Verification Quiz (16 Challenges)

ทดสอบความเข้าใจเชิงลึกระดับ Runtime & Architecture ทุกข้อมีคะแนนสะสมและอธิบายเหตุผลอย่างละเอียด

Section 6: Hands-on Engineering Labs

เขียนโค้ด Go ตามหลัก Idiomatic Architecture ตรวจสอบความถูกต้องด้วย Verification Engine

Lab 1: Banking Ledger - Implicit Transaction Authorizer & Compile Guard

โจทย์: ประกาศ Interface Authorizer มีเมธอด AuthorizeTx(txID string, amt int64) bool จากนั้นสร้าง Struct CoreLedger ที่มีเมธอด AuthorizeTx ด้วย Pointer Receiver โดยคืนค่า amt > 0 พร้อมทั้งเขียนบรรทัด Compile-Time Interface Verification Guard เพื่อรับประกันว่า CoreLedger implement Authorizer

Lab 2: Race Timing - RFID Tag Packet Safe Type Assertion

โจทย์: สร้าง Struct MatPacket (มีฟิลด์ BibNumber int) และฟังก์ชัน ExtractBib(packet any) (int, bool) โดยใช้ Safe Type Assertion (Value, Ok idiom) เพื่อตรวจสอบว่า packet เป็น *MatPacket หรือไม่ หากใช่ ให้คืนค่า BibNumber, true หากไม่ใช่ ให้คืน 0, false

Lab 3: Polymorphic Payment Routing with Type Switch

โจทย์: สร้างฟังก์ชัน ClassifyPayment(p any) string โดยใช้ Type Switch ตรวจสอบ Concrete Type:
- หากเป็น string ให้คืนค่า "PromptPay QR"
- หากเป็น int64 ให้คืนค่า "Cash Settle"
- นอกเหนือจากนั้น (default) ให้คืนค่า "Unknown"