เรียน Go ภาษาง่าย 📘

🪜

หนังสือภาษา Go ระดับกลาง ฉบับภาษาง่ายๆ

ขึ้นชั้นกลาง: module, tools, slice/map, generics, context

14 บท
24 ข้อสอบ
ตอนที่ 0: เช็กตัวเองก่อนขึ้นระดับกลาง

ลองตอบในใจ ถ้าทำได้เกิน 7 ข้อขึ้นเล่มนี้ได้เลย ถ้ายังไม่ได้กลับไปเปิดเล่ม Beginner ตอนนั้นก่อน:

  1. สร้างโปรเจกต์ go mod init + รัน go run . ได้
  2. อธิบายได้ว่า := ต่างจาก = ยังไง
  3. เขียน if err != nil ได้โดยไม่ต้องดู
  4. เขียน for + range วน slice/map ได้
  5. เขียน func รับ-คืนค่าได้
  6. ใช้ append โดยไม่ลืม s = append(s, ...)
  7. สร้าง struct + เรียก p.Name ได้
  8. สร้าง channel + go func + close + range ได้

เช็กเวอร์ชันตัวเองก่อน:

bash
go version

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 1: จัดโปรเจกต์แบบมืออาชีพด้วย Module
Module คืออะไร? (แบบบ้านๆ)

เล่มก่อนเราสร้างโฟลเดอร์ hello แล้ว go mod init hello ได้ไฟล์ go.mod มา 1 ไฟล์

ให้นึกว่า go.mod = บัตรประชาชนของโปรเจกต์ บอกว่า:

  • ชื่ออะไร
  • ใช้ Go เวอร์ชันไหน
  • ยืมของ (library) จากใครมาบ้าง

เปิดดู:

bash
cat go.mod

จะเห็นประมาณนี้ (Go 1.27):

code
module hello

go 1.25

หลายคนงง: "ลง 1.27 ทำไมขึ้น 1.25?" ตอบ: ปกติ Go ตั้งใจให้เลขต่ำไว้ก่อนเพื่อให้เพื่อนที่ยังไม่อัปเดตเปิดได้ ไม่ต้องแก้ ถ้าอยากขยับค่อยสั่ง:

bash
go get go@1.27
ยืมของคนอื่นมาใช้
bash
go get github.com/fatih/color

แล้วใช้ได้เลย:

go
import "github.com/fatih/color"
color.Red("แดง!")

Go จะจดลง go.mod + สร้าง go.sum (ใบเสร็จกันของปลอม) ให้เอง จบด้วย:

bash
go mod tidy  # เก็บกวาด: ของใหม่ 1.27 จะรวม require ให้เหลือแค่ 2 กลุ่ม direct/indirect สวยๆ
โครงโฟลเดอร์ที่คนทำงานใช้กัน

ไม่ต้องซับซ้อน เริ่มแบบนี้พอ:

code
myapp/
  go.mod
  main.go          # ทางเข้า
  handler/         # รับ request
    user.go
  store/           # คุยกับข้อมูล
    user.go

กฎง่ายๆ:

  • 1 โฟลเดอร์ = 1 package
  • ชื่อ package สั้น ตัวเล็ก: store ไม่ใช่ StoreData
  • อย่าสร้างโฟลเดอร์ลึกเกิน 2 ชั้นถ้ายังไม่จำเป็น

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 2: เครื่องมือหากิน go fmt vet fix doc

4 คำสั่งนี้คือชุดปฐมพยาบาล ใช้ทุกวัน:

bash
go fmt ./...  # แม่บ้าน: จัดหน้าให้สวย
go vet ./...  # หมอ: ตรวจว่าสั่งอะไรแปลกๆ ไหม
go fix ./...  # ครู: (ของใหม่ Go 1.26 เก่งมาก) แก้โค้ดเก่าเป็นแบบใหม่ให้เอง
go test ./... # ครูสอบ: รันเทสต์
go fix ของใหม่ เก่งขึ้นเยอะ

