บทเรียน 19 · Day 6 · Generics

Generics: เขียนครั้งเดียว ใช้ได้หลาย type

~15 นาที · ต้องมี: บทเรียน 09 (underlying type), บทเรียน 10 (interface), บทเรียน 11 (any)

ก่อน Go 1.18 ถ้าอยากได้ Sum ที่ใช้กับทั้ง int และ float64 คุณมีสองทาง: copy ฟังก์ชันแล้วแก้ type หรือ ใช้ any แล้วเดารายทาง — ทางที่สองไม่ได้แค่ "อ่านยาก" แต่พังตอนรันจริง บทนี้เริ่มจากพิสูจน์ว่ามันพังยังไง แล้วค่อยดูว่า generics แก้อะไร

1 · ปัญหา: สองทางที่แย่ทั้งคู่

❌ ทางที่ 1 — copy แล้วแก้ type

func SumInts(values []int) int {
    var total int
    for _, v := range values {
        total += v
    }
    return total
}

func SumFloat64s(values []float64) float64 {
    var total float64
    for _, v := range values {
        total += v
    }
    return total
}

โค้ดเหมือนกันเป๊ะ ต่างแค่คำว่า int/float64
เจอบั๊ก = ต้องแก้ทุกก๊อป

❌ ทางที่ 2 — ใช้ any

func Sum(values []any) any {
    var total any
    for _, value := range values {
        switch v := value.(type) {
        case int:
            total = total.(int) + v
        case float64:
            total = total.(float64) + v
        }
    }
    return total
}

เขียนครั้งเดียวจริง แต่โค้ดนี้ระเบิด — ดู predict ข้างล่าง

fmt.Println(Sum([]any{1, 2, 3}))

ฟังก์ชัน Sum ฝั่งขวา (แบบ any) พิมพ์อะไรออกมา?

ไม่พิมพ์อะไรเลย — มัน panic ผมรันแล้ว:

panic: interface conversion: interface {} is nil, not int

ทำไม: var total any ทำให้ total เริ่มต้นเป็น nil — ไม่ใช่ 0 พอวนรอบแรกเจอ case int มันสั่ง total.(int) คือ "จงตีความ nil ว่าเป็น int" ซึ่งทำไม่ได้ (บทเรียน 11 — type assertion แบบไม่มี comma-ok พังแล้วพังเลย)

ประเด็นที่สำคัญกว่านั้น: โค้ดนี้คอมไพล์ผ่าน compiler ไม่มีทางรู้เลยว่ามีอะไรอยู่ใน any — พังตอน production เท่านั้น นี่คือเหตุผลที่ generics ไม่ใช่แค่ "ทางที่สวยกว่า" แต่เป็นทางที่ปลอดภัยกว่า

2 · ทางที่สาม: type parameter

อ่าน Number ให้ออกก่อน — ข้างในไม่ใช่รายชื่อ method แต่เป็น "รายชื่อ type ที่ยอมรับ" เครื่องหมาย | อ่านว่า "หรือ" (เป็น int ก็ได้ int64 ก็ได้ float64 ก็ได้) ส่วน ~ ข้างหน้าอ่านว่า "type ใดก็ตามที่มี underlying type เป็นตัวนี้" ซึ่งทำให้ type ที่คุณตั้งเองอย่าง type Score int ผ่านด่านไปด้วย
ตอนนี้จำแค่นี้พอ — หัวข้อ 7 จะรันให้ดูว่าถ้าลืม ~ แล้ว compiler ด่าว่าอะไร

type Number interface {
    ~int | ~int64 | ~float64
}

func Sum[T Number](values []T) T {
    var total T          // zero value ของ T = 0 ไม่ใช่ nil
    for _, value := range values {
        total += value    // compiler รู้ว่า T บวกกันได้
    }
    return total
}

