บทเรียน 10 · Day 4 · Interface

Interface: สัญญาที่ไม่ต้องเซ็นชื่อ

~15 นาที · ต้องมี: บทเรียน 07 (method & receiver), บทเรียน 09

ภาษาอื่นต้องเขียน implements Notifier ประกาศตัวว่าทำตามสัญญา Go ไม่ต้องประกาศอะไรเลย — ถ้า type คุณมี method ครบตามที่ interface ขอ ก็ถือว่าใช้ได้ทันที ฟังดูหลวม แต่มันคือสิ่งที่ทำให้เปลี่ยน PayPal เป็น GooglePay ได้โดยแก้โค้ดบรรทัดเดียว

1 · interface คือรายชื่อ method เฉย ๆ

type Notifier interface {
    Notify(message string) error
}

อ่านว่า: "อะไรก็ตามที่มี method ชื่อ Notify รับ string คืน error — ถือว่าเป็น Notifier" ไม่มีการบอกว่าข้างในทำงานยังไง มีแค่รูปร่างของ method

🔌 เทียบให้เห็นภาพ — เต้ารับไฟบ้าน

เต้ารับบนกำแพงไม่รู้จักพัดลม ไม่รู้จักที่ชาร์จมือถือ มันสนใจอย่างเดียวคือ “หัวปลั๊กที่ยื่นมามีรูปร่างตรงกับรูไหม” และไม่มีใครต้องไปแจ้งการไฟฟ้าก่อนว่าพัดลมตัวนี้ขอขึ้นทะเบียนเป็นของที่เสียบเต้ารับได้ — รูปร่างตรง ก็เสียบได้เลย

Go คิดแบบเดียวกัน: interface = รูปร่างของรูเต้ารับ · method = ขาปลั๊กแต่ละขา · ครบขาเมื่อไหร่ก็เสียบได้ทันที

ที่ไม่เหมือนกัน: เต้ารับรู้ตอนเราเสียบจริง แต่ Go ตรวจให้ตั้งแต่ตอน compile · และที่ต้องระวังคือทั้งคู่ตรวจแค่ “รูปร่าง” — ปลั๊กเสียบเข้าไม่ได้แปลว่าพัดลมจะหมุน เหมือนที่ method ครบตาม interface ไม่ได้แปลว่าข้างในเขียนถูก

ต่างจากภาษาอื่นตรงไหน

ภาษาอื่น (explicit)

class EmailNotifier
    implements Notifier {
    // ...
}

Go (implicit)

type EmailNotifier struct{}

func (EmailNotifier) Notify(
    message string,
) error {
    return nil
}

ฝั่ง Go ไม่มีคำว่า Notifier โผล่มาเลยสักที่ แต่ EmailNotifier ก็เป็น Notifier เรียบร้อยแล้ว เรื่องนี้เรียกว่า structural typing หรือชื่อเล่นที่ติดปากกว่าคือ…

2 · Duck Typing

"If it walks like a duck and it quacks like a duck, then it must be a duck"

type DuckLike interface {
    Walk()
    Quack()
}

type RealDuck struct{}
func (RealDuck) Walk()  { fmt.Println("Duck is walking") }
func (RealDuck) Quack() { fmt.Println("Quack!") }

type ToyDuck struct{}
func (ToyDuck) Walk()  { fmt.Println("Toy duck moves forward") }
func (ToyDuck) Quack() { fmt.Println("Electronic quack!") }

func ObserveDuck(d DuckLike) {
    d.Walk()
    d.Quack()
}

ObserveDuck(RealDuck{})   // Duck is walking / Quack!
ObserveDuck(ToyDuck{})    // Toy duck moves forward / Electronic quack!

ObserveDuck ไม่รู้จัก ToyDuck เลย และ ToyDuck ก็ไม่เคยประกาศว่าเป็น DuckLike — compiler (ตัวแปลโค้ดของเราให้เป็นโปรแกรมที่เครื่องรันได้) จับคู่ให้เองตอนที่คุณส่งเข้าไป

🗣 ศัพท์ที่เพิ่งเรียนไป

ใช้คำให้ตรง แล้วคำตอบที่ได้จาก senior หรือจาก AI จะตรงขึ้นทันที