Go 1.26 รื้อ go fix ใหม่หมด มีตัวช่วยชื่อ modernizers เช่น:

  • atomictypes — เปลี่ยนวิธีเขียน atomic เก่าเป็นแบบใหม่
  • embedlit — ช่วยเรื่อง struct ฝังกัน (ตรงกับบทที่ 4)
  • slicesbackward — ช่วยวน slice กลับหลังให้ถูก
  • unsafefuncs — เตือนจุดที่ใช้ unsafe เสี่ยงพัง

วิธีใช้แบบคนขี้เกียจ:

bash
go fix ./...

มันจะบอกว่าจะแก้อะไรบ้าง ให้กดยืนยัน ไม่ต้องจำเองทุกตัว

ดูคู่มือแบบไม่ต้องเปิดเว็บ
bash
go doc fmt.Println
go doc strings.Builder

go doc example.com/pkg@v1.2.3

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 3: Slice Map ลึกขึ้นอีกนิด (จุดที่พังบ่อย)
1. Slice คือ 3 อย่างใน 1 เดียว

จำภาพนี้: slice = กระดาษโน้ตแปะไว้ว่า ของอยู่ตรงไหน ยาวเท่าไหร่ จุได้เท่าไหร่

go
s := make([]int, 2, 5) // len=2 cap=5
fmt.Println(len(s), cap(s)) // 2 5
  • len = ของที่มีตอนนี้
  • cap = จุได้สูงสุดก่อนต้องย้ายบ้าน

กับดักยอดฮิต:

go
a := []int{1, 2, 3}
b := a // b ชี้บ้านหลังเดียวกัน!
b[0] = 99
fmt.Println(a) // [99 2 3] a โดนด้วย!

ถ้าอยากได้บ้านใหม่จริงๆ ต้อง copy หรือ append ไป slice ว่าง:

go
c := append([]int{}, a...) // บ้านใหม่
// หรือ
d := make([]int, len(a))
copy(d, a)

และห้ามลืม:

go
s = append(s, 4) // ต้องมี s = ข้างหน้า เพราะอาจย้ายบ้าน

ข่าวดี Go 1.26: compiler ฉลาดขึ้น เอาที่เก็บ slice ไปไว้บน stack ได้บ่อยขึ้น = เร็วขึ้นเองโดยไม่ต้องแก้โค้ด

2. Map ใช้พร้อมกันไม่ได้ ต้องล็อก

Map เหมือนสมุดโทรศัพท์เล่มเดียว ถ้า 2 คนเขียนพร้อมกันจะขาด:

go
// ผิด! อย่าเขียน map จากหลาย goroutine ตรงๆ
// m[key] = v  // พร้อมกันหลายตัว = race

ทางแก้มี 2 แบบ:

go
// แบบ 1: ใส่กุญแจ (ง่ายสุด)
var mu sync.Mutex
mu.Lock()
m[key] = v
mu.Unlock()

// แบบ 2: ใช้ sync.Map (เหมาะกับอ่านเยอะเขียนน้อย)
var sm sync.Map
sm.Store(key, value)
v, ok := sm.Load(key)

เช็กว่ามี key ไหมยังใช้สูตรเดิม:

go
v, ok := m[key]
if !ok {
    // ไม่มี
}

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 4: Struct ฝังกัน + new แบบใหม่ 1.26
Embedding = เอากล่องยัดในกล่อง แล้วเรียกของข้างในได้เลย
go
type Logger struct{}
func (Logger) Log(s string) { fmt.Println("log:", s) }

type Job struct {
    Command string
    Logger // ฝังไว้ ไม่ต้องตั้งชื่อ field
}

j := Job{Command: "run"}
j.Log("start") // เรียกของ Logger ได้เลยเหมือนเป็นของตัวเอง