ints   := Sum([]int{10, 20, 30})       // 60
floats := Sum([]float64{1.5, 2.5, 3.5})  // 7.5
  ตำแหน่งของวงเล็บบอกว่าเป็นอะไร

  func Sum [T Number] (values []T) T
           ─────┬─────   ────┬────    ┬
                │            │        └─ คืนค่าเป็น type เดียวกัน
                │            └────────── parameter ปกติ ใช้ T ได้
                └─────────────────────── type parameter
                                         "T คือ type อะไรก็ได้ที่เข้าเกณฑ์ Number"

  [] เหลี่ยม = ส่ง type เข้าไป   ·   () กลม = ส่ง ค่า เข้าไป
🧰 เทียบให้เห็นภาพ — เครื่องคั้นน้ำผลไม้

เครื่องคั้นน้ำผลไม้เครื่องเดียวใช้ได้กับของหลายอย่าง — ใส่ส้มได้น้ำส้ม ใส่แครอทได้น้ำแครอท ไม่ต้องซื้อสองเครื่อง

สองอย่างที่เครื่องนี้ทำได้ ตรงกับ Sum[T Number] พอดี:
· ใส่อะไรลงไป ได้ของชนิดนั้นออกมา — ไม่ใช่ "น้ำอะไรก็ไม่รู้" นี่คือความหมายของ T ที่โผล่ทั้งขาเข้าและขาออก
· มีป้ายบอกว่าใส่อะไรได้บ้าง — "ผลไม้กับผักเท่านั้น อย่าใส่ก้อนหิน" ป้ายนั้นคือ Number

ที่ไม่เหมือนกัน: เครื่องคั้นรู้ว่ากำลังคั้นอะไรก็ตอนที่คุณยัดผลไม้เข้าไปแล้วจริง ๆ แต่ Go ตัดสินว่า T คือ type อะไรตั้งแต่ตอน compile — ถ้าเอาของที่ไม่เข้าเกณฑ์ไปใส่ มันจะไม่ยอม build ให้ตั้งแต่แรก ไม่ใช่ปล่อยผ่านแล้วไปพังตอนรัน

🗣 ศัพท์ที่เพิ่งเรียนไป
type parameter
ตัว T ที่อยู่ในวงเล็บเหลี่ยม = "ช่องว่างที่ยังไม่รู้ว่าเป็น type อะไร" · เวลาคุยให้พูดว่า “ฟังก์ชันนี้มี type parameter กี่ตัว” แทนที่จะพูดว่า “ในวงเล็บเหลี่ยมมีอะไรบ้าง”
type argument
type จริงที่ไปเติมในช่องว่างนั้นตอนเรียกใช้ · เขียน Sum[int](...) แปลว่า “ส่ง int เป็น type argument” — เทียบกับ argument ปกติที่เป็น ค่า ตัวนี้เป็น type
constraint
"ป้ายบอกว่าใส่อะไรได้บ้าง" · ในตัวอย่างคือ Number · ประโยคที่ใช้ได้จริง: “constraint ของ T ตัวนี้คืออะไร” เวลาเจอ error ที่ compiler ไม่ยอมให้เอา T ไปทำอะไร
generic function
ฟังก์ชันที่มี type parameter · ตรงข้ามกับฟังก์ชันธรรมดาที่ผูกกับ type เดียวตายตัว · เวลาถาม AI ให้พิมพ์ว่า “เขียน generic function ใน Go ที่รับ slice ของ T” จะได้คำตอบตรงกว่าพิมพ์ว่า “ฟังก์ชันที่ใช้ได้หลาย type”

3 · [T any] ไม่เหมือน parameter ที่เป็น any

จุดนี้คนสับสนมากที่สุด เพราะคำว่า any โผล่ทั้งสองแบบ แต่ความหมายคนละเรื่อง:

❌ value any — ลืม type ไปแล้ว

func Identity(value any) any {
    return value
}

name := Identity("Alice")

// name เป็น any → ใช้กับ string ไม่ได้
fmt.Println(strings.ToUpper(
    name.(string),  // ต้อง assert เอง
))