implement (อิมพลีเมนต์)
การที่ type หนึ่งมี method ครบตามที่ interface ขอ · พูดว่า “EmailNotifier implement Notifier แล้ว” แทน “มันใช้กับ Notifier ได้”
method signature
“รูปร่างของ method” = ชื่อ + ชนิดที่รับเข้า + ชนิดที่คืนออก ต้องตรงครบทั้งสาม · เวลาโค้ดไม่ผ่านให้พูดว่า “signature ไม่ตรง” แทน “มันไม่ยอมให้ใส่”
structural typing
ตัดสินจากรูปร่างว่ามี method อะไรบ้าง ไม่ใช่จากชื่อที่ประกาศไว้ · ของ Go เป็นแบบ static คือตรวจให้ครบตั้งแต่ตอน compile · ประโยคที่ใช้ได้เลย: “Go ใช้ interface แบบ implicit (ไม่ต้องเขียนบอก) เพราะเป็น structural typing”
duck typing
คำที่ติดมาจากภาษาอย่าง Python/Ruby ซึ่งเรียก method ไปก่อน ถ้าไม่มีค่อยพังตอนรัน · คนเรียก Go ว่า duck typing กันบ่อยเพราะไอเดีย “ดูรูปร่าง” เหมือนกัน แต่จังหวะตรวจคนละจังหวะ · ถ้าจะพูดให้ตรงเป๊ะกับ senior ให้ใช้คำว่า structural typing แบบ static

3 · ทำไมต้องมี — ปัญหาของโค้ดที่ผูกติดตายตัว

// สมมติ Checkout Service ผูกกับ Paypal โดยตรง
type CheckoutService struct {
    paypal PaypalGateway
}

func (s CheckoutService) Checkout(amount int64) error {
    return s.paypal.Charge(amount)
}

โค้ดนี้ทำงานได้ แต่พังทันทีเมื่อ:

🏪 เทียบให้เห็นภาพ — คู่มือแคชเชียร์ร้านสะดวกซื้อ

แคชเชียร์ทำงานเดิมทุกวัน: สแกนของ → บอกยอด → รับชำระ
ถ้าคู่มือเขียนว่า “รูดบัตรกับเครื่องของ PayPal ที่วางอยู่ซ้ายมือ” วันที่ร้านเปลี่ยนเจ้า ต้องพิมพ์คู่มือใหม่ทั้งเล่ม
แต่ถ้าเขียนว่า “รับชำระด้วยเครื่องที่วางอยู่ตรงช่องนี้” ก็แค่ยกเครื่องเก่าออก วางเครื่องใหม่ลงช่องเดิม — คู่มือไม่ต้องแก้สักบรรทัด และตอนเทรนพนักงานใหม่ ก็วางเครื่องปลอมที่ไม่ตัดเงินจริงลงช่องเดิมได้ (= ตอนเขียน test)
PaymentGateway คือช่องวางเครื่อง · PaypalGateway คือเครื่องยี่ห้อหนึ่ง · CheckoutService คือคู่มือที่ไม่ต้องรู้ยี่ห้อเลย

ที่ไม่เหมือนกัน: แคชเชียร์เงยหน้ามาก็เห็นว่าเครื่องยี่ห้ออะไร แต่ CheckoutService มองไม่เห็นอะไรนอกจาก method ที่ interface ระบุไว้ ต่อให้ PaypalGateway มี method อื่นอีกสิบตัวก็เรียกไม่ได้ (จะแอบดูของข้างในต้องใช้ type assertion ใน บทเรียน 11)

เขียนใหม่ด้วย interface

// เริ่มจากกำหนด Contract กลาง:
type PaymentGateway interface {
    Pay(amount int64) error
}

// Checkout Service สนใจแค่ว่า "จ่ายเงินได้" ไม่สนว่าเจ้าไหน
type CheckoutService struct {
    gateway PaymentGateway
}

func NewCheckoutService(gateway PaymentGateway) *CheckoutService {
    return &CheckoutService{gateway: gateway}
}

func (s *CheckoutService) Checkout(amount int64) error {
    return s.gateway.Pay(amount)
}
// Implementation แต่ละเจ้า อยู่คนละไฟล์ก็ได้:
type PaypalGateway struct{}
func (PaypalGateway) Pay(amount int64) error {
    fmt.Printf("Paid %d satang via Paypal\n", amount)
    return nil
}

type GooglePayGateway struct{}
func (GooglePayGateway) Pay(amount int64) error {
    fmt.Printf("Paid %d satang via GPay\n", amount)
    return nil
}