ของใหม่ Go 1.27: ตอนสร้างก็ยัดไส้ชั้นในได้เลย ไม่ต้องแกะ:

go
type Habitat struct{ Burrow string }
type Gopher struct {
    Name string
    Habitat
}
g := Gopher{
    Name:   "Gopher",
    Burrow: "Burrow #42", // เมื่อก่อนต้องเขียน Habitat: Habitat{Burrow: ...} ยาวกว่า
}

go fix มีตัว embedlit คอยช่วยเปลี่ยนแบบเก่าเป็นแบบใหม่ให้ด้วย

new แบบใหม่ สั้นลง

เล่มก่อนบอกว่า new(T) = สร้างกล่องเปล่า ตอนนี้ Go 1.26 ใส่ไส้ได้เลย:

go
// เก่า: 2 บรรทัด
x := int64(300)
p1 := &x

// ใหม่: บรรทัดเดียว ความหมายเดียวกัน
p2 := new(int64(300))

อ่านว่า "สร้างกล่อง int64 ใส่ 300 พร้อมบอกที่อยู่" ใช้ตอนอยากได้ pointer แบบไวๆ


✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 5: Function ลึก: Closure Variadic Defer
1. Closure = ฟังก์ชันจำบ้านเกิดได้
go
func counter() func() int {
    n := 0
    return func() int { // ฟังก์ชันข้างในจำ n ได้แม้ข้างนอกจบไปแล้ว
        n++
        return n
    }
}

c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2

เหมือนกระปุกออมสินที่จำได้ว่าหยอดไปเท่าไหร่แล้ว

2. Variadic = รับกี่ชิ้นก็ได้ด้วย ...
go
func Sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
Sum(1, 2)      // 3
Sum(1, 2, 3, 4) // 10
s := []int{1, 2, 3}
Sum(s...) // มี ... ท้าย = แกะกล่องโปรยเข้าไป
3. Defer แบบลึก: ทำความสะอาด + ดัก panic

รู้แล้วว่า defer = สัญญาว่าจะทำตอนจบ แต่มี 2 ทริก:

go
func work() {
    mu.Lock()
    defer mu.Unlock() // ปลดล็อกแน่ ไม่ว่าจะ return ทางไหน

    f, err := os.Open("a.txt")
    if err != nil { return }
    defer f.Close() // ปิดไฟล์แน่
}

อาร์กิวเมนต์ถูกถ่ายรูปตอน defer ไม่ใช่ตอนทำจริง:

go
for i := 0; i < 3; i++ {
    defer fmt.Println(i) // เก็บค่า i ตอนนั้นเลย
}
// พิมพ์ 2 1 0 (เข้าหลังออกก่อน)

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 6: Interface + Generics แบบไม่ปวดหัว (รวมของใหม่ 1.27)
Interface = สัญญาว่าทำอะไรได้บ้าง
go
type Stringer interface {
    String() string
}

ใครมีเมธอด String() string ถือว่าเป็นพวกเดียวกันอัตโนมัติ ไม่ต้องสมัคร:

go
func Print(s fmt.Stringer) {
    fmt.Println(s.String())
}

กฎ: รับ interface เล็กๆ คืนของจริงก้อนใหญ่ โค้ดจะต่อกับใครก็ได้

Generics = ฟังก์ชันครอบจักรวาล (จำแค่ใช้เป็นพอ)

เมื่อก่อนอยากได้ฟังก์ชันใช้ได้หลาย type ต้องเขียนซ้ำ ตอนนี้เขียนทีเดียว:

go
func Min[T int | float64](a, b T) T {
    if a < b { return a }
    return b
}
Min(1, 2)       // int
Min(1.5, 2.5)   // float64

ของใหม่ Go 1.27 มี 2 อย่าง:

1. Generic method ใน struct ได้แล้ว (เมื่อก่อนทำได้แค่ func ลอยๆ):