คุณรู้ว่ามันคือ string แต่ compiler ไม่รู้ — ต้อง assert ทุกครั้ง และ assert ผิดเมื่อไหร่ = panic

✅ [T any] — จำ type ไว้

func Identity[T any](value T) T {
    return value
}

name := Identity("Alice")

// name เป็น string เต็มตัว
fmt.Println(strings.ToUpper(name))

compiler เห็นว่าส่ง string เข้าไป → T = string → คืนค่าเป็น string
ไม่ต้อง assert ไม่มีทาง panic

สรุปสั้น: any ในฐานะ parameter แปลว่า "ฉันไม่สนว่าเป็นอะไร" — ทิ้ง type ไว้ที่ประตู
[T any] แปลว่า "ฉันยังไม่รู้ว่าเป็นอะไร แต่ตอนเรียกจะรู้ และจะจำไว้ทั้งฟังก์ชัน" — type ยังอยู่ครบ

🧰 เทียบให้เห็นภาพ — เคาน์เตอร์รับฝากของหน้าห้าง

คุณเอาร่มไปฝากที่เคาน์เตอร์ มีสองแบบ
แบบไม่มีบัตร — พนักงานรับของไปวางกองรวมกัน ตอนมารับคุณต้องบอกเองว่า "ของผมคือร่ม" ถ้าจำผิดบอกเป็น "หมวก" เรื่องแดงตรงนั้นเลย
นี่คือ value any — ตอนเอาค่าออกมาใช้ต้อง .(string) เอง บอกผิดเมื่อไหร่โปรแกรมตายทันที (ศัพท์ที่ Go ใช้เรียกอาการตายกลางทางแบบนี้คือ panic)
ที่ตายคือแบบดิบเท่านั้นนะ — s := x.(string) · ถ้าเขียนแบบถามก่อน s, ok := x.(string) จะได้ ok = false แล้วเดินต่อได้ ไม่พัง (สำนวน comma-ok จาก บทเรียน 11)

แบบมีบัตร — พนักงานเขียนบัตรให้ว่า "ร่ม" ตอนมารับยื่นบัตร ได้ของถูกชิ้นแน่นอน ไม่ต้องเดาสักคำ
นี่คือ [T any] — ส่ง string เข้าไป compiler ออกบัตรให้ว่า T = string แล้วจำไว้ทั้งฟังก์ชัน

ที่ไม่เหมือนกัน: ของที่ฝากไว้ไม่เคยเปลี่ยนเป็นอย่างอื่นเลยทั้งสองแบบ — ค่าที่ใส่ใน any ก็ยังเป็น string ตัวเดิม สิ่งที่หายไปคือ "บัตร" ที่บอก compiler ว่าข้างในคืออะไร
และบัตรใบนี้ compiler เป็นคนออกให้ ไม่ใช่คุณ — มันตัดสินตอนแปลโค้ด จาก type ของค่าที่คุณส่งเข้าไป · คุณจะเลือก T เองตอนโปรแกรมรันไม่ได้ (Show[ตัวแปร](x) คอมไพล์ไม่ผ่าน) และเขียน switch T เพื่อถามตรง ๆ ว่า T คืออะไรก็ไม่ได้ — T เป็นชื่อ type ไม่ใช่ค่า