// สลับเจ้าได้ที่จุดเดียว — business logic ไม่ต้องแก้เลย
service := NewCheckoutService(PaypalGateway{})
service = NewCheckoutService(GooglePayGateway{})
สิ่งที่เพิ่งได้มาฟรี ๆ

ตอนเขียน test คุณสร้าง type FakeGateway struct{} ที่มี Pay() คืน nil เฉย ๆ แล้วยัดเข้าไป — test รันได้โดยไม่ต่อเน็ต ไม่เสียเงิน ไม่ต้องรอ API นี่คือเหตุผลอันดับหนึ่งที่โค้ด Go ในโปรดักชันใช้ interface

🗣 ศัพท์ที่เพิ่งเรียนไป
dependency injection (DI)
dependency = ของที่โค้ดเราต้องพึ่ง (ที่นี่คือ gateway จ่ายเงิน) · DI = ส่งมันเข้าไปทางพารามิเตอร์ แทนที่จะให้โค้ดสร้างเองข้างใน · NewCheckoutService(gateway) คือ DI แบบง่ายที่สุด · “inject gateway เข้าไปตอนสร้าง service เลยดีกว่าไหม”
coupling / decouple
ผูกกันแน่นจนแยกไม่ออก / ทำให้ผูกกันหลวมลง · ประโยคที่ reviewer พูดจริง: “ตรงนี้ coupling กับ PayPal แน่นไป แยกเป็น interface ได้ไหม”
implementation (impl)
“ตัวจริงที่ทำงานได้” ที่อยู่หลัง interface · “Paypal เป็น implementation หนึ่งของ PaymentGateway”
stub
ของปลอมที่คืนค่าที่เราเซ็ตไว้เฉย ๆ ไม่ตรวจอะไรทั้งนั้น · FakeGateway ข้างบนที่ Pay() คืน nil เฉย ๆ จริง ๆ แล้วคือ stub (ชื่อขึ้นต้นว่า Fake เป็นแค่ธรรมเนียมตั้งชื่อ)
mock
ของปลอมที่คอยจำว่าถูกเรียกจริงไหม กี่ครั้ง ด้วยค่าอะไร แล้วให้เทสเช็กได้ · “mock ไว้ด้วยว่า Pay ต้องถูกเรียกครั้งเดียวนะ”
fake
ของจริงย่อส่วนที่ทำงานได้จริง เช่น DB ที่เก็บใน memory แทนการต่อ Postgres จริง

สามคำนี้วงการแยกกันจริง ๆ — ถ้า reviewer บอกว่า “อันนี้ควรเป็น stub ไม่ใช่ mock” เขากำลังบอกว่า เทสนี้ไม่ควรไปผูกกับจำนวนครั้งที่ถูกเรียก แค่ให้ค่ากลับมาก็พอ

4 · กฎที่ทำให้ interface ไม่พัง: เล็กเข้าไว้

อย่าสร้าง interface ตามทุกสิ่งที่ type ทำได้ ให้สร้างตามสิ่งที่ผู้เรียกจำเป็นต้องใช้

❌ ก้อนใหญ่

type UserService interface {
    CreateUser()
    UpdateUser()
    DeleteUser()
    FindUser()
    Login()
    Logout()
    ResetPassword()
    SendEmail()
    ExportReport()
    GenerateInvoice()
}

✅ แยกเล็ก ๆ

type UserFinder interface {
    FindUser(id string) (User, error)
}

type UserCreator interface {
    CreateUser(user User) error
}

type Authenticator interface {
    Login(username, password string) error
}

ปัญหาของก้อนใหญ่: ฟังก์ชันที่แค่อยากอ่านข้อมูล user ต้องรับ interface ที่มี DeleteUser() ติดมาด้วย เขียน fake สำหรับ test ทีต้อง implement 10 method ทั้งที่ใช้จริงอันเดียว และผู้อ่านโค้ดก็เดาไม่ออกว่าฟังก์ชันนี้จะไปทำอะไรกับ user บ้าง

🔑 เทียบให้เห็นภาพ — กุญแจที่ยื่นให้ช่างที่มาเปลี่ยนหลอดไฟ

ยื่นกุญแจดอกเดียวของห้องนั้น คนเห็นก็รู้ทันทีว่างานวันนี้แตะแค่ห้องเดียว ต่างจากยื่นพวงกุญแจทั้งตึกที่ใครก็เดาไม่ออกว่าช่างจะไปทำอะไรบ้าง
UserFinder คือกุญแจดอกเดียว · UserService ก้อนใหญ่คือพวงทั้งตึก