go
// ตัวอย่างจาก math/rand/v2
func (r *Rand) N[Int intType](n Int) Int
// เมธอดเดียวใช้ได้ทุกชนิด int ไม่ต้องเขียน Int32N Int64N แยกกันแล้ว

2. Go เดาชนิดให้ในที่ที่เคยต้องเขียนย้ำ รวมถึงส่งเข้า channel:

go
func GenericFormatter[T any](v T) string {
    return fmt.Sprintf("value: %v", v)
}
type IntFormatter func(int) string

formatters := []IntFormatter{GenericFormatter} // ไม่ต้อง [int] แล้ว เดาให้
ch := make(chan IntFormatter, 1)
ch <- GenericFormatter // ส่งเข้า channel ได้เลย เดาให้เหมือนกัน

มือกลางจำแค่นี้: ถ้าเห็นโค้ด generic ไม่มี [ ] แล้วไม่พัง นั่นคือของใหม่ 1.27 ไม่ใช่บั๊ก


✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 7: Error แบบมืออาชีพ Is As Wrap

เล่มก่อน: if err != nil เช็กเลย เล่มนี้: ทำให้ error บอกทางกลับบ้านได้

ห่อ error ด้วย %w
go
data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("อ่าน config: %w", err) // %w = ห่ออันเก่าไว้ข้างใน แกะดูได้
}
แกะดูด้วย Is / As
go
if errors.Is(err, os.ErrNotExist) {
    // ไฟล์ไม่มีจริงๆ ไม่ใช่พังแบบอื่น
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("พังที่ path:", pathErr.Path)
}

เหมือนซองจดหมายซ้อนกัน Is = ถามว่าไส้ในใช่คนนี้ไหม As = แกะเอาคนข้างในออกมา

Custom error ง่ายๆ
go
type AgeError struct{ Age int }
func (e *AgeError) Error() string {
    return fmt.Sprintf("อายุ %d ใช้ไม่ได้", e.Age)
}

อย่า panic กับงานปกติ ให้ return error ส่วน panic + recover เก็บไว้กันตายทั้งเซิร์ฟเวอร์เท่านั้น


✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 8: Concurrency ขั้นกลาง Channel Select Pipeline

ทบทวนแบบ 1 นาที:

  • go f() = จ้างพ่อครัวเพิ่ม
  • ch <- v = วางจาน, <-ch = หยิบจาน
  • close(ch) = บอกว่าหมดแล้ว คนส่งเท่านั้นที่ปิด
  • for v := range ch = หยิบจนหมด
1. Buffered vs Unbuffered เลือกยังไง?
go
unbuf := make(chan int)    // ส่งแล้วต้องมีคนรับทันที (ซิงค์ 100%)
buf := make(chan int, 10)  // วางค้างได้ 10 ชิ้น (กันพ่อครัวรอ)
  • อยากให้ 2 ฝั่งเดินพร้อมกันเป๊ะๆ → unbuffered
  • อยากกันกระตุก (producer เร็วกว่า consumer) → buffered
2. Select + Timeout
go
select {
case v := <-ch:
    fmt.Println(v)
case <-time.After(2 * time.Second):
    fmt.Println("รอ 2 วิแล้ว ไม่มา เลิก!")
}

ทริก non-blocking:

go
select {
case v := <-ch:
    fmt.Println(v)
default:
    fmt.Println("ยังไม่มีของ ไปทำอย่างอื่นก่อน")
}
3. Pipeline + Fan-out Fan-in

ของไหลเป็นทอดๆ:

go
func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums { out <- n }
        close(out)
    }()
    return out
}
func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in { out <- n * n }
        close(out)
    }()
    return out
}
// ใช้: for n := range sq(gen(1,2,3,4)) { ... } // 1 4 9 16

ถ้างานหนักให้แตกหลายคนทำแล้วรวม (fan-out/fan-in) + worker pool แบบเล่มก่อน

