interface และการต่อกฎส่วนลด
orderctl คิดยอดได้แล้ว แต่ยอดที่คิดได้ยังเป็นยอดเต็มเสมอ
บทนี้เติมส่วนลด และส่วนลดคือที่ที่ทุกภาษาเอาเครื่องมือ abstraction ออกมาใช้ Go มีเครื่องมือนั้นตัวเดียว และมันทำงานกลับด้านกับที่คุณคุ้น
ไม่มีที่ให้ประกาศว่า implement
หัวข้อที่มีชื่อว่า “ไม่มีที่ให้ประกาศว่า implement”ชนิดหนึ่งเข้ากับ interface เมื่อมันมี methodครบ ไม่มีบรรทัดไหนให้เขียนความผูกพันนั้น
implicit interfaceimplicit interface satisfactionการที่ชนิดหนึ่งเข้ากับ interface โดยอัตโนมัติเมื่อมี method ครบ ไม่มีคำว่า `implements` ให้เขียน — interface จึงนิยามที่ฝั่งผู้ใช้ ไม่ใช่ฝั่งผู้สร้าง คือกฎข้อเดียวของเรื่องนี้ ถ้า PercentOff มี method Apply
ที่รูปร่างตรงกับที่ Rule ต้องการ มันก็เป็น Rule ทันที
ไม่มี : IRule ไม่มี implements Rule และไม่มี attribute ให้ติด
สเปกของภาษาตัดสินจาก methodที่มีอยู่จริงเท่านั้น
package main
import "fmt"
// Baht กับ Order ย่อมาจากบทที่ 2 เพื่อให้ block นี้รันได้เดี่ยวๆtype Baht int64
type Order struct{ Amounts []Baht }
func (o Order) Subtotal() Baht { var sum Baht for _, a := range o.Amounts { sum += a } return sum}
// Rule ย่อยยอดหนึ่งก้อนให้เหลือยอดใหม่ · เล็กที่สุดเท่าที่ผู้ใช้ต้องการtype Rule interface { Apply(subtotal Baht) Baht}
type PercentOff struct{ Percent int }
func (p PercentOff) Apply(subtotal Baht) Baht { return subtotal * Baht(100-p.Percent) / 100}
type FlatOff struct{ Amount Baht }
func (f FlatOff) Apply(subtotal Baht) Baht { if f.Amount >= subtotal { return 0 } return subtotal - f.Amount}
var ( _ Rule = PercentOff{} _ Rule = FlatOff{})
func Total(o Order, rules ...Rule) Baht { total := o.Subtotal() for _, r := range rules { total = r.Apply(total) } return total}
func main() { o := Order{Amounts: []Baht{120, 80}} fmt.Println(Total(o)) fmt.Println(Total(o, PercentOff{Percent: 10})) fmt.Println(Total(o, PercentOff{Percent: 10}, FlatOff{Amount: 30}))}$ go run .200180150บรรทัดที่หน้าตาแปลกที่สุดคือ _ Rule = PercentOff{} มันไม่ได้ทำงานตอนรัน
มันคือการขอให้คอมไพเลอร์ตรวจความผูกพันให้
เพราะการสะกดชื่อ methodผิดหนึ่งตัวยังคอมไพล์ผ่าน
ถ้าคุณพิมพ์ func (p PercentOff) Aply(...) ชนิดนั้นก็ยังเป็นชนิดที่ถูกต้อง
มันแค่ไม่ใช่ Rule อีกต่อไป และไม่มีใครรู้จนกว่าจะมีคนพยายามส่งมันเข้า Total
บรรทัด var _ Rule = PercentOff{} ย้ายความผิดพลาดนั้นมาไว้ที่ file ของผู้สร้าง
และทำให้มันแดงตอน build แทนที่จะแดงที่ call site ของคนอื่นในอีกสามสัปดาห์
interface เกิดที่ฝั่งผู้ใช้ ไม่ใช่ฝั่งผู้สร้าง
หัวข้อที่มีชื่อว่า “interface เกิดที่ฝั่งผู้ใช้ ไม่ใช่ฝั่งผู้สร้าง”เมื่อไม่มีบรรทัดผูกพันให้เขียน ฝั่งที่ประกาศ interface จึงย้ายไปอยู่ที่ผู้เรียกใช้ได้
ในภาษาที่ต้องประกาศ implements ฝั่งที่สร้างชนิดต้องรู้จัก interface ก่อน
ลำดับการพึ่งพาจึงชี้จากผู้สร้างไปหาสัญญา
Go กลับด้านนั้น internal/pricing ประกาศ Rule เพราะ pricing คือคนที่ใช้ ค่านั้น
ส่วน internal/order ไม่เคยรู้จัก Rule เลยสักครั้ง
wiki ของ Go เขียนกฎข้อนี้ไว้ที่ต้นหัวข้อ Interfaces เลย:
“Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types: that way, new methods can be added to implementations without requiring extensive refactoring.”
อีกย่อหน้าในหน้าเดียวกันเป็นคำสั่งห้ามพร้อมเหตุผล: “Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.”
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ — ในโปรเจกต์จริงมันคือ internal/pricing/pricing.gopackage pricing
import "example.com/orderctl/internal/order"
type Rule interface { Apply(subtotal order.Baht) order.Baht}
type PercentOff struct{ Percent int }
func (p PercentOff) Apply(subtotal order.Baht) order.Baht { return subtotal * order.Baht(100-p.Percent) / 100}
type FlatOff struct{ Amount order.Baht }
func (f FlatOff) Apply(subtotal order.Baht) order.Baht { if f.Amount > subtotal { return 0 } return subtotal - f.Amount}
// Counting ห่อ Rule ตัวอื่นไว้ข้างใน แทนการสืบทอดtype Counting struct { Rule Seen int}
func (c *Counting) Apply(subtotal order.Baht) order.Baht { c.Seen++ return c.Rule.Apply(subtotal)}
func Total(o order.Order, rules ...Rule) order.Baht { total := o.Subtotal() for _, r := range rules { total = r.Apply(total) } return total}
var ( _ Rule = PercentOff{} _ Rule = FlatOff{} _ Rule = (*Counting)(nil))block ที่รันได้ข้างบนรวมทุกอย่างไว้ใน file เดียวเพื่อให้คัดลอกไปรันได้ทันที ของจริงแยกเป็นสอง package ตามที่เห็นตรงนี้
ราคาที่จ่ายคือ interface ตัวเดียวกันอาจถูกประกาศซ้ำในหลาย package Go ยอมแลกข้อนั้นกับการที่ package ระดับล่างไม่ต้องพึ่งใครเลย
method set ตัดสินว่าใครเข้ากับ interface ได้
หัวข้อที่มีชื่อว่า “method set ตัดสินว่าใครเข้ากับ interface ได้”ตัวรับแบบ value กับตัวรับแบบ pointer ให้คำตอบคนละแบบ และนี่คือกับดักที่เจอบ่อยที่สุด
method setmethod setชุด method ที่ผูกกับชนิดหนึ่ง — method set ของ `T` มีเฉพาะ method ที่รับ `T` ส่วนของ `*T` มีทั้งสองแบบ ของชนิด T มีเฉพาะ methodที่ประกาศตัวรับเป็น T
ส่วน method set ของ *T มีทั้ง methodที่รับ T และ methodที่รับ *T
ผลคือ interface ที่ต้องการ methodซึ่งประกาศตัวรับเป็น pointer รับได้เฉพาะรูป pointer ส่วน interface ที่ต้องการเฉพาะ methodตัวรับแบบ value รับได้ทั้งสองรูป
package main
import "fmt"
type Baht int64
type Order struct { Total Baht Paid bool}
func (o Order) Summary() string { return fmt.Sprintf("ยอด %d บาท", o.Total) }
func (o *Order) MarkPaid() { o.Paid = true }
type Payer interface{ MarkPaid() }
func main() { o := Order{Total: 150}
o.MarkPaid() // ผ่าน: คอมไพเลอร์เขียน (&o).MarkPaid() ให้ เพราะ o มีที่อยู่ fmt.Println(o.Summary(), o.Paid)
var p Payer = &o // ผ่าน: method set ของ *Order มี MarkPaid อยู่ p.MarkPaid()
// var bad Payer = o // cannot use o (variable of struct type Order) as Payer value in variable declaration: // Order does not implement Payer (method MarkPaid has pointer receiver)}บรรทัด o.MarkPaid() ผ่าน แต่ var bad Payer = o ไม่ผ่าน สองบรรทัดนี้ดูขัดกันเอง
มันไม่ขัดกัน เพราะการเรียก methodตรงๆ คอมไพเลอร์เติม & ให้ได้เมื่อค่านั้นมีที่อยู่
ส่วนการยัดค่าเข้า interface คือการคัดลอกค่าไปเก็บ ค่าที่คัดลอกแล้วไม่มีที่อยู่ให้เติม &
flowchart TD
A["ชนิด PercentOff<br/>method Apply ตัวรับเป็น value"] --> Q{"ค่าที่จะยัดเข้า Rule<br/>มี methodครบไหม"}
B["ชนิด Counting<br/>method Apply ตัวรับเป็น pointer"] --> Q
Q -- "PercentOff{} ครบ" --> R1["ใช้ได้ทั้ง PercentOff และ *PercentOff"]
Q -- "Counting{} ไม่ครบ" --> R2["ใช้ได้เฉพาะ *Counting"]
R2 --> N["method set ของ value<br/>ไม่มี methodตัวรับ pointer อยู่ข้างใน"]
คำบรรยายภาพ: ชนิดที่มี methodตัวรับเป็น value ใช้ได้ทั้งรูป value และรูป pointer · ชนิดที่มี methodตัวรับเป็น pointer ใช้ได้เฉพาะรูป pointer เพราะ method set ของค่าแบบ value ไม่มี methodนั้นอยู่
ถ้าคุณมาจาก TypeScript คุณจะคิดว่า structural typing เหมือนกัน จึงคาดว่า struct ที่มี field ตรงกับรูปร่างของ interface ก็เข้ากันได้
Go เทียบ method set อย่างเดียว interface ของ Go บรรจุ field ไม่ได้เลย และตัวรับแบบ pointer เปลี่ยนคำตอบทั้งที่ field เหมือนเดิมทุกตัว
ถามชนิดจริงข้างใน comma-ok กับ type switch
หัวข้อที่มีชื่อว่า “ถามชนิดจริงข้างใน comma-ok กับ type switch”ค่าที่อยู่ใน interface ถามกลับได้ และรูปที่ถามมีสองรูป ปลอดภัยรูปเดียว
v.(T) แบบเดี่ยวคืนค่าตัวเดียว และ panic ทันทีที่ชนิดข้างในไม่ใช่ T
t, ok := v.(T) คืนสองค่า และไม่ panic เลย
เมื่อต้องแยกหลายชนิด switch v := r.(type) อ่านง่ายกว่าการเขียน comma-ok ซ้อนกัน
package main
import "fmt"
type Baht int64
type Rule interface { Apply(subtotal Baht) Baht}
type PercentOff struct{ Percent int }
func (p PercentOff) Apply(subtotal Baht) Baht { return subtotal * Baht(100-p.Percent) / 100 }
type FlatOff struct{ Amount Baht }
func (f FlatOff) Apply(subtotal Baht) Baht { if f.Amount >= subtotal { return 0 } return subtotal - f.Amount}
func Describe(r Rule) string { switch v := r.(type) { case PercentOff: return fmt.Sprintf("ลด %d เปอร์เซ็นต์", v.Percent) case FlatOff: return fmt.Sprintf("ลด %d บาท", v.Amount) default: return "กฎที่ยังไม่รู้จัก" }}
func main() { rules := []Rule{PercentOff{Percent: 10}, FlatOff{Amount: 30}} for _, r := range rules { fmt.Println(Describe(r)) }
// รูป comma-ok ไม่ panic เมื่อชนิดไม่ตรง มันคืน ok เป็น false if p, ok := rules[1].(PercentOff); ok { fmt.Println(p.Percent) } else { fmt.Println("ตัวที่สองไม่ใช่ PercentOff") }
// รูปเดี่ยว rules[1].(PercentOff) จะ panic ทันทีที่ชนิดไม่ตรง}as ของ C# คืน null เมื่อชนิดไม่ตรง จึงปลอดภัยเสมอ
v.(T) ของ Go หน้าตาคล้ายกัน แต่มันคือรูปที่ panic ตัวที่ปลอดภัยคือรูปสองค่า
t, ok := v.(T) เท่านั้น
ตั้งชื่อใหม่ให้ค่าที่ได้ด้วย ถ้าใช้ชื่อเดิมในขอบเขตเดียวกัน เช่น v, ok := v.(T)
v จะยังเป็นชนิด interface เหมือนเดิม เพราะ := แค่กำหนดค่าทับตัวเดิม ไม่ได้ประกาศตัวใหม่
กฎง่ายที่ใช้ได้ทุกครั้ง คือเขียนรูปสองค่าเสมอ ยกเว้นตอนที่ชนิดไม่ตรงแปลว่าโปรแกรมพังไปแล้วจริงๆ
การฝังไม่ใช่การสืบทอด
หัวข้อที่มีชื่อว่า “การฝังไม่ใช่การสืบทอด”method ที่ชื่อซ้ำกันไม่ override ของเดิม เพราะไม่มี virtual dispatch ให้ override
embeddingembeddingการใส่ชนิดอื่นเป็น field ที่ไม่มีชื่อ ทำให้ method ถูกส่งต่อให้อัตโนมัติ แต่ไม่ใช่การสืบทอด ไม่มี virtual dispatch และ override ไม่ได้ คือการใส่ชนิดอื่นเป็น field ที่ไม่มีชื่อ แล้วคอมไพเลอร์เขียน methodส่งต่อให้ มันคือ method forwarding ไม่ใช่ตาราง methodที่ผูกตอนรัน
ผลที่ตามมาคือ methodของชนิดข้างใน เรียก methodของชนิดข้างในเสมอ แม้ชนิดข้างนอกจะมีชื่อซ้ำ
package main
import "fmt"
type Printer struct{ Prefix string }
func (p Printer) Line() string { return p.Prefix + ":base" }func (p Printer) Render() string { return "<" + p.Line() + ">" }
type Fancy struct{ Printer }
func (f Fancy) Line() string { return f.Prefix + ":fancy" }
func main() { f := Fancy{Printer{Prefix: "p"}} fmt.Println(f.Line()) fmt.Println(f.Render())
// var b Printer = f // cannot use f (variable of struct type Fancy) as Printer value in variable declaration}$ go run .p:fancy<p:base>บรรทัดที่สองคือจุดที่คนมาจาก C# หรือ Java สะดุด พวกเขาคาด <p:fancy>
เพราะในภาษาเหล่านั้น Fancy.Line จะ override และ Render จะเรียกตัวที่ override
Printer.Render ของ Go ถูกแปลไว้แล้วว่าเรียก Printer.Line
มันไม่เคยรู้ว่ามีชนิดชื่อ Fancy อยู่บนโลก
FAQ ของ Go เขียนเหตุผลไว้ประโยคเดียว: “Because there are no explicit relationships between types and interfaces, there is no type hierarchy to manage or discuss.”
บรรทัด var b Printer = f ที่ปิดคอมเมนต์ไว้ ยืนยันข้อเดียวกันจากอีกด้าน
Fancy ไม่ใช่ Printer มันแค่มี Printer อยู่ข้างใน
ห่อ Rule ด้วย Rule แทน template method
หัวข้อที่มีชื่อว่า “ห่อ Rule ด้วย Rule แทน template method”ท่าที่ใช้แทนการสืบทอดคือฝัง interface ไว้ข้างใน แล้วเขียนทับเฉพาะ methodที่อยากเปลี่ยน
เมื่อไม่มี template method ให้ใช้ ท่าที่เหลือคือการห่อ ชนิดข้างนอกรับ Rule เข้ามาเก็บไว้
ทำงานของตัวเอง แล้วส่งต่อให้ตัวข้างใน
การฝังส่งต่อ methodที่คุณไม่เขียนทับให้เองทั้งสองแบบ ต่างกันที่ปลายทางของการส่งต่อ
การฝัง struct ผูกปลายทางไว้ตั้งแต่ตอนแปล ส่วนการฝัง interface เลือกปลายทางตอนรัน
จากค่าที่ถูกใส่เข้ามา Counting จึงห่อ Rule ตัวไหนก็ได้ และเขียนแค่ methodที่อยากเปลี่ยน
package main
import "fmt"
type Baht int64
type Rule interface { Apply(subtotal Baht) Baht}
type PercentOff struct{ Percent int }
func (p PercentOff) Apply(subtotal Baht) Baht { return subtotal * Baht(100-p.Percent) / 100}
// Counting ห่อ Rule ตัวอื่นไว้ข้างใน แทนการสืบทอดtype Counting struct { Rule // ฝัง interface: method ที่ไม่เขียนทับ ถูกส่งต่อให้ตัวข้างในเอง Seen int}
func (c *Counting) Apply(subtotal Baht) Baht { c.Seen++ return c.Rule.Apply(subtotal)}
// เฉพาะ *Counting เท่านั้นที่เข้ากับ Rule เพราะ Apply มีตัวรับเป็น pointervar _ Rule = (*Counting)(nil)
func main() { c := &Counting{Rule: PercentOff{Percent: 10}} fmt.Println(c.Apply(200)) fmt.Println(c.Apply(100)) fmt.Println(c.Seen)}$ go run .180902c.Rule.Apply(subtotal) คือคู่ของ base.Apply(...) ในภาษาที่มีการสืบทอด
ต่างกันตรงที่เส้นทางนั้นเขียนไว้ให้เห็นด้วยตา ไม่ได้ซ่อนอยู่ในกฎการ dispatch
สังเกตบรรทัด var _ Rule = (*Counting)(nil) ว่ามันตรวจ *Counting ไม่ใช่ Counting{}
เพราะ Apply ของ Counting มีตัวรับเป็น pointer ตามกฎ method set ของหัวข้อที่แล้ว
Go ไม่มี variance เลย
หัวข้อที่มีชื่อว่า “Go ไม่มี variance เลย”
[]PercentOffส่งเข้าที่ที่ต้องการ[]Ruleไม่ได้ ต้องคัดลอกทีละตัว
Java ส่ง List<Order> เข้า List<? extends Object> ได้ C# มี out และ in ให้ประกาศ
Go ไม่มีทั้งสองอย่าง
PercentOff เข้ากับ Rule ได้ แต่ []PercentOff กับ []Rule เป็นสองชนิดที่ไม่เกี่ยวกันเลย
เพราะสอง slice นั้นวางข้อมูลในหน่วยความจำคนละแบบ
package main
import "fmt"
type Baht int64
type Rule interface{ Apply(subtotal Baht) Baht }
type PercentOff struct{ Percent int }
func (p PercentOff) Apply(subtotal Baht) Baht { return subtotal * Baht(100-p.Percent) / 100 }
func main() { offers := []PercentOff{{Percent: 10}, {Percent: 5}}
// var rules []Rule = offers // cannot use offers (variable of type []PercentOff) as []Rule value in variable declaration
rules := make([]Rule, 0, len(offers)) for _, o := range offers { rules = append(rules, o) } fmt.Println(len(rules))}ลูปสามบรรทัดนั้นคือคำตอบเดียวที่มี และมันไม่ใช่ของฟรี มันคัดลอกทุกตัวเข้ากล่อง interface ใหม่ ซึ่งเป็นราคาที่ Go เลือกจ่ายอย่างเปิดเผย
เมื่อไรควรใช้ generics แทน interface
หัวข้อที่มีชื่อว่า “เมื่อไรควรใช้ generics แทน interface”เส้นแบ่งอยู่ที่ว่าคุณต้องการเรียก method หรือต้องการเขียน code ชุดเดิมซ้ำ
Ian Lance Taylor เขียนเส้นแบ่งข้อแรกไว้ตรงตัว: “If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter.”
Rule ของบทนี้ตกอยู่ในข้อนั้นพอดี สิ่งเดียวที่ Total ทำกับค่าที่รับมา คือเรียก Apply
generics จึงไม่ช่วยอะไรเลย
เขาเขียนเส้นแบ่งข้อที่สองไว้เป็นสัญญาณให้รอ: “Another way to say this is that you should avoid type parameters until you notice that you are about to write the exact same code multiple times.”
Go proverb ข้อที่คู่กันสรุปอีกด้านของเรื่องเดียวกัน คือ “The bigger the interface, the weaker the abstraction.”
ก่อน Go 1.27 method ประกาศ type parameter ของตัวเองไม่ได้เลย ตั้งแต่ 1.27 ทำได้แล้ว
แต่มันไม่ได้ลบข้อจำกัดทั้งหมด release note ของ 1.27 เขียนไว้ตรงตัวว่า “Note that methods of interfaces may not declare type parameters nor can interface methods be implemented by generic methods.” นั่นคือสองข้อนี้ยังอยู่:
- method ที่ประกาศไว้ใน interface ยังมี type parameter ของตัวเองไม่ได้
- generic method ใช้ implement method ของ interface ไม่ได้
คอร์สนี้ตั้งขั้นต่ำไว้ที่ go 1.25.0 ของในกล่องนี้จึงยังใช้ไม่ได้ในเนื้อหาหลัก
คอมไพเลอร์ตอบว่า generic method requires go1.27 or later (-lang was set to go1.25; check go.mod)
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”
pricing.Ruleเสร็จแล้ว และคุณเห็นครบสามข้อที่ interface ของ Go ทำงานกลับด้านกับที่คาด
คุณออกแบบ interface method เดียว ต่อกฎส่วนลดเข้าด้วยกันด้วย Total
และห่อกฎด้วย Counting โดยไม่ใช้การสืบทอดเลยสักครั้ง
สามข้อที่ควรติดตัวไปคือ interface เกิดที่ฝั่งผู้ใช้ ตัวรับแบบ pointer เปลี่ยนคำตอบของ method set และการฝังคือการส่งต่อ method ไม่ใช่การ override
บทหน้าเป็นเรื่อง error ที่เป็นค่า ไม่ใช่ข้อยกเว้น และเราจะพบว่า error เองก็เป็น interface
ที่มี methodเดียว ซึ่งทำให้ทุกอย่างในบทนี้ใช้ซ้ำได้ทันที
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- The Go Programming Language Specification — Implementing an interface (เข้าถึง 2026-05-26) — นิยามของการ implement ตัดสินด้วย type set ไม่มีการประกาศ
- The Go Programming Language Specification — Method sets (เข้าถึง 2026-05-26) — method set ของ
Tกับของ*Tต่างกันตรงไหน - Go Code Review Comments — Interfaces (เข้าถึง 2026-08-27) — interface อยู่ใน package ที่ใช้ค่านั้น ฝั่งที่ implement คืน concrete type และห้ามนิยาม interface ก่อนมีคนใช้
- Go FAQ — Why is there no type inheritance? (เข้าถึง 2026-08-27) — เพราะไม่มีความสัมพันธ์ที่ประกาศไว้ระหว่างชนิดกับ interface จึงไม่มีลำดับชั้นของชนิดให้ดูแล
- Go 1.27 Release Notes — Changes to the language (เข้าถึง 2026-08-27) — generic method เข้ามาที่ 1.27 พร้อมข้อจำกัดสองข้อที่ยกมาในกล่อง
- When To Use Generics (Ian Lance Taylor, 2022-04-12) — เส้นแบ่งระหว่าง interface กับ type parameter
- Go Proverbs (2015-11-18) — “The bigger the interface, the weaker the abstraction.”
เช็กความเข้าใจ — บทที่ 5
ข้อ 1 / 3ชนิด `Counting` มี method `Apply` ที่ประกาศตัวรับเป็น `*Counting` ข้อใดถูก