ที่ไม่เหมือนกัน: ช่างที่ได้กุญแจดอกเดียวเข้าห้องอื่นไม่ได้จริง ๆ แต่ interface เล็กไม่ได้กันใครทำอะไร — ฟังก์ชันที่รับ UserFinder ยัง type assert (บทเรียน 11) กลับไปเรียก DeleteUser() ได้ ถ้าของที่ส่งมามี method นั้นติดตัวอยู่ · interface เล็กจึงเป็นเรื่องสื่อสารเจตนาและเขียนเทสง่าย ไม่ใช่การกันสิทธิ์

สำนวนที่ควรจำ

"Accept interfaces, return structs" — พารามิเตอร์รับเป็น interface (ยืดหยุ่น ใครส่งอะไรมาก็ได้) แต่ค่าที่คืนเป็น struct จริง ๆ (ผู้เรียกได้ของครบ ไม่ถูกจำกัด) สังเกต NewCheckoutService ข้างบน: รับ PaymentGateway แต่คืน *CheckoutService

🗣 ศัพท์ที่เพิ่งเรียนไป
interface pollution
อาการ “สร้าง interface พร่ำเพรื่อ” ทั้งที่มีเจ้าเดียวและไม่ได้ใช้ประโยชน์อะไร · reviewer Go พูดกันจริง: “อันนี้ interface pollution หรือเปล่า”
ชื่อลงท้ายด้วย -er
ธรรมเนียมตั้งชื่อ interface ของ Go — ตั้งตามสิ่งที่มันทำ → Reader · UserFinder · ถ้าตั้งชื่อไม่ลงตัว มักแปลว่า interface ยังใหญ่ไป

5 · กับดักใหญ่: pointer receiver กับ interface

type Notifier interface {
    Notify(message string) error
}

type EmailNotifier struct{}

// สังเกต: pointer receiver
func (e *EmailNotifier) Notify(message string) error {
    return nil
}

var n Notifier = EmailNotifier{}

บรรทัดสุดท้าย compile ผ่านไหม?

ไม่ผ่าน — ผมรันทดสอบแล้ว:

cannot use EmailNotifier{} (value of struct type EmailNotifier)
    as Notifier value in variable declaration:
    EmailNotifier does not implement Notifier
    (method Notify has pointer receiver)

กฎ:

  • func (e *EmailNotifier) Notify() → เฉพาะ *EmailNotifier ที่เป็น Notifier
  • func (e EmailNotifier) Notify() → ทั้ง EmailNotifier และ *EmailNotifier ใช้ได้

ทำไมถึงไม่สมมาตร — และทำไมมันขัดกับบทเรียน 07:

ตอนเรียก method ตรง ๆ อย่าง e.Notify("hi") compiler เติม & ให้เองได้ เพราะมันรู้ที่อยู่ของ e (e เป็นตัวแปรจริงที่มีที่อยู่)
แต่ตอนยัดค่าใส่ interface Go จะก็อปค่านั้นเข้าไปเก็บในกล่อง interface — ของที่อยู่ในกล่องไม่ใช่ตัวแปรของใคร ไม่มีที่อยู่ให้เอา & ไปชี้ ถ้า Go ยอมให้ผ่าน method ที่ต้องแก้ค่าจะไปแก้ "ก๊อปปี้ในกล่อง" ซึ่งไม่มีผลกับตัวแปรข้างนอกเลย — เป็นบั๊กเงียบแบบเดียวกับ value receiver ใน บทเรียน 07
Go จึงเลือกให้ compile ไม่ผ่านไปเลย ดีกว่าปล่อยให้แก้ค่าแล้วหาย ส่วนทางกลับกัน (value receiver + *T) ทำได้เสมอ เพราะจาก pointer ย้อนไปเอาค่ามาก็อปได้ตลอด

แก้ได้โดยเขียน var n Notifier = &EmailNotifier{} (ใส่ &) — นี่คือ error ที่คนเริ่มเขียน Go เจอบ่อยที่สุด อันหนึ่ง เห็นคำว่า "has pointer receiver" เมื่อไหร่ ให้มองหา & ที่หายไป

🪪 เทียบให้เห็นภาพ — บัตรตัวจริงกับสำเนา