กับดัก:

  • all goroutines are asleep - deadlock = ส่งแล้วไม่มีคนรับ หรือรับแล้วไม่มีคนส่ง
  • send on closed channel = ส่งหลัง close
  • ลืม close = รอจนค้าง

ตรวจเสมอ:

bash
go run -race .
go test -race ./...

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 9: Context + Sync + เลิกให้ goroutine รั่ว
Context = รีโมทสั่งยกเลิกงาน

พ่อครัวทำอยู่ ลูกค้ายกเลิก จะตะโกนบอกยังไง? ใช้ context:

go
func work(ctx context.Context) {
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("เสร็จ")
    case <-ctx.Done():
        fmt.Println("โดนยกเลิก:", ctx.Err())
    }
}

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go work(ctx)
  • WithTimeout = ให้เวลาทำ 2 วิ
  • WithCancel = ทำจนกว่าจะสั่งหยุดเอง
  • จำ: ได้ cancel มาแล้วต้อง defer cancel() เสมอ ไม่งั้นรั่ว
Sync พื้นฐาน 3 ตัว
go
var mu sync.Mutex
mu.Lock()
// แตะของที่ใช้ร่วมกันตรงนี้
mu.Unlock()

var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    // ทำงาน
}()
wg.Wait() // รอให้หมด

var once sync.Once
once.Do(func() { /* ทำแค่ครั้งเดียว เช่น init */ })

ของใหม่ Go 1.26 มี go fix atomictypes ช่วยเปลี่ยนโค้ด atomic เก่าเป็นแบบใหม่ให้ ถ้าเจอโค้ด sync/atomic แปลกๆ ให้รัน go fix ดู


✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 10: Testing + JSON ใหม่ + HTTP ใหม่ (ของใหม่ 1.27)
1. เทสต์แบบตาราง (table test) เขียนทีเดียวคุ้ม
go
func TestAdd(t *testing.T) {
    tests := []struct{ a, b, want int }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
    }
    for _, tt := range tests {
        if got := Add(tt.a, tt.b); got != tt.want {
            t.Errorf("Add(%d,%d)=%d want %d", tt.a, tt.b, got, tt.want)
        }
    }
}
bash
go test ./...
go test -race ./...  # แถมตรวจแย่งกันด้วย
go test -run TestAdd -v

ของใหม่: testing/synctest + httptest.NewTestServer ไว้เทสต์โค้ด concurrent + HTTP โดยไม่ต้องต่อเน็ตจริง (เซิร์ฟเวอร์ปลอมใน memory) — ถ้าเพิ่งขึ้นกลางรู้ว่ามีก็พอ ยังไม่ต้องใช้ก็ได้

2. JSON ใหม่ เร็วขึ้น (1.27)

Go 1.27 มี encoding/json/v2 (แบบตั้งค่าได้) + jsontext (แบบสตรีม) และเอาไส้ v2 มาใส่ใน encoding/json ตัวเดิมให้เลย = โค้ดเดิมเร็วขึ้นเองโดยไม่ต้องแก้

มือกลางจำแค่นี้:

  • เขียนแบบเดิมได้เลย json.Marshal / Unmarshal ยังใช้ได้
  • ถ้าอยากได้เร็วสุด/เข้มสุดค่อยไปอ่าน json/v2 ทีหลัง
  • ถ้าเจอโค้ดเก่าช้า ลองอัป Go เป็น 1.27 แล้วเทสต์ใหม่ก่อน อาจเร็วขึ้นเฉยๆ
go
type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
b, _ := json.Marshal(User{"Ann", 20})
fmt.Println(string(b)) // {"name":"Ann","age":20}

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ตอนที่ 11: เร็วขึ้นโดยไม่ต้องทำอะไร + Mini Project
ของที่เร็วขึ้นเองใน 1.26-1.27
  • Green Tea GC เปิดเป็นค่าเริ่มต้น (1.26): เก็บขยะไว หยุดรอ (pause) น้อยลง ไม่ต้องตั้งอะไร
  • cgo เร็วขึ้น ~30%: ใครเรียก C จาก Go จะเร็วขึ้นเฉยๆ
  • slice เก็บใน stack ได้บ่อยขึ้น: เร็วขึ้น กินแรมน้อยลง โดยไม่ต้องเปลี่ยนวิธีเขียน