🗣 ศัพท์ที่เพิ่งเรียนไป
type safety
สภาพที่ compiler การันตีให้ได้ว่า type ถูกต้อง โดยที่คุณไม่ต้องเดาเอง · ประโยคที่เอาไปพูดได้: “แบบ generic มัน type safe กว่า เพราะไม่ต้อง assert”
type assertion
การเขียน x.(string) = ดึงค่าที่ซ่อนอยู่ใน interface ออกมา พร้อมตรวจว่าใช่ type นั้นจริงไหม — ไม่ใช่การแปลง type และใช้ได้เฉพาะกับค่าที่เป็น interface (เช่น any) เท่านั้น · เขียนกับ int ตรง ๆ compiler ด่าว่า is not an interface
คือการบอกเองว่าของคือชิ้นไหน ในเคาน์เตอร์แบบไม่มีบัตร · x.(string) แบบดิบ = พังถ้าผิด · v, ok := x.(string) = ถามก่อน ไม่พัง (comma-ok จาก บทเรียน 11) · เวลาถาม senior ให้พูดว่า “ตรงนี้ต้อง type assert ไหม”
compile time / runtime
compile time = ตอนแปลโค้ด (ยังไม่ได้รัน) · runtime = ตอนโปรแกรมกำลังทำงานจริง · คำคู่นี้คือหัวใจของบทนี้ทั้งบท: “ปัญหานี้จับได้ตอน compile time หรือไปโผล่ตอน runtime”
panic
อาการที่โปรแกรม Go ตายกลางทางเพราะเจอสิ่งที่ทำต่อไม่ได้ · ต่างจาก compile error ตรงที่เกิดตอน runtime คือหลุดถึงมือผู้ใช้แล้ว · เวลาแปะ error ให้คนอื่นดู บอกไปเลยว่า “มัน panic ตรงบรรทัดนี้”

4 · comparable — เกณฑ์สำหรับของที่เทียบกันได้

any กว้างเกินไปสำหรับงานที่ต้องใช้ == Go มีเกณฑ์สำเร็จรูปให้ชื่อ comparable:

func Contains[T comparable](values []T, target T) bool {
    for _, value := range values {
        if value == target {   // ใช้ == ได้เพราะ comparable
            return true
        }
    }
    return false
}

Contains([]int{10, 20, 30}, 20)                  // true
Contains([]string{"Go", "Java"}, "Go")            // true
Contains(points, Coordinate{X: 10, Y: 20})       // true — struct ที่ทุก field เทียบได้

ที่ใช้บ่อยอีกที่คือ key ของ map — เพราะ Go บังคับอยู่แล้วว่า key ต้องเทียบได้:

func Keys[K comparable, V any](items map[K]V) []K {
    keys := make([]K, 0, len(items))
    for key := range items {
        keys = append(keys, key)
    }
    return keys
}

⚠️ สไลด์เขียน // [Alice Bob] — อย่าเชื่อลำดับนั้น
ผมรัน Keys กับ map 4 ตัว 5 ครั้งติด ได้: [Alice Bob Carol Dave] · [Carol Dave Alice Bob] · [Bob Carol Dave Alice] · … ไม่ซ้ำกันเลย
Go สุ่มลำดับ for range บน map โดยตั้งใจ เพื่อไม่ให้ใครเผลอเขียนโค้ดที่พึ่งลำดับ ถ้าต้องการลำดับแน่นอนต้อง sort.Strings(keys) เอง

5 · Type inference — ไม่ต้องบอก type ก็ได้

type inference = "compiler เดา type ให้เอง" · คุณส่ง []int เข้าไป มันก็รู้เองว่า T คือ int ไม่ต้องพิมพ์บอกซ้ำ

// แบบกำหนดเอง — ชัดเจนแต่ยาว
Contains[int]([]int{10, 20, 30}, 20)

// แบบให้ compiler เดาเอาจาก argument — นิยมกว่า
Contains([]int{10, 20, 30}, 20)

ส่วนใหญ่เขียนแบบสั้นได้เลย
จะต้องระบุเองก็ต่อเมื่อ compiler เดาไม่ออก — คือกรณีที่ T ไม่ได้โผล่ใน argument ที่ส่งเข้าไปเลย มีแต่ใน return type พอไม่มีอะไรให้ดู มันก็เดาไม่ถูก

6 · Constraint — กำหนดว่า T ต้องทำอะไรได้บ้าง

constraint คือ "เกณฑ์" ที่บอกว่า T ต้องเป็น type แบบไหนถึงจะผ่าน
ทีนี้ [T any] แปลว่า "อะไรก็ได้" ซึ่งแปลว่าไม่มีเกณฑ์อะไรเลย ผลคือ compiler ไม่ยอมให้คุณทำอะไรกับมัน นอกจากส่งต่อ — เพราะมันไม่มีทางรับประกันได้ว่าของที่ส่งเข้ามาบวกกันได้:

func Sum[T any](values []T) T {
    var total T
    for _, value := range values {
        total += value   // ❌ compile error — T อาจเป็น struct ที่บวกไม่ได้
    }
    return total
}

วิธีแก้คือบอก compiler ว่า T แคบลงแค่ไหน — เขียนเป็น interface ที่ระบุรายชื่อ type แทนรายชื่อ method:

Constraintมาจากไหนให้ทำอะไรได้
anybuilt-inส่งต่อ / เก็บ / เทียบกับ nil เท่านั้น
comparablebuilt-in== · != · ใช้เป็น map key
cmp.Orderedimport "cmp" (Go 1.21+)< > <= >= — ตัวเลขทุกชนิด + string
constraints.Orderedgolang.org/x/expเหมือนกัน แต่เป็น package ทดลอง — ใช้ cmp แทน
เขียนเองคุณเท่าที่รายชื่อ type ในนั้นทำได้ร่วมกัน
import "cmp"

func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

Min(10, 20)            // 10
Min(1.5, 2.5)          // 1.5
Min("apple", "zoo")   // apple — string เรียงได้เหมือนกัน

สังเกตว่า constraint คือ interface เหมือนเดิม — แค่ข้างในเปลี่ยนจาก "รายชื่อ method" เป็น "รายชื่อ type" Go ไม่ได้เพิ่มคำใหม่ในภาษา แต่ขยายความหมายของ interface ที่คุณรู้จักจาก บทเรียน 10 แล้ว
เครื่องหมาย | อ่านว่า "หรือ" — ~int | ~float64 คือ "เป็น int ก็ได้ float64 ก็ได้"

7 · ~ — สัญลักษณ์ที่ทำให้ custom type ผ่านด่าน

type Integer interface { int | int64 }   // ← ไม่มี ~

func Max[T Integer](a, b T) T {
    if a > b { return a }
    return b
}

type Score int          // จากบทเรียน 09

var first  Score = 80
var second Score = 95
fmt.Println(Max(first, second))

Score มี underlying type เป็น int อยู่แล้ว — ผ่านไหม?

ไม่ผ่าน ผมรันแล้วได้:

Score does not satisfy Integer (possibly missing ~ for int in Integer)

compiler บอกวิธีแก้ให้ในวงเล็บเลย — int เฉย ๆ ใน constraint แปลว่า "ต้องเป็น int เป๊ะ ๆ"
Score ไม่ใช่ int มันคือ Score — เรื่องเดิมจาก บทเรียน 09 ที่ว่า defined type เป็นคนละ type กับต้นแบบ

เติม ~ เข้าไปแล้วผ่านทันที:

type Integer interface { ~int | ~int64 }
// ~int อ่านว่า "type ใดก็ตามที่มี underlying type เป็น int"
// → int ผ่าน · Score ผ่าน · Temperature ผ่าน · UserID ผ่าน
  int  กับ  ~int  ต่างกันตรงนี้

   type Score int          type Temperature int        int
        │                        │                      │
        └── underlying: int ─────┴──── underlying: int ─┘
                                 │
              ┌──────────────────┴──────────────────┐
              │                                     │
      constraint  int                      constraint  ~int
      รับเฉพาะตัวขวาสุด                      รับทั้งสามตัว
      (Score, Temperature ตกด่าน)            (ผ่านหมด)
🧰 เทียบให้เห็นภาพ — ป้ายชื่อหน้ากล่อง