งานบางอย่างที่อำเภอ ต้องใช้บัตรตัวจริงเท่านั้น เพราะเจ้าหน้าที่ต้องไปแก้ข้อมูลในทะเบียน ถ้าเรายื่นแต่สำเนา เขาจะปฏิเสธตั้งแต่ที่เคาน์เตอร์ — เพราะถ้ารับไว้แล้วไปแก้บนสำเนา ตัวจริงจะไม่เปลี่ยนเลย และเราจะกลับบ้านโดยคิดว่าเรื่องเสร็จแล้ว

การเอา ค่า ใส่ interface คือการยื่นสำเนา (Go ก็อปค่านั้นไปเก็บในกล่อง interface) · method ที่เขียนเป็น *EmailNotifier คืองานที่ต้องใช้ตัวจริง · การใส่ & คือการยื่นตัวจริงให้

ที่ไม่เหมือนกัน: ที่อำเภอปฏิเสธตอนเรายื่นเอกสาร แต่ Go ปฏิเสธ ตั้งแต่ตอน compile คือก่อนโปรแกรมเริ่มทำงานด้วยซ้ำ · และในสายตา Go EmailNotifier กับ *EmailNotifier เป็น type คนละตัวกันจริง ๆ ที่ถือ method ติดตัวไม่เท่ากัน
ที่สำคัญกว่านั้น: ที่อำเภอเขาดูว่างานนั้นต้องแก้ทะเบียนจริงไหม แต่ Go ไม่ได้อ่านข้างใน method เลย — Notify() ข้างบนแค่ return nil ไม่ได้แก้อะไรสักบรรทัด ก็ยัง compile ไม่ผ่านเหมือนเดิม เพราะ Go ดูอย่างเดียวว่าเขียน receiver เป็น *EmailNotifier หรือเปล่า · เรื่อง “แก้แล้วหาย” คือที่มาของกฎ ไม่ใช่เงื่อนไขที่ compiler ไล่ตรวจรายครั้ง

🗣 ศัพท์ที่เพิ่งเรียนไป
value receiver / pointer receiver
ตัวที่อยู่ในวงเล็บหน้าชื่อ method · (e EmailNotifier) = value receiver (ทำงานกับสำเนา) · (e *EmailNotifier) = pointer receiver (ทำงานกับตัวจริง) · ถามว่า “method นี้ใช้ receiver แบบไหน” ดีกว่าถามว่า “ตัวนี้มีดาวหรือเปล่า”
method set
“รายชื่อ method ที่ type หนึ่งถือติดตัวไปได้” — compiler ใช้รายชื่อนี้ตัดสินว่าครบตาม interface ไหม · “method set ของ EmailNotifier มี Notify ไหม”
addressable
“มีที่อยู่ให้เอา & ไปชี้ได้ไหม” · ตัวแปรทั่วไป addressable แต่ค่าที่ถูกก็อปเข้าไปอยู่ในกล่อง interface แล้วไม่ addressable — นี่คือรากของกฎทั้งข้อนี้
compile-time error
error ที่เจอตอนแปลโค้ด ยังไม่ทันได้รัน (ตรงข้ามกับ runtime error ที่พังตอนโปรแกรมทำงานอยู่) · “has pointer receiver” เป็น compile-time error — เห็นเมื่อไหร่ให้มองหา & ที่หาย

6 · ทดสอบความจำ

1. type ใน Go ประกาศว่าตัวเองทำตาม interface ยังไง?

Go ใช้ structural typing — compiler ดูแค่ว่า method ครบตาม signature ไหม ผลพลอยได้คือคุณเขียน interface มาครอบ type ของ library คนอื่นได้ โดยที่เขาไม่ต้องรู้เรื่องด้วยเลย

2. method ใช้ pointer receiver — เอา value ใส่ interface ได้ไหม?

"method set" คือรายชื่อ method ที่ type หนึ่ง ๆ ถือติดตัวไปได้ — compiler ใช้รายชื่อนี้ตัดสินว่า type นั้นครบตาม interface ไหม

  • EmailNotifier (ค่า) ถือได้เฉพาะ method ที่เขียน func (e EmailNotifier)
  • *EmailNotifier (pointer) ถือได้ทั้งสองแบบ — ทั้งที่เขียน func (e EmailNotifier) และ func (e *EmailNotifier)

พอ Notify เขียนเป็น func (e *EmailNotifier) มันจึงอยู่ในรายชื่อของ *EmailNotifier เท่านั้น ตัวค่าเปล่า ๆ จึงยังไม่เป็น Notifier ต้องใส่ &

3. ทำไม interface เล็ก ๆ ถึงดีกว่า interface ก้อนใหญ่?