ถ้าโปรแกรมช้า อย่าเพิ่งโทษภาษา ให้เช็ก 3 อย่างก่อน:

  1. ลืม close channel ไหม → goroutine ค้าง
  2. ใช้ map ร่วมกันโดยไม่ล็อกไหม → race
  3. ทำ JSON/HTTP แบบเก่าไหม → อัปเป็น 1.27 แล้ววัดใหม่
Mini Project: API เช็คราคา + timeout + เทสต์

โจทย์: ดึงราคา 5 หุ้นพร้อมกัน ใครเกิน 500ms ถือว่าตก แล้วมีเทสต์

go
package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func fetchPrice(ctx context.Context, s string) (int, error) {
    select {
    case <-time.After(time.Duration(rand.Intn(800)) * time.Millisecond):
        return rand.Intn(1000), nil
    case <-ctx.Done():
        return 0, ctx.Err()
    }
}

func main() {
    symbols := []string{"AAPL", "GOOG", "TSLA", "AMZN", "MSFT"}
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()

    var wg sync.WaitGroup
    ch := make(chan string, len(symbols))

    for _, s := range symbols {
        wg.Add(1)
        go func(sym string) {
            defer wg.Done()
            price, err := fetchPrice(ctx, sym)
            if err != nil {
                ch <- fmt.Sprintf("%s: ตก (%v)", sym, err)
                return
            }
            ch <- fmt.Sprintf("%s = %d", sym, price)
        }(s)
    }
    go func() { wg.Wait(); close(ch) }()
    for r := range ch {
        fmt.Println(r)
    }
}

แบบฝึกต่อ:

  1. เอาโค้ดนี้ไปใส่ go vet + go fix ดูว่ามันแนะนำอะไร
  2. เขียน table test ให้ fetchPrice ด้วย context 10ms ต้อง error
  3. เปลี่ยนจากพิมพ์จอเป็นคืน JSON ด้วย json.Marshal
  4. ลองรัน go test -race + เปลี่ยน buffer channel เป็น 0 แล้วดูว่าช้าลงไหม

ไปต่อ:


✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


คำศัพท์ท้ายเล่ม
คำแปลบ้านๆ
moduleบัตรประชาชนโปรเจกต์ (ไฟล์ go.mod)
dependencyของที่ยืมคนอื่นมาใช้
closureฟังก์ชันจำบ้านเกิดได้
genericสูตรครอบจักรวาล ใช้ได้หลาย type
wrap errorห่อซองจดหมาย (%w) แกะดูไส้ได้ด้วย Is/As
contextรีโมทยกเลิกงาน
pipelineสายพานโรงงาน (channel ต่อกัน)
fan-out/inแตกหลายคนทำแล้วรวมผล
raceแย่งของกันจนพัง (ตรวจด้วย -race)
goroutine leakจ้างแล้วลืมไล่กลับ ค้างตลอดไป
Green Tea GCแม่บ้านเก็บขยะรุ่นใหม่ (1.26) ไวขึ้น
json/v2เครื่องแปลง JSON รุ่นใหม่ เร็วขึ้น (1.27)
go fixครูแก้โค้ดเก่าเป็นแบบใหม่ให้

เล่มกลาง ฉบับภาษาง่าย อัปเดตสำหรับ Go 1.27.1 ตุลาคม 2026 — ต่อจากเล่ม Beginner ถ้าอ่านแล้วอยากกลับไปทวน channel พื้นฐานเปิดเล่มก่อนตอนที่ 13 ได้เลย