type Score int คือการเอา int ใส่กล่อง แล้วแปะป้ายชื่อใหม่ว่า "Score" — ของข้างในเหมือนเดิมเป๊ะ เปลี่ยนแค่ป้าย
ทีนี้ constraint ก็เหมือนยามหน้าประตูที่ตรวจกล่องก่อนให้เข้า และมีสองแบบ:
· int เฉย ๆ = ยามที่ดูแค่ป้ายหน้ากล่อง "ป้ายต้องเขียนว่า int เท่านั้น" → กล่องที่แปะว่า Score ตกด่าน ทั้งที่ข้างในเหมือนกันทุกอย่าง
· ~int = ยามที่เปิดดูข้างใน "ป้ายจะเขียนอะไรก็ได้ ขอแค่ข้างในเป็น int" → Score, Temperature ผ่านหมด (ตรงกับรูปข้างบน)

ที่ไม่เหมือนกัน: ยามต้องแกะกล่องดูของจริง แต่ compiler ไม่ต้องแกะอะไรเลย มันอ่านจากบรรทัด type Score int ที่คุณเขียนไว้เอง — ตัดสินได้ตั้งแต่ยังไม่รันโปรแกรม
และการมีป้ายคนละใบไม่ใช่เรื่องไร้สาระ: มันคือสิ่งที่กันไม่ให้เผลอเอา Score ไปบวกกับ Temperature — ข้างในเป็น int เหมือนกันทั้งคู่ แต่ compiler ฟ้องว่า mismatched types Score and Temperature

ใช้ได้กับ slice type ที่ตั้งเองด้วย:

type Names []string

type StringSlice interface { ~[]string }

func CountNames[S StringSlice](names S) int {
    return len(names)
}

CountNames([]string{"Alice", "Bob"})   // ผ่าน
CountNames(Names{"Alice", "Bob"})        // ผ่านเพราะมี ~
💡 กฎง่าย ๆ

เขียน constraint เอง → ใส่ ~ ไว้ก่อนแทบทุกครั้ง เพราะโค้ดจริงเต็มไปด้วย type Baht int64, type UserID string ที่คุณสร้างเองตามบทเรียน 09 — ถ้าไม่ใส่ ~ ฟังก์ชัน generic ของคุณ จะใช้กับ type ของโปรเจกต์ตัวเองไม่ได้เลย

🗣 ศัพท์ที่เพิ่งเรียนไป
underlying type
"ของที่อยู่ข้างในกล่อง" · Score มี underlying type เป็น int · เวลาถามให้พูดว่า “underlying type ของตัวนี้คืออะไร” จะสื่อกว่าถามว่า “ตัวนี้จริง ๆ แล้วเป็น int ใช่ไหม”
defined type
"กล่องที่แปะป้ายชื่อใหม่" — type ที่คุณประกาศเองด้วย type Score int · Go ถือว่ามันเป็นคนละ type กับ int เสมอ (เรื่องเดิมจาก บทเรียน 09)
type set
"รายชื่อ type ทั้งหมดที่ constraint ตัวหนึ่งยอมรับ" · ~int | ~int64 มี type set ที่กว้างกว่า int | int64 · ประโยคที่ใช้ได้จริงตอนเจอ error: “type set ของ constraint นี้ครอบคลุม defined type ด้วยไหม”
satisfy
คำที่ compiler ใช้เวลา type ผ่าน/ไม่ผ่าน constraint — Score does not satisfy Integer อ่านว่า "Score ไม่เข้าเกณฑ์ Integer" · เอาไปพิมพ์ถาม AI ได้ทั้งประโยค จะตรงกว่าถามว่า “ทำไมมัน error”

ทดสอบความเข้าใจ

1. func Sum(values []any) any ที่มี var total any มีปัญหาอะไร?

var total any ได้ nil ไม่ใช่ 0 → total.(int) รอบแรกคือ panic: interface conversion: interface {} is nil, not int
อันตรายตรงที่คอมไพล์ผ่าน — compiler มองไม่เห็นข้างใน any จึงไปพังเอาตอนรันจริง

2. func F[T any](v T) T ต่างจาก func F(v any) any ยังไง?