ไม่เกี่ยวกับความเร็วหรือหน่วยความจำเลย และ Go ก็ไม่ได้บังคับ — มันเป็นเรื่องของการออกแบบ interface ที่เล็กทำให้ signature ของฟังก์ชันบอกความจริงว่ามันแตะอะไรบ้าง และเขียน fake สำหรับ test ได้ง่าย ดู io.Reader ใน standard library เป็นตัวอย่าง: มี method เดียว แต่ทั้งภาษาใช้มัน

4. สำนวน "Accept interfaces, return structs" แปลว่าอะไร?

พารามิเตอร์เป็น interface → ใครมี method ครบก็ส่งเข้ามาได้ ยืดหยุ่นสูงสุด · ค่าคืนเป็น struct → ผู้เรียกได้ field และ method ครบ ไม่ถูกบีบให้เห็นแค่บางส่วน ถ้าคืนเป็น interface ผู้เรียกจะเข้าถึง อะไรเพิ่มไม่ได้เลยโดยไม่ทำ type assertion

7 · ลงมือทำ

ภารกิจ: ระบบแจ้งเตือนที่เปลี่ยนช่องทางได้ (workshop จากสไลด์)

บริษัทต้องการระบบส่งข้อความแจ้งเตือน ส่งผ่าน Email หรือ LINE ก็ได้ — ใช้ interface ให้ฟังก์ชัน SendAlert ทำงานกับหลายช่องทางได้

  1. ประกาศ type Alerter interface { Send(message string) error }
  2. สร้าง EmailAlerter และ LineAlerter ที่ implement มัน — พิมพ์ "Email: ..." และ "LINE: ..." ตามลำดับ
  3. เขียน func SendAlert(a Alerter, message string) error
  4. เรียกให้ได้ผลลัพธ์:
    Email: Server CPU is high
    LINE: Database connection failed
  5. ท้าทาย 1: ทำให้ส่งพร้อมกันทุกช่องทางได้ — คำใบ้: func SendAll(alerters []Alerter, message string)
  6. ท้าทาย 2: เขียน FakeAlerter ที่เก็บ ข้อความไว้ใน slice แทนที่จะพิมพ์ แล้วใช้มันตรวจว่า SendAll ส่งครบทุกช่องทางจริงไหม — นี่คือรูปแบบของ unit test ที่คุณจะเขียนจริงในสัปดาห์หน้า
    รู้ได้ยังไงว่าถูก: หลังเรียก SendAll แล้ว len(fake.messages) ต้องเท่ากับจำนวนครั้งที่ส่ง
    ⚠️ ถ้าได้ 0 ทั้งที่ไม่มี error เลย — นั่นไม่ใช่บั๊กของ SendAll ย้อนไปดู บทเรียน 07: func (f FakeAlerter) Send(...) ได้ copy มา append ลงไปก็หายพร้อม copy ต้องใช้ func (f *FakeAlerter) Send(...) แล้วส่ง &FakeAlerter{} เข้า slice ของ Alerter
  7. ท้าทาย 3: ลองเปลี่ยน receiver ของ EmailAlerter.Send เป็น pointer (func (e *EmailAlerter) Send(...)) แล้วรันใหม่ — พังตรงไหน แล้วแก้ยังไง?

อ่านต่อจากต้นทาง

แหล่งหลักที่แนะนำ: A Tour of Go — Interfaces ถึงหน้า 14 — กดรันแก้โค้ดได้ในเว็บเลย ประมาณ 15 นาทีจบ พอเข้าใจแล้วค่อยอ่าน Go Code Review Comments — Interfaces ซึ่งบอกกฎที่ reviewer จริง ๆ ใช้ ข้อที่สำคัญที่สุดสรุปได้ว่า ให้ประกาศ interface ไว้ใน package ของฝั่งที่ใช้ ไม่ใช่ฝั่งที่ implement — ตรงข้ามกับสัญชาตญาณของคนมาจากภาษา OOP แต่มันคือสิ่งที่ทำให้ผู้ใช้กำหนดได้เองว่าต้องการ method อะไรบ้าง

💬 ติดตรงไหนถามได้เลย — ท้าทายข้อ 3 (pointer receiver) ถ้า error ยาวจนอ่านไม่ออกว่ามันบ่นเรื่องอะไร ส่งมาได้เลย มันเป็น error ที่คนเขียน Go ใหม่ ๆ เจอทุกคน

อ้างอิง