โหมดมืด
บทที่ 2 — Control Flow
หลังจบบท คุณจะ:
- เขียน if / for / switch แบบสำนวน Go แท้ (idiomatic = สำนวนการเขียนที่ชาว Go นิยม ถือว่า "เขียนแบบ Go แท้")
- ใช้
for4 รูปแบบ (Go มี loop เดียว!) - ใช้
defer+goto(เผื่อจำเป็น) - เข้าใจ break/continue/labels
1. if / else
เริ่มจากการตัดสินใจพื้นฐานที่สุด — if/else Go มีกฎต่างจากภาษาตระกูล C เล็กน้อย ดูรายละเอียดในตาราง "กฎ Go" ด้านล่าง:
go
x := 10
if x > 5 {
fmt.Println("big")
} else if x > 0 {
fmt.Println("positive small")
} else {
fmt.Println("zero or negative")
}กฎ Go
text
✅ no parentheses around condition
✅ braces { } บังคับ — แม้ 1 บรรทัด
✅ { ต้องอยู่บรรทัดเดียวกับ if/else (gofmt บังคับ)go
// ❌
if (x > 5) // เกินไป
{ // brace ต้องอยู่บรรทัดเดียวกัน
...
}
if x > 5 { // ✅
...
}2. if with Initial Statement — Idiomatic Go
(ต่างจาก C#: C# ไม่มี syntax นี้ — ใกล้เคียงสุดคือประกาศตัวแปรก่อน
ifแยกบรรทัด; Go รวมประกาศ+เงื่อนไขในบรรทัดเดียว ทำให้ scope แคบกว่า)
go
// Common pattern: declare + check
if err := doSomething(); err != nil {
return err
}
// err out of scope here ← good!go
// Without init (verbose)
err := doSomething()
if err != nil {
return err
}
// err still in scope — polluteหมายเหตุสำหรับมือใหม่:
errคือค่าข้อผิดพลาดที่ฟังก์ชันคืนกลับมา — เราจะเรียนเรื่องนี้เต็ม ๆ ในบท 03 Functions และบท 07 Error Handling ตอนนี้แค่รู้ว่ารูปแบบนี้มีอยู่ก็พอ
ทำไม developer (นักพัฒนา) Go ชอบ pattern (รูปแบบการเขียน) นี้?
errมี scope (ขอบเขตการมองเห็นตัวแปร) แคบลง- ตัวแปรไม่หลุดออกไปนอก
if(ลดตัวแปรค้างในขอบเขตภายนอก — pollute outer scope) - เป็น idiom (สำนวนที่ชาว Go นิยม) — อ่านแล้วรู้ทันที
Multiple Use Case
go
if v, ok := m[key]; ok {
fmt.Println(v)
}
if user, err := getUser(id); err == nil {
fmt.Println(user)
}3. for — Loop เดียวใน Go
Go มี loop ตัวเดียว: for — แต่ใช้ได้ 4 แบบ
A. Classic C-style
go
for i := 0; i < 10; i++ {
fmt.Println(i)
}B. While-style
go
i := 0
for i < 10 {
fmt.Println(i)
i++
}→ Go ไม่มี while keyword — for condition แทน
C. Infinite Loop
go
for {
// forever
if shouldStop() {
break
}
}→ ใช้กับ server loop (ลูปของเซิร์ฟเวอร์ที่รอรับ request เรื่อย ๆ), event loop (ลูปรอเหตุการณ์เข้ามาแล้วทำงาน)
D. for range (collection)
go
nums := []int{1, 2, 3, 4, 5}
for i, v := range nums {
fmt.Printf("index=%d value=%d\n", i, v)
}
// only index
for i := range nums {
fmt.Println(i)
}
// only value (discard index)
for _, v := range nums {
fmt.Println(v)
}
// map
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Printf("%s = %d\n", k, v)
}
// string (rune by rune)
for i, r := range "héllo" {
fmt.Printf("%d: %c\n", i, r)
}
// 0: h
// 1: é (note: index jumps because é = 2 bytes)
// 3: l
// 4: l
// 5: o(เดี๋ยวบท 04 Collections อธิบายเรื่อง rune/byte เต็ม ๆ ว่าทำไม index ถึงกระโดดข้าม — ตอนนี้แค่สังเกตว่า
éกิน 2 ช่องพอ)
⚙️ Range Over Channel (ขั้นสูง — ข้ามได้ บท 08)
range ยังใช้กับ channel (ช่องสื่อสารระหว่าง goroutine) ได้ด้วย — ข้ามส่วนนี้ไปก่อน ถ้ายังไม่รู้จัก channel เดี๋ยวบท 08 Concurrency อธิบายเต็ม
go
// channel (ช่องสื่อสาร — เรียนบท 08)
for msg := range ch {
fmt.Println(msg)
}E. Range Over Int (Go 1.22+)
go
// loop 0..9
for i := range 10 {
fmt.Println(i)
}
// equivalent
for i := 0; i < 10; i++ {
fmt.Println(i)
}→ Concise version of for-i
4. break + continue
ภายใน loop เราควบคุมการวนได้ด้วยคำสั่งสองตัว: continue ข้ามไปรอบถัดไปทันที (ไม่ทำโค้ดที่เหลือในรอบนั้น) ส่วน break ออกจาก loop เลย:
go
for i := 0; i < 10; i++ {
if i == 3 {
continue // skip rest of iteration
}
if i == 7 {
break // exit loop
}
fmt.Println(i)
}
// 0 1 2 4 5 65. Labels — Break / Continue Specific Loop
ปกติ break ออกจาก loop ชั้นในสุดเท่านั้น แต่ถ้ามี loop ซ้อนกันแล้วอยากออกจากทั้งหมดทีเดียวล่ะ? ให้ตั้ง "label" (ชื่อ) ให้ loop ชั้นนอก แล้วสั่ง break outer — ใช้ไม่บ่อยแต่จำเป็นในบางกรณี:
go
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j > 6 {
break outer // break out of BOTH loops
}
fmt.Println(i, j)
}
}→ ใช้น้อย แต่จำเป็นบาง case
6. switch — มากกว่า if/else if/else
go
day := "Monday"
switch day {
case "Monday":
fmt.Println("Start of week")
case "Friday":
fmt.Println("TGIF!")
case "Saturday", "Sunday": // multiple values
fmt.Println("Weekend")
default:
fmt.Println("Midweek")
}กฎ Go
- ❌ ไม่ต้องมี
break— auto break (ต่างจาก C#/C/Java ที่ต้องเขียนbreakเอง — C# บังคับbreak/returnทุก case ห้าม fall-through เงียบ ส่วน Go เติมให้อัตโนมัติ และถ้าอยากรันต่อ case ถัดไปต้องเขียนfallthroughชัดเจน) - ✅ Multiple values:
case "a", "b", "c": - ✅ (ขั้นสูง — ข้ามได้) ตัวเลขเปล่า ๆ ใน
caseเช่นcase 1:ใช้ได้แม้ตัวแปรที่ switch จะเป็น type พิเศษอย่างint64ก็ตาม (ไม่ต้องเขียนcase int64(1):) เพราะ Go แปลง type ให้อัตโนมัติ
Fallthrough — explicit ถ้าต้องการ
go
switch x {
case 1:
fmt.Println("one")
fallthrough // run next case too
case 2:
fmt.Println("two")
}ใช้น้อยมาก — แค่ flag ที่เห็นได้ใน code
7. switch with Initial Statement
เช่นเดียวกับ if เราใส่คำสั่งเริ่มต้นใน switch ได้ (ประกาศตัวแปรก่อนคั่นด้วย ;) — ทำให้ตัวแปร (เช่น err) มี scope แคบอยู่แค่ใน switch นี้ ไม่รั่วไปข้างนอก:
go
// ErrNotFound, ErrInternal เป็น sentinel error (ค่า error ที่ประกาศไว้ล่วงหน้า) ที่เราประกาศเอง — เดี๋ยวบท 07 อธิบาย
switch err := doSomething(); err {
case nil:
fmt.Println("ok")
case ErrNotFound:
fmt.Println("not found")
case ErrInternal:
fmt.Println("server error")
default:
fmt.Printf("unknown: %v\n", err)
}8. switch Without Tag (if-else chain)
switch ที่ไม่ใส่ค่าให้ตรวจ (ไม่มี expression ต่อท้าย) จะกลายเป็น "if-else if chain" ที่อ่านง่ายกว่า — แต่ละ case เป็นเงื่อนไข boolean เต็มรูปแบบ เหมาะเวลาเทียบช่วงค่า:
go
score := 85
switch { // no expression after switch
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 70:
fmt.Println("C")
default:
fmt.Println("F")
}→ readability ดีกว่า if/else if/else if/else
9. Type Switch
"Type switch" เป็น switch รูปแบบพิเศษที่ใช้ตรวจว่าค่าใน interface (อินเทอร์เฟซ — เดี๋ยวบท 06 อธิบายเต็ม) เป็น type อะไรจริง ๆ ตอน runtime (ขณะโปรแกรมกำลังรัน)
ใช้รูป i.(type) (เขียนได้เฉพาะใน switch เท่านั้น) แล้วแตกเคสตามชนิด เจอบ่อยเวลาทำงานกับ any (= interface{} ตั้งแต่ Go 1.18 — ชนิดที่รับค่าอะไรก็ได้)
โซนขั้นสูง — มือใหม่ข้ามได้ ส่วนนี้ใช้ interface ที่ยังไม่ได้เรียน ตอนนี้แค่รู้ว่า "Go เช็คชนิดข้อมูลจริงตอนรันได้" ก็พอ เดี๋ยวกลับมาอ่านหลังเรียนบท 06 Interfaces จะเข้าใจทันที
go
var i any = "hello" // any = interface{} — รับค่าอะไรก็ได้
switch v := i.(type) {
case int:
fmt.Printf("int: %d\n", v)
case string:
fmt.Printf("string: %s\n", v)
case bool:
fmt.Printf("bool: %t\n", v)
default:
fmt.Printf("unknown type: %T\n", v)
}.(type) ใช้ได้ใน switch เท่านั้น — เช็ค dynamic type ของ interface
10. defer — Run Later
defer เป็นคีย์เวิร์ดเด่นของ Go — มันบอกว่า "เลื่อนการทำคำสั่งนี้ไปทำตอนฟังก์ชันกำลังจะ return" นิยมใช้เก็บกวาดทรัพยากร (ปิดไฟล์, ปลดล็อก) ให้แน่ใจว่าทำเสมอแม้จะ return หลายจุด:
(ต่างจาก C#:
defer≈using/finallyแต่ ต่างกันสำคัญ —using/finallyทำงานตอนออกจาก block{}(block-scoped) ส่วนdeferทำงานตอนออกจากทั้งฟังก์ชัน (function-scoped) นี่คือเหตุผลที่deferในลูปจะสะสมรอจนฟังก์ชันจบถึงจะรันทั้งหมด — ต่างจากusingที่ไม่มีปัญหานี้เพราะผูกกับ scope ของ block)
go
func main() {
fmt.Println("Start")
defer fmt.Println("Deferred")
fmt.Println("End")
}
// Output:
// Start
// End
// Deferreddefer = "run นี้ตอน function return"
ใช้สำหรับ cleanup
go
func readFile(path string) {
file, err := os.Open(path)
if err != nil {
return
}
defer file.Close() // guaranteed close
// ... do stuff with file
// file.Close() จะถูกเรียกเมื่อ function จบ
}Multiple defer — LIFO Stack
go
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// Output:
// 3
// 2
// 1Defer + Arguments — evaluated ตอน defer
go
func main() {
x := 10
defer fmt.Println("x =", x) // captures x = 10 NOW
x = 20
}
// Output: x = 10go
// ใช้ closure (ฟังก์ชันที่จำตัวแปรรอบตัวได้ — เดี๋ยวบท 03 อธิบาย) เพื่อให้ผูกค่าตอนหลัง
func main() {
x := 10
defer func() { fmt.Println("x =", x) }() // อ้างถึงตัว x จริง (capture by reference) ไม่ใช่ค่าตอนนี้
x = 20
}
// Output: x = 20Common Patterns
ตรงนี้แค่ดูรูปแบบ defer ที่เจอบ่อยพอ — รายละเอียดของ
Mutex(ตัวล็อกกันชนกันใน goroutine) อยู่บท 08 Concurrency,panic/recoverอยู่บท 07 Error Handling
go
// File
defer file.Close()
// Mutex (ตัวล็อก — เดี๋ยวบท 08 อธิบาย)
mu.Lock()
defer mu.Unlock()
// HTTP body
defer resp.Body.Close()
// Time function
defer timeTrack(time.Now(), "myFunc")
// Recover from panic (กู้คืนจาก panic = โปรแกรมพัง — รายละเอียดบท 07; ตรงนี้แค่เห็นรูปแบบ defer + recover ที่ใช้บ่อย)
defer func() {
if r := recover(); r != nil {
log.Printf("recovered: %v", r)
}
}()11. goto (rare)
go
func search() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if found(i, j) {
goto done
}
}
}
done:
fmt.Println("done")
}ใช้น้อยมาก — มัก break/continue + labels ดีกว่า
Idiomatic Go ไม่ค่อยใช้ goto
12. Range Over Function (Go 1.23+) — Modern
⛔ โซนขั้นสูงมาก — มือใหม่ข้ามไปก่อนได้เลย แล้วค่อยกลับมาอ่านหลังจบบท 3 หัวข้อนี้ใช้ฟังก์ชันซ้อนฟังก์ชัน (closure — ยังไม่ได้สอน จะเรียนบท 03 Functions) รูปแบบที่ใช้ (ดูโค้ดด้านล่าง) อาจดูน่ากลัวถ้ายังไม่รู้จัก closure
ตั้งแต่ Go 1.23 เราใช้ for range วนบน "ฟังก์ชัน iterator (ตัววนซ้ำ)" ที่เราเขียนเองได้
ข้อดีคือ lazy — คำนวณค่าทีละตัวตอนที่ for-range ขอเท่านั้น ไม่สร้างข้อมูลทั้งก้อนในหน่วยความจำล่วงหน้า ประหยัด RAM มากเวลาข้อมูลจำนวนมาก:
go
// Iterator function
func count(n int) func(yield func(int) bool) {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return
}
}
}
}
// Use
for i := range count(5) {
fmt.Println(i)
}
// 0 1 2 3 4→ Pattern สำหรับ lazy iteration (like Python generator)
13. Pattern: Loop + State
ตัวอย่างด้านล่างสมมติว่ามี
names []string(เช่น[]string{"Anna", "Ben", "Cara"}) และnums []int(เช่น[]int{-3, 5, 8, -1}) ประกาศไว้แล้ว — โฟกัสที่รูปแบบ (pattern) ของ loop เอง ไม่ต้องกังวลว่าตัวแปรมาจากไหน
Find First
go
target := "Anna"
found := false
for _, name := range names {
if name == target {
found = true
break
}
}
if found {
fmt.Println("Found!")
}Or — Use Function
go
func contains(slice []string, target string) bool {
for _, s := range slice {
if s == target {
return true
}
}
return false
}
if contains(names, "Anna") {
fmt.Println("Found!")
}Count
go
count := 0
for _, n := range nums {
if n > 0 {
count++
}
}Filter (build new slice)
go
var positive []int
for _, n := range nums {
if n > 0 {
positive = append(positive, n)
}
}Map (transform)
go
doubled := make([]int, len(nums))
for i, n := range nums {
doubled[i] = n * 2
}Reduce (sum)
go
sum := 0
for _, n := range nums {
sum += n
}Go ไม่มี
.map()/.filter()/.reduce()แบบ functional (สไตล์ฟังก์ชัน — เขียนต่อท่อกัน เช่น JS, Python, Java Stream) ภาษาอื่น — เขียน loop ตรง ๆ
(ต่างจาก C#: ก็ไม่มี LINQ (Where/Select/Aggregate) ที่ dev .NET ใช้ทุกวันด้วย — ต้องเขียน loop ตรง ๆ แทน Go 1.23+ iterators ช่วยได้บ้างแต่ไม่ครอบคลุมเท่า LINQ)
ปี 2026 มี Go 1.23+ iterators ช่วยได้บ้าง — แต่ idiomatic = loop
14. Common Pitfalls
❌ Loop Variable Capture (Go ก่อน 1.22)
ตัวอย่างนี้ใช้ closure (ฟังก์ชันที่จำตัวแปรรอบตัวได้) ที่ยังไม่ได้เรียน — ตอนนี้แค่ "รู้ว่ามีกับดักนี้" ก็พอ เดี๋ยวบท 03 Functions อธิบาย closure เต็ม ถ้าส่วนนี้ดูงงให้ข้ามไปก่อนได้เลย
go
// Go < 1.22 — มี bug นี้
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f()
}
// Output (Go < 1.22): 3 3 3 (i shared!)
// Output (Go >= 1.22): 0 1 2 (per-iteration scope)→ Go 1.22+ แก้แล้ว — แต่ระวังถ้าใช้ version เก่า:
go
// Fix สำหรับ Go < 1.22
for i := 0; i < 3; i++ {
i := i // shadow — new variable per iteration
funcs = append(funcs, func() {
fmt.Println(i)
})
}❌ Modify Slice While Ranging
go
s := []int{1, 2, 3, 4, 5}
for i, v := range s {
if v == 3 {
s = append(s[:i], s[i+1:]...) // 😱 modify while iterating
}
}→ Build new slice instead
❌ for-range Value = Copy
go
type User struct{ Name string }
users := []User{{"Anna"}, {"Ben"}}
for _, u := range users {
u.Name = "Modified" // ❌ modify copy — original unchanged
}
fmt.Println(users) // [{Anna} {Ben}]
// ✅ Use index
for i := range users {
users[i].Name = "Modified"
}❌ Forget to handle default in switch
go
switch status {
case "ACTIVE":
...
case "INACTIVE":
...
// no default — silent ignore unknown status
}
// ✅ Add default
switch status {
case "ACTIVE":
...
case "INACTIVE":
...
default:
return fmt.Errorf("unknown status: %s", status)
}15. ตัวอย่างจริง — FizzBuzz
มาประยุกต์ใช้ control flow กับโจทย์คลาสสิก "FizzBuzz" — พิมพ์เลข 1-100 แต่แทนตัวที่หาร 3 ลงตัวด้วย "Fizz", หาร 5 ด้วย "Buzz", หารทั้งคู่ด้วย "FizzBuzz" สังเกตการใช้ switch แบบไม่มี tag ที่อ่านสะอาดกว่า if ซ้อน:
go
package main
import "fmt"
func main() {
for i := 1; i <= 100; i++ {
switch {
case i%15 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}16. ตัวอย่างจริง — Number Guesser
ตัวอย่างเกมทายเลขที่รวม control flow หลายอย่างเข้าด้วยกัน — for {} วนไม่รู้จบ, อ่าน input จากผู้ใช้, จัดการ error ตอนแปลงเลข, ใช้ continue ข้ามเมื่อกรอกผิด และ switch/return เมื่อทายถูก:
go
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
)
func main() {
// Go 1.20+ auto-seed ให้เรียบร้อย — รุ่นเก่าจะได้เลขเดิมทุกรอบ
target := rand.Intn(100) + 1
reader := bufio.NewReader(os.Stdin)
attempts := 0
for {
fmt.Print("Guess (1-100): ")
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Bye!") // stdin ปิด (EOF / Ctrl-D)
return
}
input = strings.TrimSpace(input)
if input == "quit" {
fmt.Println("Bye!")
return
}
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid number")
continue
}
attempts++
switch {
case guess < target:
fmt.Println("Too low")
case guess > target:
fmt.Println("Too high")
default:
fmt.Printf("Correct! Took %d attempts.\n", attempts)
return
}
}
}17. Performance — for vs range
go
nums := make([]int, 1_000_000)
// Both same performance (compiler optimize)
for i := 0; i < len(nums); i++ {
_ = nums[i]
}
for i := range nums {
_ = nums[i]
}
for _, v := range nums {
_ = v // คัดลอก v ทุกรอบ — ตรงนี้เป็น int เลยไม่มีผลด้านความเร็ว แต่ถ้า element เป็น struct ขนาดใหญ่ การคัดลอกทุกรอบจะช้าลงเห็นได้ชัด
}→ For most cases, ใช้ range — readability ดีกว่า
→ For big struct, ใช้ index ก็ได้ ถ้า perf สำคัญ:
go
for i := range users {
user := &users[i] // pointer, no copy
process(user)
}18. Idioms ที่ควรรู้
รวม "สำนวน" (idiom) ที่เป็นเอกลักษณ์และพบบ่อยมากในโค้ด Go จริง — เช่นการเช็คค่าใน map พร้อมประกาศตัวแปรใน if (if v, ok := m[key]; ok) และรูปแบบรับ-เช็ค error ที่เห็นแทบทุกฟังก์ชัน คุ้นกับสำนวนเหล่านี้แล้วจะอ่านโค้ด Go คนอื่นออกทันที:
Initial assignment in if
go
if v, ok := m[key]; ok {
use(v)
}Multiple return + check error
go
result, err := fn()
if err != nil {
return err
}
use(result)Switch type
go
switch v := i.(type) {
case string:
...
}Defer for cleanup
go
defer cleanup()Loop pattern
go
for ; condition; update {} // 1 statement
for condition {} // 1 statement
for {} // forever
for range collection {} // iterate19. Checkpoint
🛠️ Checkpoint 2.1 — FizzBuzz
เขียน FizzBuzz เลข 1-100 — ใช้ switch ไม่ใช้ if/else
🛠️ Checkpoint 2.2 — Number Guesser (เกมทายเลข)
ทำ checkpoint ข้อ 16 — เพิ่ม:
- จำกัดจำนวนครั้งที่ทาย (attempts) 10 ครั้ง
- บันทึกสถิติดีที่สุด (high score = ใช้จำนวนครั้งน้อยสุด)
🛠️ Checkpoint 2.3 — Word Count (นับคำ)
รับ string จาก stdin → นับจำนวนคำ + พิมพ์คำที่ไม่ซ้ำ (unique word) พร้อมจำนวนครั้งที่เจอ
🛠️ Checkpoint 2.4 — Defer Practice (ฝึกใช้ defer)
เขียน function ที่:
- เปิดไฟล์
- อ่าน + ประมวลผล (process)
- ใช้ defer file.Close()
- ทดสอบกรณีเกิด error ระหว่างทาง — ไฟล์ต้องยังถูกปิด (close) อยู่ดี
🛠️ Checkpoint 2.5 — Range Pitfall (กับดักของ range)
- เขียนโค้ดที่มี slice ของ struct แล้วลองแก้ค่าผ่าน range
- ดูว่ามันทำงานยังไง (ทำไมแก้ไม่ติด)
- แก้ให้ถูก (ใช้ index)
20. สรุปบท
✅ if ไม่มี () แต่บังคับ {} แม้ 1 บรรทัด
✅ if with init = idiomatic: if err := fn(); err != nil { ... }
✅ for เป็น loop เดียว — ใช้ได้ 4-5 รูปแบบ (C-style, while, infinite, range, range int)
✅ range over slice/map/string/channel/int (1.22+) / function (1.23+)
✅ switch ไม่มี implicit fallthrough — ไม่ต้องเขียน break ก็จบ case อัตโนมัติ (แต่ break ยังใช้ได้ถ้าจำเป็น เช่น ออกจาก switch ข้างใน loop; ใช้ fallthrough ถ้าต้องการรันต่อ)
✅ switch { case condition: ... } = if-else if chain readable
✅ Type switch: switch v := i.(type)
✅ defer = run ตอน function return — LIFO, capture argument ตอน defer
✅ Loop variable capture แก้แล้วใน Go 1.22
✅ for-range value = copy — ใช้ index ถ้า modify struct