ข้ามไปยังเนื้อหา

Control Flow

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 here

Pattern นี้ใช้บ่อยมากกับ operation ที่ return (value, error) — มันทำให้ตัวแปร error อยู่ใกล้กับจุดตรวจสอบและไม่ทำให้ scope ข้างนอกรก

Go มี loop keyword เดียวคือ for ซึ่งรองรับสามรูปแบบ:

รูปแบบที่ 1: C-style (init; condition; post)

for i := 0; i < 5; i++ {
fmt.Println(i)
}

รูปแบบที่ 2: Condition-only (เทียบเท่า while)

n := 1
for n < 100 {
n *= 2
}

รูปแบบที่ 3: Infinite loop

for {
if done() {
break
}
}

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 bytes
for 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 มากขึ้นแล้ว

ตั้งแต่ 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

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 ทันที

นี่คือหนึ่งใน 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
break ธรรมดาออกจากแค่ switch; break ที่มี label ออกจาก for ที่ label นั้นชี้ไป

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 visited

switch ใน 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 ไปยัง case ถัดไปอย่างชัดเจน ใช้ fallthrough เป็น statement สุดท้ายใน case มันไม่มีเงื่อนไข — มันรัน body ของ case ถัดไปไม่ว่า condition ของ case นั้นจะเป็นอย่างไร:

switch x := score(); {
case x > 10:
fmt.Println("big")
fallthrough
case x > 5:
fmt.Println("medium-or-big") // runs even though this case's own test was never evaluated
default:
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 switch

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 statementscope ของ err แคบที่สุดsyntax แปลกสำหรับ developer ที่มาจากภาษาอื่น
Labeled break/continueควบคุมได้แม่นยำว่า jump จะไปที่ loop ไหนbreak ธรรมดาใน switch/select ที่อยู่ใน for เป็น bug คลาสสิกที่เงียบมาก
  • “range copy ทั้ง slice” — range copy เฉพาะ value ของแต่ละ element เท่านั้น ไม่ copy ทั้ง slice และ slice header ก็ถูก evaluate แค่ครั้งเดียวตอนเริ่ม loop
  • “Go for loop ต้องใช้ semicolon เหมือน C” — for i := 0; i < n; i++ ใช้ semicolon แต่ for range และ condition-only loop ไม่ต้องใช้
  • “break ใน switch ที่อยู่ใน for หยุด loop” — มันแค่ออกจาก switch (หรือ select) เท่านั้น — loop ยังทำงานต่อไป ใช้ labeled break เมื่อต้องการหยุด loop จริง ๆ
  • “append เข้า slice ระหว่าง range ทำให้ loop เห็น element ใหม่” — ไม่จริง range evaluate ความยาวของ slice แค่ครั้งเดียวก่อน iteration แรก

Bug เรื่อง control-flow เป็นหนึ่งใน bug ที่ generated code ซ่อนได้ง่ายที่สุด เพราะ program ยัง compile ผ่านและมักยังรันได้ — มันแค่วนจำนวนรอบผิดหรือออกจากสิ่งที่ผิด:

  • มี break ธรรมดาอยู่ใน switch (หรือ select) ที่ตัวมันเองอยู่ใน for หรือเปล่า? ตรวจว่าตั้งใจจะหยุด loop ไหม ถ้าใช่ต้องมี label
  • for range loop แก้ตัวแปร 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; ... } }

อะไรคือ Go equivalent ของ while loop ในภาษาอื่น?
ใน Go switch statement เมื่อ case ตรงกันและไม่มี fallthrough keyword จะเกิดอะไร?
ตัวแปรที่ประกาศใน if init statement มี scope อย่างไร?
เมื่อ range over string ด้วย for i, r := range s ตัวแปร r มี type อะไร?
โค้ดนี้ print อะไร?
for i := 0; i < 3; i++ {
    switch i {
    case 1:
        break
    default:
        fmt.Println("processing", i)
    }
}
โค้ดนี้ print กี่บรรทัด?
s := []int{1, 2, 3}
for i, v := range s {
    if i == 0 {
        s = append(s, 4)
    }
    fmt.Println(i, v)
}