Control Flow
if — พร้อม Init Statement
หัวข้อที่มีชื่อว่า “if — พร้อม Init Statement”if statement ใน Go คล้ายกับภาษาอื่น แต่ parenthesis รอบ condition ถูกตัดออก — ส่วน brace ต้องมีเสมอ:
if x > 0 { fmt.Println("positive")}Go เพิ่ม init statement คั่นด้วย semicolon ก่อน condition ตัวแปรที่ประกาศใน init statement มี scope ครอบทั้ง if/else block:
if err := doWork(); err != nil { fmt.Println("error:", err) return}// err is not in scope herePattern นี้ใช้บ่อยมากกับ operation ที่ return (value, error) — มันทำให้ตัวแปร error อยู่ใกล้กับจุดตรวจสอบและไม่ทำให้ scope ข้างนอกรก
for — Loop Keyword เดียวของ Go
หัวข้อที่มีชื่อว่า “for — Loop Keyword เดียวของ Go”Go มี loop keyword เดียวคือ for ซึ่งรองรับสามรูปแบบ:
รูปแบบที่ 1: C-style (init; condition; post)
for i := 0; i < 5; i++ { fmt.Println(i)}รูปแบบที่ 2: Condition-only (เทียบเท่า while)
n := 1for n < 100 { n *= 2}รูปแบบที่ 3: Infinite loop
for { if done() { break }}for range
หัวข้อที่มีชื่อว่า “for range”for range ใช้สำหรับ iterate ผ่าน collections:
fruits := []string{"apple", "banana", "cherry"}for i, v := range fruits { fmt.Println(i, v)}
// Discard the index with _for _, v := range fruits { fmt.Println(v)}
// Range over a string yields runes (Unicode code points), not bytesfor i, r := range "Go!" { fmt.Printf("%d: %c\n", i, r)}range ยังใช้กับ map (key, value) และ channel (value จนกว่าปิด) ได้ด้วย
ตั้งแต่ Go 1.23 range ยังขับเคลื่อน for loop บน function ที่มี signature พิเศษได้ด้วย — นี่คือรากฐานของ package iter และ custom iterator ซึ่งอธิบายเต็มในบท Iterators เมื่อคุณเห็น type system มากขึ้นแล้ว
Range บน integer และ loop variable แบบต่อรอบ
หัวข้อที่มีชื่อว่า “Range บน integer และ loop variable แบบต่อรอบ”ตั้งแต่ Go 1.22 range รับ integer ได้ด้วย ใช้แทน counting loop แบบเดิมเมื่อต้องการแค่ index:
for i := range 5 { // 0, 1, 2, 3, 4 fmt.Println(i)}for range 3 { // repeat three times, no variable fmt.Println("tick")}Go 1.22 ยังเปลี่ยน semantics ของ loop variable ด้วย: แต่ละรอบได้ i (และ v) ตัวใหม่ ดังนั้น closure และ goroutine ที่สร้างใน loop จะไม่เห็นค่าสุดท้ายพร้อมกันอีกต่อไป การเขียน i := i เพื่อ copy จึงไม่จำเป็นแล้วใน module ที่ go.mod ระบุ go 1.22 ขึ้นไป — การเทียบก่อน/หลังแบบเต็มพร้อม output จริงทั้งสองแบบอยู่ในบท Functions
Labeled break และ continue
หัวข้อที่มีชื่อว่า “Labeled break และ continue”break หรือ continue ธรรมดาจะใช้กับ for ที่อยู่ในสุดเสมอ Label — identifier ตามด้วย colon วางไว้บรรทัดก่อน for — ให้คุณตั้งชื่อ loop นั้นและเล็งเป้าจาก loop ที่ซ้อนอยู่ข้างในได้:
outer:for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { if j == 1 { continue outer // skip to the next i, not the next j } if i == 2 { break outer // exit both loops entirely } fmt.Println(i, j) }}continue outer ข้ามส่วนที่เหลือของ loop ข้างในไปพร้อมกับส่วนที่เหลือของ iteration นั้นใน loop ข้างนอกด้วย; break outer ออกจากทั้งสอง loop ทันที
break ใน switch หรือ select ไม่ออกจาก Loop
หัวข้อที่มีชื่อว่า “break ใน switch หรือ select ไม่ออกจาก Loop”นี่คือหนึ่งใน bug ที่พบบ่อยที่สุดใน Go code ที่ generate มา break ธรรมดาจะออกจาก switch หรือ select ที่อยู่ในสุด เท่านั้น ไม่ใช่ for ที่ครอบอยู่ — แม้ว่าคนส่วนใหญ่ที่เขียนมันตั้งใจจะหยุด loop ก็ตาม:
for i := 0; i < 3; i++ { switch i { case 1: break // exits only this switch — the loop keeps going default: fmt.Println("processing", i) }}โค้ดนี้ print processing 0 แล้วตามด้วย processing 2 — case 1 รัน break ของมัน ซึ่งไม่ทำอะไรนอกจากออกจาก switch แล้ว for loop ก็เดินหน้าไปที่ i = 2 ตามปกติ กฎเดียวกันใช้กับ select ที่อยู่ใน for ด้วย (อธิบายในโมดูล concurrency เมื่อ channel ถูกสอนแล้ว): break ใน case ใด case หนึ่งของมันออกจาก select เท่านั้น ถ้าต้องการหยุด loop จริง ๆ ต้อง label loop แล้ว break label นั้น:
loop:for i := 0; i < 5; i++ { switch { case i == 3: break loop // this one really does exit the for default: fmt.Println(i) }}flowchart LR
subgraph Unlabeled["plain break inside a switch"]
direction TB
U1["for i := ..."] --> U2["switch i { case: break }"]
U2 -->|"exits switch only"| U3["loop continues to next i"]
end
subgraph Labeled["break loop (labeled)"]
direction TB
L1["loop: for i := ..."] --> L2["switch i { case: break loop }"]
L2 -->|"exits the labeled for"| L3["loop stops entirely"]
end range Copy Element และ Evaluate Expression แค่ครั้งเดียว
หัวข้อที่มีชื่อว่า “range Copy Element และ Evaluate Expression แค่ครั้งเดียว”range มีสอง behavior ที่คนใหม่มักแปลกใจ และทั้งสองสำคัญตอนอ่าน generated code:
มัน copy แต่ละ element เข้า loop variable การแก้ตัวแปรนั้นไม่เปลี่ยน collection ต้นฉบับเลย:
nums := []int{1, 2, 3}for _, v := range nums { v *= 10 // modifies the local copy only}fmt.Println(nums) // [1 2 3] — unchangedมัน evaluate expression ที่ range อยู่แค่ครั้งเดียว ตอนเริ่ม loop สำหรับ slice หมายความว่าขอบเขตของ loop ถูกกำหนดตายตัวเป็นความยาวของ slice ณ ตอนนั้น — การ append เข้า slice ระหว่าง loop body ไม่ทำให้ loop ไปเยี่ยม element ใหม่:
s := []int{1, 2, 3}for i, v := range s { if i == 0 { s = append(s, 4) // extends s, but range already captured the original length } fmt.Println(i, v)}// loop body still only runs for i = 0, 1, 2 — the appended 4 is never visitedswitch ใน Go evaluate case จากบนลงล่างและ หยุดที่ case แรกที่ตรง — ไม่มี fallthrough อัตโนมัติไปยัง case ถัดไป ไม่ต้องเขียน break ท้าย case:
switch day {case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday": fmt.Println("weekday")case "Saturday", "Sunday": fmt.Println("weekend")default: fmt.Println("unknown")}เหมือนกับ if, switch รับ init statement ก่อน condition ได้ scope ครอบทั้ง switch — สะดวกสำหรับคำนวณค่าที่จะ branch ตรงที่ switch เอง:
switch x := score(); {case x > 10: fmt.Println("big")case x > 5: fmt.Println("medium")default: fmt.Println("small")}Switch-true pattern (expressionless switch อย่างที่เห็นด้านบน) ทำหน้าที่เหมือน if-else chain ที่สะอาดกว่า แต่ละ case เป็น boolean expression อิสระ ถ้าต้องการจับหลายค่าใน case เดียว ให้ list ด้วย comma
ข้อจำกัดของ Fallthrough
หัวข้อที่มีชื่อว่า “ข้อจำกัดของ Fallthrough”ถ้าต้องการ fallthrough ไปยัง case ถัดไปอย่างชัดเจน ใช้ fallthrough เป็น statement สุดท้ายใน case มันไม่มีเงื่อนไข — มันรัน body ของ case ถัดไปไม่ว่า condition ของ case นั้นจะเป็นอย่างไร:
switch x := score(); {case x > 10: fmt.Println("big") fallthroughcase x > 5: fmt.Println("medium-or-big") // runs even though this case's own test was never evaluateddefault: fmt.Println("other")}fallthrough มีข้อจำกัดสองอย่างที่ compiler บังคับ มันปรากฏใน case สุดท้าย ของ switch ไม่ได้:
./main.go:9:3: cannot fallthrough final case in switchและใช้ใน type switch ไม่ได้เลย:
./main.go:10:3: cannot fallthrough in type switchgoto ในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “goto ในหนึ่งประโยค”goto กระโดดไปยัง labeled statement ใน function เดียวกัน มีไว้สำหรับ generated code และ cleanup pattern ที่พบไม่บ่อยเป็นหลัก — Go code ที่เขียนมือแบบ idiomatic แทบไม่ใช้มันเลย มักใช้ labeled loop, early return, หรือปรับโครงสร้าง function แทน
package main
import "fmt"
func classify(n int) string { switch { case n < 0: return "negative" case n == 0: return "zero" case n < 10: return "small" default: return "large" }}
func main() { // C-style for loop fmt.Println("-- C-style loop --") for i := 1; i <= 3; i++ { fmt.Println(i) }
// Condition-only loop (while equivalent) fmt.Println("-- condition-only loop --") n := 1 for n < 10 { n *= 2 } fmt.Println("n =", n)
// for range over slice fmt.Println("-- range over slice --") fruits := []string{"apple", "banana", "cherry"} for i, v := range fruits { fmt.Println(i, v) }
// switch fmt.Println("-- switch classify --") for _, val := range []int{-5, 0, 7, 42} { fmt.Println(val, "->", classify(val)) }
// if with init statement fmt.Println("-- if with init --") if x := 16; x%2 == 0 { fmt.Println(x, "is even") } else { fmt.Println(x, "is odd") }} Playground ตัวที่สองสาธิตทุก depth topic ข้างต้นในโปรแกรมรันได้ตัวเดียว: labeled break/continue, break ที่ติดอยู่ใน switch, พฤติกรรม copy-และ-evaluate-once ของ range, และ switch ที่มี init statement พร้อม fallthrough
package main
import "fmt"
func score() int { return 12 }
func main() { // Labeled break and continue reach an outer loop by name. fmt.Println("-- labeled continue/break --")outer: for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { if j == 1 { continue outer } if i == 2 { break outer } fmt.Println(i, j) } }
// A plain break inside a switch only exits the switch, not the loop. fmt.Println("-- break inside switch inside for --") for i := 0; i < 3; i++ { switch i { case 1: break // exits only this switch — the loop keeps going default: fmt.Println("processing", i) } }
// A labeled break reaches all the way out of the for. fmt.Println("-- labeled break exits the for --")loop: for i := 0; i < 5; i++ { switch { case i == 3: break loop default: fmt.Println(i) } }
// range copies each element; mutating v does not change the slice. fmt.Println("-- range copies elements --") nums := []int{1, 2, 3} for _, v := range nums { v *= 10 } fmt.Println(nums)
// range evaluates its expression once: appending during the loop does // not extend how many times the loop runs. fmt.Println("-- range evaluates its expression once --") s := []int{1, 2, 3} for i, v := range s { if i == 0 { s = append(s, 4) } fmt.Println(i, v) } fmt.Println("final s:", s)
// switch with an init statement, and fallthrough running the next // case unconditionally. fmt.Println("-- switch init + fallthrough --") switch x := score(); { case x > 10: fmt.Println("big") fallthrough case x > 5: fmt.Println("medium-or-big") default: fmt.Println("other") }} ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
for keyword เดียว | เรียนรู้น้อยกว่า, code สม่ำเสมอ | ต้องจำรูปแบบสามแบบ (C-style, condition-only, infinite) |
for range | อ่านง่าย ไม่ต้องจัดการ index เอง | range copy value และ evaluate expression แค่ครั้งเดียว — ทั้งคู่ทำให้แปลกใจกับ code ที่แก้ในตัวหรือ append ระหว่าง loop |
switch ไม่มี fallthrough อัตโนมัติ | ลด bug จาก forgotten break | ต้องเขียน fallthrough ชัดเจนถ้าต้องการ (rare) และใช้ใน case สุดท้ายหรือ type switch ไม่ได้ |
if/switch init statement | scope ของ err แคบที่สุด | syntax แปลกสำหรับ developer ที่มาจากภาษาอื่น |
| Labeled break/continue | ควบคุมได้แม่นยำว่า jump จะไปที่ loop ไหน | break ธรรมดาใน switch/select ที่อยู่ใน for เป็น bug คลาสสิกที่เงียบมาก |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
rangecopy ทั้ง slice” — range copy เฉพาะ value ของแต่ละ element เท่านั้น ไม่ copy ทั้ง slice และ slice header ก็ถูก evaluate แค่ครั้งเดียวตอนเริ่ม loop - “Go
forloop ต้องใช้ semicolon เหมือน C” —for i := 0; i < n; i++ใช้ semicolon แต่for rangeและ condition-only loop ไม่ต้องใช้ - “
breakในswitchที่อยู่ในforหยุด loop” — มันแค่ออกจากswitch(หรือselect) เท่านั้น — loop ยังทำงานต่อไป ใช้ labeledbreakเมื่อต้องการหยุด loop จริง ๆ - “append เข้า slice ระหว่าง range ทำให้ loop เห็น element ใหม่” — ไม่จริง
rangeevaluate ความยาวของ slice แค่ครั้งเดียวก่อน iteration แรก
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”Bug เรื่อง control-flow เป็นหนึ่งใน bug ที่ generated code ซ่อนได้ง่ายที่สุด เพราะ program ยัง compile ผ่านและมักยังรันได้ — มันแค่วนจำนวนรอบผิดหรือออกจากสิ่งที่ผิด:
- มี
breakธรรมดาอยู่ในswitch(หรือselect) ที่ตัวมันเองอยู่ในforหรือเปล่า? ตรวจว่าตั้งใจจะหยุด loop ไหม ถ้าใช่ต้องมี label for rangeloop แก้ตัวแปร value ของมันหรือเปล่า (for _, v := range s { v = ... }) โดยคาดหวังว่า slice ต้นฉบับจะเปลี่ยน? มันจะไม่มีวันเปลี่ยน- loop
appendเข้า slice ตัวเดียวกับที่มัน range อยู่ โดยคาดว่าจะไปเยี่ยม element ที่เพิ่งเพิ่มหรือเปล่า?rangeกำหนดจำนวนรอบไว้แล้วก่อนที่ append แรกจะรันด้วยซ้ำ fallthroughอยู่ใน case ที่กลายเป็น case สุดท้าย หรืออยู่ใน type switch หรือเปล่า? ทั้งคู่เป็น compile error ไม่ใช่ runtime surprise- labeled
continue/breakถูกใช้ในที่ที่ตั้งใจจะใช้ตัวธรรมดา หรือกลับกัน — ตรวจว่า label นั้นชี้ไป loop ไหนจริง ๆ
🐛 หา bug ในโค้ดนี้
for i := 0; i < 3; i++ { switch i { case 1: break default: fmt.Println("processing", i) }}เฉลย
break ข้างใน case 1 ออกจาก switch statement เท่านั้น — ไม่มีผลต่อ for loop ที่ครอบอยู่ ซึ่งยังทำงานต่อตามปกติ output จริงคือ processing 0 ตามด้วย processing 2: i == 1 ไม่ทำอะไรนอกจากออกจาก switch แล้ว loop ก็เดินหน้าไปที่ i == 2 เลย ถ้าตั้งใจจะหยุด loop ทั้งหมดที่ i == 1 ต้อง label loop แล้วเขียน break loop แทน: loop: for i := 0; i < 3; i++ { switch i { case 1: break loop; ... } }