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

interface และ​การ​ต่อ​กฎ​ส่วนลด

orderctl คิด​ยอด​ได้​แล้ว แต่​ยอด​ที่​คิด​ได้​ยัง​เป็น​ยอด​เต็ม​เสมอ

บท​นี้​เติม​ส่วนลด และ​ส่วนลด​คือ​ที่​ที่​ทุก​ภาษา​เอา​เครื่อง​มือ abstraction ออก​มา​ใช้ Go มี​เครื่อง​มือ​นั้น​ตัว​เดียว และ​มัน​ทำงาน​กลับ​ด้าน​กับ​ที่​คุณ​คุ้น

ชนิด​หนึ่ง​เข้า​กับ 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 .
200
180
150

บรรทัด​ที่​หน้าตา​แปลก​ที่สุด​คือ _ Rule = PercentOff{} มัน​ไม่​ได้​ทำงาน​ตอน​รัน มัน​คือ​การ​ขอ​ให้​คอม​ไพ​เลอ​ร์​ตรวจ​ความ​ผูกพัน​ให้

ทำไม​ต้อง​เขียน `var _ Rule = PercentOff{}` ด้วย

เพราะ​การ​สะกด​ชื่อ methodผิด​หนึ่ง​ตัว​ยัง​คอม​ไพล์​ผ่าน

ถ้า​คุณ​พิมพ์ func (p PercentOff) Aply(...) ชนิด​นั้น​ก็​ยัง​เป็น​ชนิด​ที่​ถูก​ต้อง มัน​แค่​ไม่ใช่ Rule อีก​ต่อ​ไป และ​ไม่มี​ใคร​รู้​จนกว่า​จะ​มี​คน​พยายาม​ส่ง​มัน​เข้า Total

บรรทัด var _ Rule = PercentOff{} ย้าย​ความ​ผิด​พลาด​นั้น​มา​ไว้​ที่ file ของ​ผู้​สร้าง และ​ทำให้​มัน​แดง​ตอน build แทนที่​จะ​แดง​ที่ call site ของ​คน​อื่น​ใน​อีก​สาม​สัปดาห์

เมื่อ​ไม่มี​บรรทัด​ผูกพัน​ให้​เขียน ฝั่ง​ที่​ประกาศ 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.go
package 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 ระดับ​ล่าง​ไม่​ต้อง​พึ่ง​ใคร​เลย

ตัว​รับ​แบบ 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นั้น​อยู่

interface ของ Go ไม่​ได้​ดู​ที่ field

ถ้า​คุณ​มา​จาก TypeScript คุณ​จะ​คิด​ว่า structural typing เหมือน​กัน จึง​คาด​ว่า struct ที่​มี field ตรง​กับ​รูป​ร่าง​ของ interface ก็​เข้า​กัน​ได้

Go เทียบ method set อย่าง​เดียว interface ของ Go บรรจุ field ไม่​ได้​เลย และ​ตัว​รับ​แบบ pointer เปลี่ยน​คำ​ตอบ​ทั้ง​ที่ field เหมือน​เดิม​ทุก​ตัว

ค่าที่​อยู่​ใน 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 ทันทีที่ชนิดไม่ตรง
}
`x as Foo` ของ C# ไม่มี​คู่​แฝด​ตรง​ตัว​ใน Go

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 อยู่​ข้าง​ใน

ท่า​ที่​ใช้​แทน​การ​สืบทอด​คือ​ฝัง 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 มีตัวรับเป็น pointer
var _ 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 .
180
90
2

c.Rule.Apply(subtotal) คือ​คู่​ของ base.Apply(...) ใน​ภาษา​ที่​มี​การ​สืบทอด ต่าง​กัน​ตรง​ที่​เส้น​ทาง​นั้น​เขียน​ไว้​ให้​เห็น​ด้วย​ตา ไม่​ได้​ซ่อน​อยู่​ใน​กฎ​การ dispatch

สังเกต​บรรทัด var _ Rule = (*Counting)(nil) ว่า​มัน​ตรวจ *Counting ไม่ใช่ Counting{} เพราะ Apply ของ Counting มี​ตัว​รับ​เป็น pointer ตาม​กฎ method set ของ​หัวข้อ​ที่​แล้ว

[]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 เลือก​จ่าย​อย่าง​เปิด​เผย

เส้น​แบ่ง​อยู่​ที่​ว่า​คุณ​ต้องการ​เรียก 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.”

generic method มา​ถึง​ใน Go 1.27 พร้อม​ข้อ​จำกัด​สอง​ข้อ

ก่อน 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` ข้อใดถูก