1 · ทุก type มี underlying type
ใน Go ทุก type มี underlying type — โครงสร้างข้อมูลจริงที่อยู่ข้างใต้
ต่อให้คุณตั้งชื่อใหม่และแปะ method เข้าไป underlying type ก็ยังเป็นตัวเดิม
(method = ฟังก์ชันที่ผูกติดอยู่กับ type ตัวหนึ่ง เรียกใช้ด้วยจุด เช่น
score.Grade() — หัวข้อ 5 จะอธิบายเต็ม ตอนนี้อ่านผ่านไปก่อนได้)
type Score float64
// มองแบบนี้:
Score
│
└── underlying type → float64
underlying type เป็นตัวกำหนดว่าทำอะไรกับค่านั้นได้บ้าง —
Score บวกลบคูณหารได้เพราะข้างใต้เป็น float64
ถ้าข้างใต้เป็น string ก็จะ + ได้อย่างเดียว (ต่อ string)
type Score float64
a := Score(80.5)
b := Score(10.0)
total := a + b
fmt.Println(total) // 90.5
fmt.Printf("%T", total) // main.Score ← ไม่ใช่ float64
สังเกต %T — บวกกันแล้วยังเป็น Score
ไม่ได้กลายเป็น float64 การคำนวณระหว่าง type เดียวกัน
ให้ผลลัพธ์เป็น type เดิมเสมอ
2 · Same underlying type ≠ same type
นี่คือหัวใจของบทนี้ type ที่มี underlying type เดียวกัน ยังเป็นคนละ type
type Score float64
type Temperature float64
type Price float64
type Distance float64
// ทั้งหมดมี underlying type เป็น float64
// แต่ compiler ถือว่าเป็น 4 type ที่ไม่เกี่ยวกันเลย
บนโต๊ะก๋วยเตี๋ยวมีขวดใสหน้าตาเหมือนกันเป๊ะ 4 ขวด — น้ำปลา น้ำส้มสายชู ซีอิ๊ว น้ำเชื่อม
ทุกขวดเทได้ ตวงได้เหมือนกัน แต่ใช้แทนกันไม่ได้
หยิบผิดขวดทีเดียวชามพัง
· ของเหลวข้างในขวด = underlying type — บอกว่าทำอะไรกับมันได้บ้าง
(ข้างในเป็น float64 ก็เลยบวกลบคูณหารได้)
· ป้ายที่แปะข้างขวด = ชื่อ type — บอกว่าใช้แทนกันได้หรือไม่ได้
ในบทนี้กฎที่ใช้คือ: ป้ายคนละชื่อ และทั้งสองฝั่งต่างมีชื่อของตัวเอง
(Score กับ float64) → assign ตรง ๆ ไม่ได้ ต้อง convert
Go เข้มกว่าโต๊ะก๋วยเตี๋ยวตรงที่ ถ้าสูตรขอ "น้ำปลา" แล้วยื่นขวด "น้ำส้มสายชู" ให้
มันปฏิเสธตั้งแต่ยังไม่ทันเท
ข้อยกเว้นที่จะเจอทีหลัง: ถ้าอีกฝั่งไม่มีชื่อ — เขียนเป็นรูปร่างดิบ ๆ
อย่าง []int — Go ยอมให้ assign ตรง ๆ ทั้งที่ป้ายต่างกัน
เช่น type MySlice []int แล้ว var s MySlice = []int{1, 2, 3}
และ var raw []int = s ผ่านทั้งคู่ (ผมรันจริงแล้ว)
จำกฎว่า "ป้ายต่างกัน = ห้ามเสมอ" จึงแคบเกินไป
ที่ไม่เหมือนกัน: ตอนเขียน float64(score)
เราไม่ได้เทของข้ามขวด ข้อมูลข้างในยังเป็นตัวเดิมเป๊ะ
เปลี่ยนแค่ป้ายที่ compiler อ่าน — การ convert ระหว่าง type ที่
underlying type เหมือนกันแบบนี้จึงไม่เสียเวลาเพิ่มตอนรันจริงเลย
แต่ conversion ที่ข้ามชนิด เช่น int(3.9) หรือ []byte(s)
ไม่ใช่การเปลี่ยนป้าย — มันแปลงข้อมูลจริงและมีค่าใช้จ่ายจริง (อ่านต่อในกล่องศัพท์ถัดไป)
type Score float64
var score Score = 95.5
var value float64 = score
fmt.Println(value)
โค้ดนี้ compile ผ่านไหม?
ไม่ผ่าน — ผมรันทดสอบแล้ว ได้ข้อความนี้:
cannot use score (variable of float64 type Score)
as float64 value in variable declaration
สังเกตข้อความ compiler เอง: "of float64 type Score" —
มันรู้ว่า underlying เป็น float64 แต่ก็ยังไม่ยอมให้ผ่าน
ต้องเขียน float64(score) เอง นี่คือกฎเดียวกับ
บทเรียน 02:
Go ไม่แปลง type ให้อัตโนมัติ ไม่ว่ากรณีไหน
// ถ้าจะเปรียบเทียบ ต้อง convert ก่อน:
value == float64(score) // ✓
score == Score(value) // ✓ ทำทางไหนก็ได้
3 · นี่แหละคือประโยชน์ — compiler จับบั๊กให้
ฟังดูน่ารำคาญใช่ไหม ทำไมต้อง convert ให้ยุ่งยาก ลองดูตัวอย่างนี้แล้วจะเห็นว่ามันช่วยอะไร:
type UserID int64
type ProductID int64
func FindUser(id UserID) {
fmt.Println(id)
}
productID := ProductID(101)
FindUser(productID) // ← ส่งผิดตัว
บรรทัดสุดท้ายเกิดอะไรขึ้น — พิมพ์ 101, compile error หรือ panic?
Compile error — จับได้ตั้งแต่ยังไม่ได้รัน:
cannot use productID (variable of int64 type ProductID)
as UserID value in argument to FindUser
ถ้าใช้ int64 เปล่า ๆ ทั้งคู่ โค้ดนี้จะผ่านฉลุย
แล้วไปพังตอน production ตอนที่ query หา user id 101 แล้วเจอคนอื่น —
บั๊กประเภทนี้หาสาเหตุยากมาก เพราะไม่มีอะไร crash
type ที่มีความหมายต่างกัน ควรเป็นคนละ type ต่อให้เก็บข้อมูล
หน้าตาเหมือนกัน — UserID/ProductID,
Baht/Satang, Celsius/Fahrenheit
การพิมพ์เพิ่มบรรทัดเดียวแลกกับการที่ compiler กันบั๊กให้ทั้งโปรเจกต์
ใช้คำให้ตรง แล้วจะถาม senior หรือถาม AI ได้ตรงจุดขึ้นมาก
- underlying type
- "ของเหลวข้างในขวด" — โครงสร้างข้อมูลจริงใต้ชื่อ type · ถามว่า "type นี้ underlying type เป็นอะไร" จะสื่อกว่า "จริง ๆ มันคือ float64 ใช่ไหม"
- defined type (บางคนเรียก custom type)
- type ใหม่ที่เราประกาศเอง แบบไม่มี
=· "ตรงนี้เป็น defined type เลยต้อง convert ก่อน"
เกร็ดไว้กันเถียงแพ้: ตาม spec ของ Go แล้วintstringfloat64ก็เป็น defined type เหมือนกัน (spec เขียนตรง ๆ ว่า "all numeric types are defined types") · เพียงแต่เวลาคุยงานคนมักใช้คำนี้เรียก type ที่เราประกาศเอง - type conversion
- การเขียน
T(x)เพื่อเปลี่ยนค่าให้เป็น typeT· คนจากภาษาอื่นเรียกว่า "cast" แต่ Go เรียก conversion
· ถ้า underlying type เหมือนกัน (Score→float64) = เปลี่ยนป้ายเฉย ๆ ข้อมูลไม่ขยับ
· แต่ถ้าข้ามชนิด conversion แปลงข้อมูลจริง และทำข้อมูลหายได้ — ผมรันจริงด้วย Go 1.26:int(3.9)ได้3,int(-2.7)ได้-2(ตัดทศนิยมทิ้ง ไม่ปัดเศษ) และ[]byte("hi")ได้[104 105]ซึ่งคือ copy ข้อมูลชุดใหม่
เจอint(price)แล้วเงินหายไปครึ่งบาท สาเหตุอยู่ตรงนี้ - compile time / run time
- ตอนแปลโค้ด กับ ตอนโปรแกรมทำงานจริง · คำถามแรกที่ dev ถามกันเสมอคือ "error นี้ขึ้นตอน compile หรือตอน run" เพราะแก้คนละวิธี
- type safety
- การที่ compiler กันไม่ให้เอาของผิดชนิดมาปนกัน · "ผมแยก UserID กับ ProductID เป็นคนละ type เพื่อ type safety ครับ"
4 · Custom type ≠ Alias — ต่างกันที่ =
· type Score = float64 (มี =) = ชื่อเล่น ·
"หมู" กับ "สมชาย" คือคนเดียวกัน เรียกชื่อไหนก็ได้คนเดิมเดินมา
ใช้แทนกันได้ทุกที่ และเวลากรอกเอกสารราชการ (%T) ก็ยังขึ้นชื่อจริงอยู่ดี
· type Score float64 (ไม่มี =) = ฝาแฝด ·
หน้าเหมือนกันทุกกระเบียดนิ้ว แต่เป็นคนละคน
มีบัตรประชาชนคนละใบ ไปสอบแทนกันไม่ได้
ทีนี้จะเห็นเองว่าทำไมชื่อเล่นที่ชี้ไป type ของคนอื่น
(เช่น float64 ของ Go) แปะ method ไม่ได้ —
เพิ่มความสามารถให้ "หมู" ก็คือเพิ่มให้ "สมชาย" นั่นแหละ
เท่ากับไปแก้ของกลางของทั้งภาษา Go เลยไม่ยอม
ผมรันจริงแล้วได้ cannot define new methods on non-local type Score
ระวังจำเหมารวม: ถ้าชื่อเล่นนั้นชี้ไป type ที่เราประกาศเองใน package เดียวกัน
Go ยอมให้แปะ — type Foo struct{ N int } แล้ว type Bar = Foo
ตามด้วย func (b Bar) Hi() string คอมไพล์ผ่าน (ผมรันแล้ว)
แต่ผลลัพธ์คือ method นั้นไปเกาะกับ Foo ตัวจริง เรียก f.Hi() ผ่านตัว
Foo ได้เลย — ไม่ได้เกิด type ใหม่ขึ้นมาสักตัว ตรงกับเรื่อง "ชื่อเล่นคือคนเดิม" พอดี
ที่ไม่เหมือนกัน: ฝาแฝดที่เป็นคนจริง ๆ โตไปนิสัยต่างกันได้
แต่ Score กับ float64 เก็บข้อมูลเหมือนกันเป๊ะตลอดไป
ต่างกันเฉพาะตอน compiler ตรวจชื่อเท่านั้น
type Score float64 | type Score = float64 | |
|---|---|---|
| เรียกว่า | Defined type (custom type) | Alias (ชื่อเล่น) |
| เป็น type ใหม่ไหม | ✅ ใช่ — คนละ type กับ float64 | ❌ ไม่ — คือ float64 ตัวเดียวกัน |
| assign ตรง ๆ ได้ไหม | ❌ ต้อง convert | ✅ ได้เลย |
| แปะ method ได้ไหม | ✅ ได้ | ❌ ไม่ได้ |
%T พิมพ์ว่า | main.Score | float64 |
| ใช้ตอนไหน | อยากให้ compiler แยกความหมาย | ตอนย้าย/เปลี่ยนชื่อ package โดยไม่พังของเก่า |
// Alias — ผมรันทดสอบแล้ว assign ได้ตรง ๆ ไม่ต้อง convert:
type ScoreAlias = float64
var alias ScoreAlias = 95.5
var plain float64 = alias // ✓ ผ่าน
fmt.Printf("%T", alias) // float64 ← ไม่ใช่ ScoreAlias
alias ที่คุณใช้อยู่แล้วโดยไม่รู้ตัว: any คือ alias ของ
interface{} และ byte คือ alias ของ uint8
ส่วน rune คือ alias ของ int32 — ทั้งหมดนี้ประกาศด้วย
= ใน standard library จริง ๆ
- type alias
- "ชื่อเล่น" ของ type ที่มีอยู่แล้ว — ประกาศด้วย
=· ถามว่า "ตัวนี้เป็น alias หรือ defined type" คำถามเดียวตัดจบได้เลยว่าต้อง convert ไหม แปะ method ได้ไหม - identical (เป็น type เดียวกัน)
- คำที่ Go spec ใช้เรียกเรื่องนี้ตรง ๆ · alias ทำให้สองชื่อ identical กัน ส่วน defined type ไม่ · พิมพ์ถาม AI ได้เลยว่า "in Go, are these two types identical or just sharing the same underlying type?"
byte·rune·any- alias ที่ใช้อยู่ทุกวันโดยไม่รู้ตัว · รู้ข้อนี้แล้วจะเข้าใจทันทีว่าทำไม
[]byteกับ[]uint8ถึงใช้แทนกันได้โดยไม่ต้อง convert
5 · ของแถมที่ได้จาก custom type: แปะ method ได้
พอเป็น type ของเราเองแล้ว ก็นิยาม method ได้เหมือน struct —
แม้ underlying type จะเป็นแค่ float64 ก็ตาม
type Score float64
func (s Score) Passed() bool {
return s >= 50
}
func (s Score) Grade() string {
switch {
case s >= 80: return "A"
case s >= 70: return "B"
case s >= 60: return "C"
case s >= 50: return "D"
default: return "F"
}
}
// ใช้งาน — ผลลัพธ์จริงจากการรัน:
Score(75).Passed() // true
Score(85.5).Grade() // A
เทียบกับการเขียน func isPassed(s float64) bool ลอย ๆ —
แบบ method ผูกตรรกะไว้กับ type ที่มันเป็นเจ้าของ
ใครเห็น Score ก็กด . แล้วเห็นเลยว่าทำอะไรได้บ้าง
แปะ method ได้เฉพาะ type ที่ประกาศใน package เดียวกัน —
เขียน func (s string) Reverse() string ไม่ได้ เพราะ string
เป็นของ Go ไม่ใช่ของคุณ ทางแก้คือ type MyString string ก่อน
รถเช่าขับได้ เติมน้ำมันได้ แต่จะเจาะติดสปอยเลอร์หรือพ่นสีใหม่ —
ไม่ได้ อยากแต่งต้องมีรถที่จอดอยู่ในบ้านเรา
(คือประกาศไว้ใน package เดียวกับ method) ก่อน
string int float64 คือรถเช่า —
ใช้ได้แต่แต่งไม่ได้ · func (s string) Reverse() string จึงไม่ผ่าน
พอประกาศ type MyString string ใน package เดียวกัน
ก็เท่ากับรถจอดอยู่ในบ้านแล้ว แปะ method ได้เต็มที่
เกณฑ์คือที่จอด ไม่ใช่ความเป็นเจ้าของ:
ต่อให้ type นั้นเราเขียนเองกับมือ ถ้ามันอยู่คนละ package ก็ยังแปะไม่ได้ —
ผมประกาศ type Thing struct{...} ไว้ใน package inner
แล้วเขียน func (x inner.Thing) Boom() ใน main
ได้ error เดียวกันเป๊ะ: cannot define new methods on non-local type inner.Thing
ที่ไม่เหมือนกัน: ซื้อรถต้องจ่ายเงินและได้รถเพิ่มมาอีกคัน
แต่ประกาศ type ใหม่นั้น ค่าแต่ละตัวไม่กินหน่วยความจำเพิ่ม
และไม่มีค่าใช้จ่ายตอนรัน — ตอนรันจริง MyString ก็คือ
string ตัวเดิม เพิ่มมาแค่ป้ายชื่อที่ compiler เอาไว้ตรวจ
(ตัวไฟล์ binary จะมีข้อมูลกำกับ type ใหม่เพิ่มขึ้นบ้าง แต่ไม่กระทบค่าที่โปรแกรมเก็บ)
- method
- ฟังก์ชันที่ผูกติดกับ type ตัวหนึ่ง เรียกด้วยจุด เช่น
score.Grade()· "อันนี้เขียนเป็น method ของ Score ดีกว่าไหม จะได้กด . แล้วเจอ" - receiver
- พารามิเตอร์พิเศษในวงเล็บที่อยู่หน้าชื่อ method คือ
(s Score)· เวลาพูดกัน receiver หมายถึงตัวsที่รับค่าเข้ามา ส่วนScoreเรียกว่า receiver base type (คำนี้อยู่ใน spec ตรง ๆ และเป็นตัวที่ต้องประกาศใน package เดียวกัน) · คำถามที่จะเจอบ่อยมากตอนทำงานจริง: "method ตัวนี้ receiver ควรเป็น value หรือ pointer" (ดู บทเรียน 07) - non-local type
- type ที่ไม่ได้ประกาศใน package ของเรา — คือ "รถเช่า" นั่นเอง ·
จำไว้เพราะคำนี้โผล่ในข้อความ error ตรง ๆ:
cannot define new methods on non-local typeเห็นเมื่อไหร่ให้รู้ทันทีว่าต้องประกาศ type ของตัวเองก่อน
6 · ทดสอบความจำ
1. type Score float64 กับ type Price = float64 อันไหนสร้าง type ใหม่?
ไม่มี = → type ใหม่ · มี = → alias
จำง่าย ๆ ว่าเครื่องหมายเท่ากับแปลว่า "เท่ากันจริง ๆ" —
Price คือ float64 ตัวเดียวกันเป๊ะ ส่วน Score
แค่หน้าตาเหมือน
2. type Score float64 แล้ว var v float64 = score ได้ผลยังไง?
underlying type เดียวกันไม่ได้แปลว่า type เดียวกัน
Go ไม่แปลง type ให้อัตโนมัติในทุกกรณี ต้องเขียน float64(score) เอง
ซึ่งเป็นการบอก compiler ว่า "ฉันตั้งใจ" ไม่ใช่พิมพ์ผิด
3. type Score float64 และ type Price float64
(ทั้งคู่ไม่มี =) — score == price ได้ไหม?
ข้อความจริงจาก compiler: invalid operation: score == price
(mismatched types Score and Price) — ต้อง convert ให้เป็น type เดียวกันก่อน
เช่น float64(score) == float64(price)
ระวังอย่าสับสนกับข้อ 1 — ในข้อ 1
Price ประกาศด้วย = จึงคือ float64
เทียบกับ float64 ตัวอื่นได้เลย แต่ข้อนี้ไม่มี =
Price จึงเป็น type ใหม่คนละตัวกับ Score
4. ทำไมถึงควรสร้าง type UserID int64 แทนใช้ int64 ตรง ๆ?
ไม่ได้เร็วขึ้นและไม่ได้ประหยัดเลย — custom type ไม่มี overhead ตอน runtime
เลยแม้แต่นิดเดียว สิ่งที่ได้คือ compiler กลายเป็นคนตรวจให้
ส่ง ProductID เข้าฟังก์ชันที่ขอ UserID ไม่ได้อีกต่อไป
7 · ลงมือทำ
ภารกิจ: ระบบเงินที่ไม่มีวันคิดหน่วยผิด
บั๊กระดับตำนานในระบบการเงินคือ "เอาบาทไปบวกสตางค์" ลองใช้ type system กันดู
- ประกาศ
type Baht int64และtype Satang int64 - เขียน method
func (b Baht) ToSatang() Satang(คูณ 100) และfunc (s Satang) ToBaht() Baht - เขียน
func Charge(amount Satang)ที่พิมพ์ยอดออกมา - ลองเรียก
Charge(Baht(150))ตรง ๆ — อ่าน error ให้ครบทั้งบรรทัด แล้วแก้ให้ถูก - เพิ่ม method
func (s Satang) String() stringที่คืนค่าเป็น"150.00 บาท"แล้วfmt.Println(Satang(15000))— ทำไมมันรู้ว่าต้องเรียกString()? เดี๋ยวเฉลยใน บทเรียน 11 - ท้าทาย: ลองเปลี่ยน
type Baht int64เป็นtype Baht = int64แล้ว build ใหม่ — ข้อไหนพัง ข้อไหนยังผ่าน และอ่าน error ทั้งสองบรรทัดให้ออกว่ามันเกี่ยวกันยังไง
สปอยล์: ผมรันแล้ว ได้cannot define new methods on non-local type Bahtตามด้วยBaht(150).ToSatang undefined— บรรทัดที่สองเป็นผลพวงของบรรทัดแรก
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ: Go spec — Types หัวข้อ "Underlying types" ยาวแค่ย่อหน้าเดียวแต่เป็นนิยามชี้ขาด ถ้าอยากรู้ว่าทำไม Go ถึงมี alias ตั้งแต่แรก อ่าน The Go Blog — Alias names ซึ่งเล่าเหตุผลว่ามันเกิดมาเพื่อการย้าย type ข้าม package
💬 ติดตรงไหนถามได้เลย — ข้อท้าทายท้ายภารกิจ (เปลี่ยนเป็น alias แล้ว method หายไปไหน) เป็นจุดที่คนพลาดกันบ่อยที่สุด ถ้า error อ่านไม่ออกก็ส่งมาได้
อ้างอิง
- Go spec — Types — นิยามของ underlying type
- Go spec — Type definitions — defined type ต่างจาก alias ตรงไหน
- Go spec — Alias declarations
- The Go Blog — Alias names — เหตุผลที่ alias ถูกเพิ่มเข้ามา
- Go spec — Method declarations — กฎว่าแปะ method กับ type ไหนได้บ้าง