ส่ง string เข้า F[T any] → ได้ string กลับมา เรียก strings.ToUpper ต่อได้ทันที
ส่งเข้า F(v any) → ได้ any กลับมา ต้อง .(string) เอง และถ้า assert ผิด = panic
สรุป: generic รักษา type ไว้ · any ทิ้ง type

3. type Score int ใช้กับ interface{ int | int64 } ไม่ได้ เพราะอะไร?

ในตัว constraint int = "ต้องเป็น int เท่านั้น" · ~int = "อะไรก็ได้ที่ underlying เป็น int"
compiler บอกทางแก้ให้ตรง ๆ: possibly missing ~ for int in Integer
เชื่อมกับบทเรียน 09: Score เป็นคนละ type กับ int เสมอ

4. อยากเขียน Min[T](a, b T) T ควรใช้ constraint ตัวไหน?

comparable ให้แค่ == และ != ไม่ให้ < — ชื่อชวนเข้าใจผิดมากที่สุดใน Go
ต้องใช้ cmp.Ordered (มีใน standard library ตั้งแต่ Go 1.21) ซึ่งครอบคลุมตัวเลขทุกชนิดและ string

🛠️ ภารกิจ — กล่องเครื่องมือ generic ของคุณเอง

ทำใน Go Playground หรือในเครื่อง สร้างไฟล์เดียวจบ

  1. เขียน Sum[T Number](values []T) T โดยที่ Number เป็น constraint ของคุณเอง ใส่ ~ ให้ครบทุกตัว
  2. ทดสอบว่าใช้ได้จริงกับ 3 แบบ: []int · []float64 · และ []Baht เมื่อ type Baht int64 (ของจากบทเรียน 09)
  3. เขียน Max[T cmp.Ordered](values []T) (T, bool) — คืน false เมื่อ slice ว่าง
    ใบ้: ค่าที่คืนตอนว่างคือ var zero T
  4. เขียน Contains[T comparable](values []T, target T) bool แล้วลองใช้กับ []struct{X, Y int}
  5. ท้าทาย 1: ลองเอา ~ ออกจาก Number แล้วรัน — จดข้อความ error ที่ได้ทั้งบรรทัด แล้วดูว่า compiler บอกทางแก้ไว้ตรงไหน
  6. ท้าทาย 2: ลองเปลี่ยน Max ให้ใช้ comparable แทน cmp.Ordered — พังตรงไหน และ error ว่าอะไร?
  7. ท้าทาย 3: เขียน Contains[T comparable] แล้วส่ง [][]int (slice ของ slice) เข้าไป — ผ่านไหม? ทำไม?
    ใบ้: slice ใน Go เอามา == กันได้หรือเปล่า

ท้าทาย 2: พังที่บรรทัดที่ใช้ >:

invalid operation: value > best (type parameter T cannot use operator >)

ชื่อ comparable ชวนเข้าใจผิดที่สุดใน Go — ภาษาไทยแปลว่า "เปรียบเทียบได้" ทำให้นึกถึง < > ด้วย แต่จริง ๆ มันหมายถึงแค่ "เอามาเทียบว่าเท่ากันไหมได้" — คือ == กับ != เท่านั้น
ของที่เรียงลำดับได้ต้องใช้ cmp.Ordered

ท้าทาย 3: ไม่ผ่าน — []int does not satisfy comparable เพราะ Go ไม่ให้ == กับ slice (เทียบได้เฉพาะกับ nil) เหตุผลเดียวกับที่ slice เป็น map key ไม่ได้ ถ้าอยากเทียบต้องใช้ slices.Equal

💬 ถามได้เลย — โดยเฉพาะถ้า error ตอนทำท้าทายอ่านไม่ออก ข้อความ error ของ generics เป็นจุดที่ Go อ่านยากที่สุด ส่งมาทั้งบรรทัดได้เลย
บทถัดไปเอา generics ไปใส่ใน struct — และดูว่า เมื่อไหร่ที่ยังไม่ควรใช้ generics ซึ่งสำคัญพอ ๆ กัน

อ้างอิง