1 · const ต่างจาก var ยังไง
const Pi = 3.14 // ระดับ package ได้
func main() {
const World = "World"
const Truth = true
fmt.Println("Hello", World, Pi, Truth)
}
const | var | |
|---|---|---|
| แก้ค่าทีหลัง | ❌ ไม่ได้ | ✅ ได้ |
ใช้ := | ❌ ไม่ได้เลย | ✅ ได้ (ในฟังก์ชัน) |
| ค่าที่ใส่ได้ | ต้องรู้ตั้งแต่ compile time | อะไรก็ได้ |
| ชนิดที่รองรับ | ตัวเลข, string, bool, rune (ตัวอักษรหนึ่งตัว — ที่จริงคือ Unicode code point หนึ่งตัว) | ทุกชนิด |
const Now = time.Now() // ✗ compile error!
time.Now() รู้ค่าตอน runtime ไม่ใช่ compile time →
เป็น const ไม่ได้ const เก็บได้แค่ค่าที่ compiler คำนวณเองได้ตั้งแต่ตอน build
2 · iota คืออะไรจริง ๆ
iota คือ "ลำดับของบรรทัด" ในบล็อก const นั้น
— บรรทัดแรกของบล็อกคือ 0 บรรทัดถัดไปเพิ่มทีละ 1 และรีเซ็ตกลับเป็น 0 เมื่อขึ้น
const block ใหม่
const (
zero = iota // 0
one // 1 ← ไม่ต้องเขียน = iota ซ้ำ
two // 2
)
บรรทัดที่ 2 และ 3 ไม่ได้เขียนอะไรเลย แต่ Go ทำซ้ำนิพจน์จากบรรทัดบน
ให้อัตโนมัติ (นิพจน์ = สูตรที่เขียนไว้ทางขวาของ =)
— one ก็คือ one = iota โดยปริยาย
และตอนนั้น iota มีค่า 1
หลักกิโลปักเรียงริมทางหลวง หลักแรกที่ต้นสายคือ กม.0 แล้วไล่ขึ้นทีละ 1 ตามลำดับที่ปัก
พอเลี้ยวเข้าทางหลวงสายใหม่ เลขไม่ได้นับต่อจากสายเดิม แต่เริ่ม 0 ใหม่ตั้งแต่ต้นสาย
iota คือเลขบนหลักกิโลของบรรทัดนั้น · const block หนึ่งอัน = ถนนหนึ่งสาย
และเหมือนหลักกิโลตรงที่เลขไล่ขึ้นเองทีละ 1 โดยไม่มีใครต้องไปกำหนดทีละหลัก
ระวังจุดนี้: เหตุผลที่บรรทัดที่ 2 กับ 3 ปล่อยว่างได้ เป็นคนละกลไกกับ iota
— มันคือกฎ "ทำซ้ำนิพจน์จากบรรทัดบน" ที่พูดถึงไปข้างบน ซึ่งทำงานต่อให้ไม่มี iota เลยก็ได้
เขียน const ( a = 5 แล้วบรรทัดถัดไปแค่ b เฉย ๆ ) —
b ก็ได้ 5 ทั้งที่ไม่มี iota อยู่ในบล็อกนั้นสักตัว
ในตัวอย่างนี้สองกลไกบังเอิญทำงานพร้อมกัน เลยดูเหมือนเป็นเรื่องเดียวกัน
ที่ไม่เหมือนกัน: เลขบนหลักกิโลคือระยะทางจริง แต่ iota เป็นแค่ลำดับ
ค่าที่ได้จริงขึ้นอยู่กับสูตรที่เขียนไว้บรรทัดแรก — ถ้าสูตรคือ iota * 10
ค่าก็กระโดดเป็น 0, 10, 20 ทั้งที่ iota ยังเดินทีละ 1 (เจอของจริงในหัวข้อ 4)
ตัวอย่างที่ใช้จริง — วันในสัปดาห์
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
เขียน 8 บรรทัด ได้ค่าครบ 7 ตัว และถ้าแทรกวันใหม่กลางลิสต์ เลขข้างล่างขยับ
ให้เองทั้งหมด — นี่คือเหตุผลที่ใช้ iota แทนการพิมพ์เลขเอง
3 · เครื่องหมาย _ — ข้ามเลขได้
const (
zero = iota
_
two
)
fmt.Println(zero, two)
พิมพ์อะไรออกมา?
0 2
_ (blank identifier) กินเลข 1 ไปแต่ไม่สร้างชื่อ →
two ได้ 2 พอดี สังเกตว่า iota ยังนับบรรทัดนั้น
อยู่ แค่ไม่มีใครเก็บค่าไว้
- iota
- อ่านว่า ไอ-โอ-ตา = ลำดับของ ConstSpec (ปกติ = หนึ่งบรรทัด ตามหมายเหตุในหัวข้อ 2)
ใน
constblock · พูดว่า “ชุดนี้ให้ iota นับให้” จะสื่อทันทีว่าจะทำอะไร - const block
- กลุ่ม
const ( ... )หนึ่งวงเล็บ = “ถนนหนึ่งสาย” · เวลาเลขไม่ตรงที่คิด ให้ถามว่า “มันอยู่ const block เดียวกันหรือคนละอัน” - enum
- ชุดตัวเลือกจำกัดที่ตั้งชื่อไว้ เช่น วันในสัปดาห์ สถานะออเดอร์ · Go ไม่มี keyword นี้
ถามว่า “ทำ enum ใน Go ยังไง” แล้วจะได้คำตอบเป็น
iota+ type ของตัวเอง (หัวข้อ 5) - blank identifier
- ชื่อทางการของ
_· พูดว่า “ทิ้งค่านั้นด้วย blank identifier” ตรงกว่าพูดว่า “ใส่ขีดล่างไว้”
4 · iota ทำเลขคณิตได้ด้วย
ก่อนไปต่อ ต้องรู้จักเครื่องหมาย << ก่อนหนึ่งตัว
ชื่อเต็มของมันคือ "เลื่อนบิตไปทางซ้าย"
แต่จำแบบเลขคณิตง่ายกว่า: 1 << n คือ
เอา 1 คูณ 2 ซ้ำ n ครั้ง (เท่ากับ 2 ยกกำลัง n)
1<<0 = 1 · 1<<1 = 2 ·
1<<2 = 4 · 1<<10 = 1024
const (
_ = 1 << (iota * 10) // 1 << 0 = 1 (ทิ้ง)
KB // 1 << 10 = ?
MB // 1 << 20 = ?
GB // 1 << 30 = ?
)
KB, MB, GB มีค่าเท่าไหร่?
KB = 1024
MB = 1048576
GB = 1073741824
นิพจน์ 1 << (iota * 10) ถูกทำซ้ำทุกบรรทัด โดย iota
เปลี่ยนไปเรื่อย ๆ ไล่ทีละบรรทัดได้แบบนี้:
บรรทัดแรก iota = 0 → 1<<(0*10) =
1<<0 = 1 → ทิ้งด้วย _
บรรทัด KB: iota = 1 → 1<<10 =
210 = 1024
บรรทัด MB: iota = 2 → 1<<20 =
1024×1024 = 1048576
บรรทัด GB: iota = 3 → 1<<30 =
1024×1024×1024 = 1073741824
นี่คือสำนวนที่เจอในโค้ด Go จริงบ่อยมาก — เขียน 4 บรรทัด ได้ค่าที่ถูกต้อง โดยไม่ต้องพิมพ์เลข 1073741824 ด้วยมือ (ซึ่งพิมพ์ผิดง่าย)
5 · ยกระดับ: ทำให้ enum มี type ของตัวเอง
ปัญหาของ iota เปล่า ๆ คือมันเป็นแค่ int —
ใครจะเอาเลขอะไรมาใส่ก็ได้ ทางแก้คือสร้าง type ขึ้นมาเอง:
type Weekday int // ← สร้าง type ใหม่จาก int
const (
Sunday Weekday = iota
Monday
Tuesday
)
func schedule(d Weekday) { ... }
schedule(Monday) // ✓
schedule(99) // ✓ ยังผ่านอยู่ (untyped constant)
n := 99
schedule(n) // ✗ compile error — n เป็น int ไม่ใช่ Weekday
บท 02 บอกว่า Go ไม่แปลง type ให้อัตโนมัติ — ยังจริงอยู่ แต่มีข้อยกเว้นหนึ่งข้อ:
ตัวเลขที่เขียนดิบ ๆ ในโค้ด อย่าง 99 ยังไม่มี type ติดตัว
Go เรียกมันว่า untyped constant — มันจะกลายเป็น type อะไรก็ได้
ณ ตอนที่เอาไปใช้ ตรงนี้จึงกลายเป็น Weekday ให้เอง
แต่พอเขียน n := 99 ปุ๊บ n ถูกตีตราเป็น
int ถาวรทันที — และ int กับ Weekday
คนละ type กัน Go จึงไม่ยอมให้ส่ง
(cannot use n (variable of type int) as Weekday value)
นี่คือกฎ "ไม่แปลงอัตโนมัติ" ที่ทำงานตามปกติ
ยังไม่กันได้ 100% (untyped constant อย่าง 99 ยังผ่าน ตามกล่องข้างบน)
แต่กันการส่งตัวแปร int ผิดตัวเข้าไปได้
ซึ่งเป็นบั๊กที่เจอบ่อยกว่ามาก
แถมทำให้ signature ของฟังก์ชันอ่านรู้เรื่องขึ้นเยอะ
(signature = หน้าตาของฟังก์ชัน: ชื่อ + ชนิดของสิ่งที่รับเข้าและคืนออก)
— เห็น schedule(d Weekday) ก็รู้เลยว่าต้องส่งวัน ไม่ใช่เลขอะไรก็ได้
คูปองส่วนลดใบหนึ่ง ช่อง "ใช้ได้ที่เคาน์เตอร์ไหน" ยังว่างอยู่ → ยื่นที่เคาน์เตอร์ไหนก็ได้
พนักงานเติมชื่อเคาน์เตอร์ตัวเองให้ตอนรับ
แต่ถ้าเราเขียนเองไปแล้วว่า "เฉพาะเคาน์เตอร์ A" ก็หมดสิทธิ์ที่เคาน์เตอร์ B ทันที
99 ที่พิมพ์ดิบ ๆ ในโค้ด = คูปองช่องยังว่าง → schedule(99) ผ่าน
เพราะมันยอมกลายเป็น Weekday ให้ตรงกับที่ฟังก์ชันขอ
n := 99 = เขียนคำว่า int ลงคูปองถาวรตั้งแต่ตอนนั้น →
schedule(n) จึงไม่ผ่าน
ที่ไม่เหมือนกัน: คูปองเปล่าเขียนอะไรลงไปก็ได้ แต่ค่าที่ยังไม่มี type
กลายร่างได้เฉพาะ type ที่เก็บค่านั้นไหว — 99 เป็น
Weekday (ซึ่งสร้างจาก int) ได้ แต่เป็น string ไม่ได้
และคูปองใบหนึ่งใช้ได้ครั้งเดียว แต่ 99 ที่เขียนไว้ในโค้ดเหมือนคูปองที่ปั๊มใบใหม่ได้ทุกครั้งที่ใช้
— ที่จุดหนึ่งกลายเป็น Weekday อีกจุดกลายเป็น float64 ได้พร้อมกัน
ไม่ได้ถูกล็อกจากการใช้ครั้งแรก (ที่ถูกล็อกคือ n เพราะมันเป็นตัวแปร ไม่ใช่ค่าคงที่)
- untyped constant
- “คูปองที่ช่องยังว่าง” — ค่าคงที่ที่ยังไม่ถูกผูก type · ครอบทั้งเลข/สตริงที่พิมพ์ตรง ๆ อย่าง
99และconstที่ประกาศโดยไม่ระบุ type อย่างconst MaxDay = 6(จึงเอาไปใส่ตัวแปรfloat64ได้ทั้งที่ดูเหมือน int) · ตรงข้ามกับ typed constant ที่ระบุ type ไว้แล้วอย่างSunday Weekday = iotaในหัวข้อ 5 · เวลางงว่าทำไมบางทีผ่านบางทีไม่ผ่าน ให้ถามว่า “ตรงนี้เป็น untyped constant หรือตัวแปรที่มี type แล้ว” - defined type
- type ใหม่ที่สร้างจาก type เดิม เช่น
type Weekday int· พูดว่า “ทำเป็น defined type ครอบ int ไว้” จะสื่อกว่า “ตั้งชื่อใหม่ให้ int” เพราะ Go ถือว่ามันคนละ type กับintจริง ๆ - type safety
- compiler กันไม่ให้เอาค่าคนละชนิดมาใช้ปนกัน — ทั้งตอนส่งเข้าฟังก์ชัน ตอน assign และตอนคำนวณ · ข้อดีคือบั๊กแบบนี้ตายตั้งแต่ compile time ไม่ไปโผล่ตอน runtime · ถูกถามว่าทำไมต้องสร้าง type เพิ่ม ตอบได้ว่า “ทำเพื่อ type safety”
6 · ทดสอบความจำ
1. const Now = time.Now() จะเกิดอะไรขึ้น?
const ต้องรู้ค่าตั้งแต่ compile time แต่
time.Now() เป็นการเรียกฟังก์ชันที่รู้ค่าตอน runtime →
const initializer time.Now() is not a constant
2. const ( a = iota; b; c ) — c มีค่าเท่าไหร่?
iota เริ่มที่ 0 → a=0, b=1, c=2 และบรรทัดที่ไม่ได้
เขียนอะไร Go จะทำซ้ำนิพจน์ = iota จากบรรทัดบนให้เอง
3. มี const block สองอันติดกัน — iota ในอันที่สองเริ่มที่เท่าไหร่?
iota reset เป็น 0 ทุกครั้งที่เจอ const block ใหม่
— จึงแยกกลุ่ม enum ที่ไม่เกี่ยวกันด้วยการขึ้น block ใหม่ได้เลย
4. ใส่ _ แทนชื่อในบรรทัดหนึ่งของ const block มีผลยังไง?
_ กินลำดับไปด้วย แค่ไม่สร้างชื่อ — ใช้ตอนอยากให้ค่าจริงเริ่มที่ 1
หรืออยากทิ้งค่าแรก (เช่นสำนวน _ = 1 << (iota * 10) ที่ทิ้ง 1 byte
เพื่อให้ KB เป็นตัวแรกที่มีความหมาย)
7 · ลงมือทำ
ภารกิจ: เขียน Rock-Paper-Scissors ด้วย iota
นี่คือ workshop จากสไลด์ Day 2 — ใช้ได้แค่ iota + if/else if/else
- ประกาศ
type Move intแล้วสร้าง const block:Rock,Paper,Scissorsด้วยiota - ประกาศ
playerAและplayerBเป็นMove - เขียนเงื่อนไขให้ได้ 3 ผลลัพธ์: เสมอ, ผู้เล่น A ชนะ, ผู้เล่น B ชนะ
- ทดสอบครบทั้ง 9 คู่ (3×3) ว่าถูกทุกกรณี
- ท้าทาย 1: ทำให้พิมพ์ชื่อท่าออกมาเป็นคำ (
"Rock") แทนตัวเลข — คำใบ้: สร้าง "ตารางแปลงเลขเป็นชื่อ" ไว้ในฟังก์ชัน แล้วเอาค่าMoveไปเปิดตาราง โดยเขียนชื่อในวงเล็บเหลี่ยมต่อท้ายnames := []string{"Rock", "Paper", "Scissors"} fmt.Println(names[playerA]) // Rock = ช่องที่ 0, Paper = ช่องที่ 1, Scissors = ช่องที่ 2 - ท้าทาย 2: เขียนเงื่อนไขชนะให้เหลือบรรทัดเดียว —
คำใบ้: ในเกมนี้ A ชนะเมื่อ
(A - B + 3) % 3 == 1ลองพิสูจน์ว่าทำไม
เห็น const (...iota...) ในโค้ดใครก็ตาม คุณอ่านออกทันทีว่า
แต่ละบรรทัดมีค่าเท่าไหร่ โดยไม่ต้องนั่งนับนิ้ว
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ: Go spec — Iota สั้นมาก (ครึ่งหน้า) และมีตัวอย่างที่ตอบทุกคำถามที่จะเกิดขึ้น อ่านคู่กับ Constants ถ้าอยากเข้าใจว่า "untyped constant" ทำงานยังไง
💬 ติดตรงไหนถามได้เลย — ท้าทายข้อ 2 (สูตร modulo) ถ้าคิดไม่ออกว่าทำไมถึงใช้ได้ ถามมาได้ มันเป็นเทคนิคที่ใช้กับเกม "วงกลมแพ้ชนะ" ได้ทุกเกม
อ้างอิง
- Go spec — Iota — นิยามว่า
iotaคือลำดับของ ConstSpec - Go spec — Constant declarations — กฎการทำซ้ำนิพจน์เมื่อบรรทัดว่าง
- The Go Blog — Constants — untyped constant ทำงานยังไง
- A Tour of Go — Constants