1 · ปัญหา: บันไดที่ไต่ไปเรื่อย ๆ
โค้ดอนุมัติสินเชื่อจากสไลด์ Day 2 — ลองอ่านแล้วตอบว่า "เงื่อนไขไหนทำให้อนุมัติ"
if age >= 20 {
if hasJob {
if income >= 15000 {
if creditScore >= 600 {
fmt.Println("อนุมัติ")
} else {
if creditScore >= 500 {
fmt.Println("อนุมัติแบบมีเงื่อนไข")
} else {
fmt.Println("ปฏิเสธ: เครดิตต่ำ")
}
}
} else {
fmt.Println("ปฏิเสธ: รายได้ไม่ถึง")
}
} else {
fmt.Println("ปฏิเสธ: ไม่มีงาน")
}
} else {
fmt.Println("ปฏิเสธ: อายุไม่ถึง")
}
สังเกตอาการ 3 อย่าง:
- ต้องอ่าน ลงไป 5 ชั้น ถึงจะเจอ "อนุมัติ" — ซึ่งเป็นเคสที่เกิดบ่อยที่สุด
elseอยู่ห่างจากifของมัน 10 กว่าบรรทัด- เพิ่มเงื่อนไขใหม่ 1 ข้อ = ขยับโค้ดทั้งก้อนเข้าไปอีกหนึ่งชั้น
2 · ทางแก้: พลิกเงื่อนไข แล้วออกก่อน
if age < 20 {
fmt.Println("ปฏิเสธ: อายุไม่ถึง")
return
}
if !hasJob {
fmt.Println("ปฏิเสธ: ไม่มีงาน")
return
}
if income < 15000 {
fmt.Println("ปฏิเสธ: รายได้ไม่ถึง")
return
}
if creditScore < 500 {
fmt.Println("ปฏิเสธ: เครดิตต่ำ")
return
}
if creditScore < 600 {
fmt.Println("อนุมัติแบบมีเงื่อนไข")
return
}
fmt.Println("อนุมัติ") // ← ชั้นเดียว อยู่ล่างสุด อ่านเจอทันที
- ไม่มี
elseเลยสักตัว - ทุกเงื่อนไขอยู่ ชั้นเดียวกันหมด
- อ่านจากบนลงล่างเหมือน checklist: "ไม่ผ่านข้อนี้ → ออก"
- เพิ่มเงื่อนไขใหม่ = แทรก 4 บรรทัด ไม่ต้องขยับอะไรเลย
ที่สนามบิน เขาไม่ได้พาเราเดินเข้าไปถึงตัวเครื่องบินก่อน แล้วค่อยมาไล่ออกทีหลัง
เขาตั้งด่านเรียงกันเป็นแถว — เช็คอิน → ตรวจบัตรประชาชน → สแกนกระเป๋า → ตรวจตั๋วที่ประตู
ตกด่านไหน ก็จบตรงด่านนั้น ไม่ต้องเดินต่อ ·
ส่วนคนที่เดินมาถึงประตูได้ แปลว่าผ่านครบทุกด่านแน่นอน ไม่ต้องตรวจซ้ำอีก
guard clause คือการเรียงด่านแบบนี้ · if ... { return } หนึ่งก้อน = ด่านหนึ่งด่าน
ส่วน if ซ้อน 5 ชั้นในหัวข้อ 1 คือการพาทุกคนเดินเข้าไปให้ลึกที่สุดก่อน
แล้วค่อยตัดสินว่าจะไล่กลับตรงไหน
ที่ไม่เหมือนกัน: ผู้โดยสารที่ตกด่านยังเดินกลับไปแก้ของ แล้วเข้าคิวใหม่ได้
แต่ return คือจบแล้วจบเลย ไม่มีย้อนกลับ —
บรรทัดที่เหลือในเส้นทางปกติของฟังก์ชันนั้นจะไม่ถูกรันอีก ·
ข้อยกเว้นเดียวคือ defer ที่ลงทะเบียนไว้ก่อน return
— ก้อนนั้นยังทำงานเสมอ แม้จะออกไปแล้ว (เรื่องนี้อยู่ใน
บทเรียน 08)
เวลาไปถาม dev คนอื่นหรือถาม AI ถ้าใช้คำให้ตรง จะได้คำตอบที่ตรง — คำพวกนี้คือคำที่เขาใช้เรียกสิ่งที่คุณเพิ่งเข้าใจไป
- guard clause
- “ด่านตรวจ” ที่วางไว้ต้นฟังก์ชัน — เช็คเคสที่ไม่ผ่านแล้ว
returnออกทันที · เวลาขอความช่วยเหลือให้พูดว่า “ช่วย refactor ก้อนนี้เป็น guard clause ให้หน่อย” จะได้ตรงกว่าพูดว่า “ทำให้มันอ่านง่ายขึ้นหน่อย” - early return
- การ
returnออกกลางทาง ก่อนถึงบรรทัดสุดท้ายของฟังก์ชัน · เป็นกลไกที่ทำให้ guard clause ทำงานได้ - happy path
- “เส้นทางปกติ” ที่ผ่านทุกด่าน — ในตัวอย่างนี้คือบรรทัด
อนุมัติ· ประโยคที่จะได้ยินบ่อยคือ “เอา happy path ไว้ชั้นนอกสุด” - nesting
- “ซ้อนกันกี่ชั้น” = จำนวนชั้นของโครงสร้าง (
ifในifในif) · เป็นเรื่องของตรรกะ · แทนที่จะบอกว่า “มันซ้อนกันเยอะ” ให้พูดว่า “ตรงนี้ nesting ลึก 5 ชั้น” - indentation level
- “ระยะเยื้องที่ตาเห็น” — เป็นเรื่องของการแสดงผล ที่ปกติสะท้อน nesting · ระวังอย่าใช้สลับกัน: ถ้าอยากให้เขาช่วยลดชั้นตรรกะแต่ไปพูดว่า “ช่วยลด indentation หน่อย” อาจได้คำตอบเป็นเรื่องจัด format แทน
- refactor
- แก้โครงของโค้ดให้ดีขึ้น โดยผลลัพธ์ต้องเหมือนเดิมเป๊ะ · ถ้าผลลัพธ์เปลี่ยนไป อันนั้นไม่เรียกว่า refactor แล้ว แต่คือเปลี่ยนพฤติกรรมของโปรแกรม
3 · สูตรแปลง 3 ขั้น
| เงื่อนไขเดิม | พลิกเป็น |
|---|---|
x >= 20 | x < 20 |
x > 0 | x <= 0 |
hasJob | !hasJob |
err == nil | err != nil |
a && b | !a || !b |
a || b | !a && !b |
สองแถวล่างมีชื่อเรียกว่า กฎเดอมอร์แกน — คิดเป็นภาษาคนจะเห็นเหตุผลทันที:
- "ต้องมีทั้งตั๋วและบัตร" (
a && b) ตรงข้ามคือ "ขาดอย่างใดอย่างหนึ่งก็ตกแล้ว" (!a || !b) — ไม่ใช่ "ขาดทั้งสองอย่าง" - "มีอย่างใดอย่างหนึ่งก็พอ" (
a || b) ตรงข้ามคือ "ต้องไม่มีเลยทั้งคู่" (!a && !b)
จะกดเงินได้ต้องมีทั้งบัตร และ รหัสที่ถูก — เขียนเป็นเงื่อนไขคือ
hasCard && pinOK
ทีนี้ลองถามกลับด้านว่า “แบบไหนที่กดไม่ได้”
คำตอบคือ ขาดอย่างใดอย่างหนึ่งก็จบแล้ว — ไม่มีบัตรก็จบ · มีบัตรแต่ใส่รหัสผิดก็จบ
ไม่ใช่ว่าต้อง “ไม่มีบัตรและรหัสผิดพร้อมกัน” ถึงจะกดไม่ได้ — นี่แหละคือเหตุผลที่
&& พลิกแล้วต้องกลายเป็น ||
ที่ไม่เหมือนกัน: ตู้ ATM จอขึ้นบอกเราได้ว่าพลาดเพราะอะไร
แต่เงื่อนไขที่รวบเป็นก้อนเดียวอย่าง if !hasCard || !pinOK
บอกได้แค่ว่า “ไม่ผ่าน” ไม่รู้ว่าตกข้อไหน —
เป็นเหตุผลที่ guard clause นิยมแยกเป็น if ทีละข้อ จะได้บอกสาเหตุได้ตรงจุด
- กฎเดอมอร์แกน (De Morgan's law)
- ชื่อทางการของกฎพลิกเงื่อนไขที่มี
&&/||· ใช้ได้ทุกภาษา ไม่ใช่แค่ Go · เวลาไม่แน่ใจให้ถามตรง ๆ ว่า “ช่วยพลิกเงื่อนไขนี้ด้วยกฎเดอมอร์แกนให้หน่อย” - negate (พลิกเงื่อนไข)
- ทำให้เงื่อนไขกลับด้าน เช่น
x >= 20เป็นx < 20· เครื่องหมาย!อ่านว่า “not” หน้าที่ของมันคือสลับ true ↔ false - logic bug
- บั๊กที่ compile ผ่าน รันได้ แต่คำตอบผิด (compiler = ตัวแปลโค้ดให้เครื่องรัน · มันตรวจแค่ว่าเขียนถูกไวยากรณ์ไหม ไม่ได้ตรวจว่าเราคิดถูกไหม) · เวลาขอให้ช่วยดู ให้พูดว่า “น่าจะเป็น logic bug ไม่ใช่ compile error” อีกฝ่ายจะรู้ทันทีว่าต้องไล่ตรรกะ ไม่ใช่ไล่ syntax
4 · นี่คือมาตรฐานทางการของ Go
Go Code Review Comments มีหัวข้อชื่อ Indent Error Flow เขียนไว้ว่า:
"Try to keep the normal code path at a minimal indentation, and indent the error handling, dealing with it first."
แปลว่า เส้นทางปกติต้องอยู่ชิดซ้ายที่สุด ส่วน error/ข้อยกเว้น ให้จัดการก่อนแล้วออกไป นี่คือเหตุผลที่โค้ด Go ทุกที่หน้าตาแบบนี้:
ตัวอย่างจริงที่จะเจอทุกไฟล์ — ตอนนี้ยังไม่ต้องเข้าใจทุกบรรทัด ให้ดูแค่รูปทรง:
f, err := os.Open(name) // เปิดไฟล์ — คืนสองค่า: ตัวไฟล์ กับ "ผิดพลาดไหม"
if err != nil { // ถ้าผิดพลาด...
return err // ...จัดการแล้วออกทันที ← นี่คือ guard clause
}
// เส้นทางปกติอยู่ชั้นนอกสุด ต่อไปยาว ๆ ได้เลย
5 · เพื่อนของ guard clause: switch
ถ้าเงื่อนไขทั้งหมดคือ "เทียบตัวแปรตัวเดียวกับหลายค่า"
ใช้ switch จะสะอาดกว่า guard clause อีก
| สถานการณ์ | ใช้ |
|---|---|
| คำถาม true/false เดียว | if |
| เลือกสองทาง | if-else |
| ตรวจหลายช่วง / หลายเงื่อนไขต่างกัน | guard clause |
| เทียบตัวแปรเดียวกับหลายค่า | switch |
// ก่อน — if ซ้อน
if status == "pending" { ... } else {
if status == "approved" { ... } else {
if status == "rejected" { ... } else { ... }
}
}
// หลัง — switch
switch status {
case "pending": fmt.Println("Waiting for approval")
case "approved": fmt.Println("Request approved")
case "rejected": fmt.Println("Request rejected")
default: fmt.Println("Unknown status")
}
- ไม่ต้องเขียน
break— จบ case แล้วออกเอง (ตรงข้ามกับ C/Java ที่ลืม break แล้วบั๊ก) - ใส่หลายค่าใน case เดียวได้:
case "Tue", "Wed", "Thu": - อยากให้ไหลลง case ถัดไปต้องสั่ง
fallthroughเอง
switch status := strings.ToUpper("approved"); status {
case "PENDING":
fmt.Println("Waiting for approval")
case "APPROVED":
fmt.Println("Request approved")
fallthrough
case "REJECTED":
fmt.Println("Request rejected")
default:
fmt.Println("Unknown status")
}
พิมพ์อะไรออกมาบ้าง?
Request approved
Request rejected
fallthrough ทำให้ไหลลง case ถัดไป โดยไม่เช็คเงื่อนไขของ case
นั้นเลย — และหยุดแค่ case เดียว ไม่ไหลต่อไปถึง default
สังเกตอีกอย่าง: switch x := ...; x { ใส่ short statement
ได้เหมือน if — ตัวแปรมีชีวิตเฉพาะใน switch นั้น
โบนัส: if ใส่คำสั่งนำหน้าได้
if n := len(name); n > 10 {
fmt.Println("ชื่อยาวไป", n)
} else {
fmt.Println("ok", n) // ← n ใช้ใน else ได้ด้วย
}
fmt.Println(n) // ✗ undefined: n — ปิดปีกกาแล้ว n ตายไปเลย
อ่านว่า "ทำ n := len(name) ก่อน แล้วค่อยเช็คเงื่อนไข"
n มีชีวิตเฉพาะใน if กับ else ของก้อนนั้น พอปิดปีกกาก็หายไป
ประโยชน์คือจำกัดอายุตัวแปรให้สั้นที่สุด ลดโอกาสเผลอเอาไปใช้ผิดที่
เข้าออฟฟิศคนอื่น เราแลกบัตรประชาชนกับบัตรผู้มาติดต่อที่ป้อมยาม
บัตรใบนั้นใช้ได้เฉพาะในตึกนั้น ในวันนั้น · เดินออกจากตึกเมื่อไหร่ ก็ต้องคืน ใช้ต่อไม่ได้
ตัวแปรที่ประกาศแบบ if n := len(name); n > 10 { ก็คือบัตรผู้มาติดต่อ —
ปีกกาเปิด = รับบัตร · ปีกกาปิด = คืนบัตร · เอาไปใช้ข้างนอกจะโดนฟ้องว่า undefined: n
ที่ไม่เหมือนกัน: บัตรผู้มาติดต่อเราต้องเดินไปคืนเอง แต่ Go
ตัดสิทธิ์ให้เองทันทีที่ปิดปีกกา ไม่มีทางลืมคืน ·
ที่ต่างอีกอย่างคือสิ่งที่หมดอายุคือชื่อ n ไม่ใช่ค่า —
เรียกชื่อข้างนอกไม่ได้อีก แต่ถ้ายังมีใครถือค่านั้นอยู่ (เช่นคืน pointer ออกไป)
ค่าก็ยังอยู่ได้ ·
อีกอย่างที่ต่างกัน: ถ้าข้างนอกมีตัวแปรชื่อ n อยู่แล้ว
ตัวข้างในจะบังตัวข้างนอกไว้ชั่วคราว ไม่ได้ทับของเดิม — เรื่องนี้ชื่อว่า shadowing อยู่ใน
บทเรียน 05
- scope
- “ขอบเขตที่ตัวแปรมีชีวิตอยู่” · แทนที่จะถามว่า “ตัวแปรนี้ใช้ได้ถึงไหน” ให้พูดว่า “ตัวแปรนี้ scope แค่ใน if block ใช่ไหม” จะได้คำตอบเร็วกว่ามาก
- block
- โค้ดที่อยู่ในปีกกาคู่หนึ่ง
{ ... }· ปกติตัวแปรมีชีวิตแค่ใน block ที่ประกาศมัน · ข้อยกเว้นที่เจอบ่อย คือตัวแปรที่ประกาศใน short statement หน้าif— ตัวนั้น scope ครอบทั้งifและelse(คือข้ามปีกกาสองคู่) อย่างที่เห็นในโค้ดข้างบน - short statement
- คำสั่งสั้น ๆ ที่แปะไว้หน้าเงื่อนไขแล้วคั่นด้วย
;· ใช้ได้กับifและswitch·forก็มีช่องเดียวกันนี้ (for i := 0; i < n; i++) แต่ตรงนั้นคนเรียกว่า init statement — จะเจอในบทหลัง · ถ้าอยากได้โค้ดสำนวนนี้ให้ขอว่า “เขียนแบบ if with short statement ให้หน่อย” - no fallthrough by default
- “
caseของ Go ไม่ไหลลง case ถัดไปเอง เหมือน Go ใส่breakให้” · นี่คือสำนวนที่ตรงกับเอกสารทางการที่สุด · เวลาคุยกับคนที่มาจาก C/Java ให้บอกว่า “switch ของ Go ไม่ fallthrough โดยปริยาย” ประโยคเดียวจบ · บางคนเรียกสั้น ๆ ว่า implicit break ซึ่งสื่อกันรู้เรื่อง แต่ไม่ใช่คำใน spec
6 · ทดสอบความจำ
1. หลักการของ guard clause คืออะไร?
พลิกเงื่อนไขให้เป็นเคสที่ "ไม่ผ่าน" → จัดการ → return ออกไปเลย
ทำให้เส้นทางปกติเหลืออยู่ชั้นนอกสุด ตรงกับ Indent Error Flow
ในเอกสารทางการ
2. พลิก if hasTicket && hasID { เป็น guard clause ต้องเขียนยังไง?
กฎเดอมอร์แกน: !(a && b) = !a || !b —
"ต้องมีทั้งตั๋วและบัตร" พลิกเป็น "ขาดอย่างใดอย่างหนึ่ง"
ไม่ใช่ "ขาดทั้งสองอย่าง"
ถ้าพลิกผิดเป็น && คนที่มีตั๋วแต่ไม่มีบัตร
จะผ่านเข้าไปได้ — บั๊กที่ compiler จับไม่ได้
3. switch ของ Go ที่ไม่เขียน break จะเป็นยังไง?
Go ใส่ break ให้โดยปริยาย — กลับด้านกับ C/Java
ถ้าอยากให้ไหลต่อต้องสั่ง fallthrough เอง ซึ่งเป็นดีไซน์ที่ตั้งใจ
เพราะ "ลืม break" เป็นบั๊กคลาสสิกของ C
4. ตัวแปรที่ประกาศใน if x := f(); cond { ใช้ได้ถึงไหน?
scope ครอบคลุมทั้ง if และ else
แต่หลังปิดปีกกาแล้วหายไป — เรียกใช้ต่อจะได้
undefined: x
ประโยชน์: จำกัดอายุตัวแปรให้สั้นที่สุด ลดโอกาสเผลอใช้ผิดที่
7 · ลงมือทำ
ภารกิจ: refactor ระบบเข้างาน (workshop จากสไลด์)
โจทย์: ทำให้เหลือ condition ชั้นเดียว และ ไม่มี else เลย
age := 22
hasTicket := true
hasID := true
isVIP := false
if age >= 18 {
if hasTicket {
if hasID {
if isVIP {
fmt.Println("เข้างานได้: ผ่านประตู VIP")
} else {
fmt.Println("เข้างานได้: ผ่านประตูทั่วไป")
}
} else {
fmt.Println("ปฏิเสธ: ไม่มีบัตรประชาชน")
}
} else {
fmt.Println("ปฏิเสธ: ไม่มีตั๋ว")
}
} else {
fmt.Println("ปฏิเสธ: อายุไม่ถึง 18 ปี")
}
- รันโค้ดเดิมก่อน จดผลลัพธ์ไว้ — จะได้เทียบทีหลัง
- refactor เป็น guard clause ทีละชั้น (อย่าทำรวดเดียว)
- รันใหม่ ต้องได้ผลเหมือนเดิมเป๊ะ
- ทดสอบให้ครบ: ลองเปลี่ยนค่าให้ครบทั้ง 5 เส้นทาง (อายุไม่ถึง / ไม่มีตั๋ว / ไม่มีบัตร / ทั่วไป / VIP)
- นับบรรทัดและนับชั้นย่อหน้าเทียบก่อน–หลัง
- ท้าทาย: โค้ดข้างบนอยู่ใน
mainซึ่งreturnใช้ได้อยู่แล้ว (แต่มันจะจบโปรแกรมทั้งโปรแกรม) — ลองย้ายมาเป็นฟังก์ชันแยกที่ คืน string ออกมาแทนการPrintln:func checkEntry(age int, hasTicket, hasID, isVIP bool) string
ข้อดี:returnจะจบแค่ฟังก์ชันนี้ ไม่จบทั้งโปรแกรม และเรียกซ้ำหลายเคสรวดเดียวได้ ทำให้เทสต์ครบ 5 เส้นทางในการรันครั้งเดียว
เห็นโค้ดย่อหน้าลึกเกิน 2 ชั้นเมื่อไหร่ มือคุณจะคันอยากพลิกเป็น guard clause ทันที — และนั่นคือนิสัยที่ทำให้โค้ดผ่าน review รอบเดียว
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ: Go Code Review Comments — หัวข้อ Indent Error Flow คือที่มาของบทนี้ทั้งบท อ่านทั้งหน้าไปเลยเพราะสั้น และเป็นเกณฑ์ที่ reviewer จริงใช้
💬 ติดตรงไหนถามได้เลย — ถ้า refactor แล้วผลลัพธ์เพี้ยนจากเดิม
ส่วนใหญ่มาจากพลิก && / || ผิด ส่งโค้ดมาได้
เดี๋ยวช่วยไล่ทีละเงื่อนไข
อ้างอิง
- Go Code Review Comments — Indent Error Flow
- Go spec — Switch statements — implicit break, multi-value case, fallthrough
- A Tour of Go — If with a short statement
- Effective Go — if — สำนวนการ return ก่อน