สารบัญ
- Generics — type parameter & constraint
- Generic struct & เมื่อไหร่ไม่ควรใช้
- Goroutine & WaitGroup
- Channel
select- Mutex & Race Detector
- Context & Graceful Shutdown
- Worker Pool & GOMAXPROCS
- Project Layout
- GORM
- อ่าน error ของ concurrency
- ตารางกับดักรวม
1 · Generics — type parameter & constraint
func Sum[T Number](values []T) T { ... }
// └── type parameter ── ┘ └ ค่า ┘ └ return
// [] เหลี่ยม = ส่ง type · () กลม = ส่งค่า
| Constraint | มาจากไหน | ให้ทำอะไรได้ |
|---|---|---|
any | built-in | ส่งต่อ/เก็บเท่านั้น — บวก/เทียบไม่ได้ |
comparable | built-in | == != · map key — ไม่ให้ < |
cmp.Ordered | import "cmp" (1.21+) | < > <= >= · ตัวเลข + string |
| เขียนเอง | interface { ~int | ~float64 } | เท่าที่ type ในนั้นทำร่วมกันได้ |
เครื่องหมาย ~
type Integer interface { int | int64 } // รับเฉพาะ int เป๊ะ ๆ
type Integer interface { ~int | ~int64 } // รับทุก type ที่ underlying เป็น int
type Score int
Max(Score(80), Score(95))
// ไม่มี ~ → Score does not satisfy Integer (possibly missing ~ for int in Integer)
เขียน constraint เอง → ใส่ ~ ไว้ก่อนเกือบทุกครั้ง
สำนวนที่ต้องจำ
func Pop[T any]() (T, bool) {
var zero T // ← ทางเดียวที่ได้ zero value ของ T
return zero, false // return nil / return 0 / return T{} ใช้ไม่ได้
}
2 · Generic struct & เมื่อไหร่ไม่ควรใช้
type Stack[T any] struct { items []T }
func (s *Stack[T]) Push(v T) { ... } // receiver ต้องมี [T]
numbers := Stack[int]{} // ต้องระบุ type เสมอ
| สถานการณ์ | เลือก |
|---|---|
สนใจแค่ "มันทำอะไรได้" (io.Writer) | interface |
| ต้องคืน type เดียวกับที่รับเข้ามา | generics |
เก็บของหลาย type ในโครงเดียว (Stack, Cache) | generics |
| เก็บของต่าง type ปนกันใน slice เดียว | interface ([]Shape) |
ต้องใช้ + < == | generics |
| ยังมี type เดียว ยังไม่ซ้ำ | ไม่ต้องทั้งคู่ |
อย่ารีบทำทุกอย่างเป็น generic — วันไหนมี type ที่สองค่อยแปลง
3 · Goroutine & WaitGroup
go doSomething() // สั่งแล้วไปต่อทันที ไม่รอ
main จบ = โปรแกรมจบทันที · runtime ไม่รอ goroutine ที่ค้างอยู่เลย
var wg sync.WaitGroup
wg.Add(1) // ← ต้องอยู่ก่อน go เสมอ
go func() {
defer wg.Done() // ← บรรทัดแรกเสมอ
work()
}()
wg.Wait()
// Go 1.25+ — จับคู่ Add/Done/go ให้ในคำสั่งเดียว
wg.Go(func() { work() })
wg.Wait()
| เขียนผิดแบบ | ผลที่ได้ | ระดับความอันตราย |
|---|---|---|
Add > Done | deadlock! · [sync.WaitGroup.Wait] | เห็นชัด |
Done > Add | panic: sync: negative WaitGroup counter | เห็นชัด |
Add อยู่ใน goroutine | จบปกติ แต่งานหายบางส่วน (วัดได้ 90–96 จาก 100) | อันตรายสุด |
| ส่งแบบไม่ใช่ pointer | ค้างตลอดกาล — go vet จับให้ | vet ช่วย |
4 · Channel
ch := make(chan int) // unbuffered — ความจุ 0
ch := make(chan int, 3) // buffered
var ch chan int // nil — ใช้ไม่ได้ ต้อง make
ch <- 10 // ส่ง
v := <-ch // รับ
v, ok := <-ch // รับ + เช็คว่าปิดหรือยัง
close(ch)
| Unbuffered | Buffered (cap N) | |
|---|---|---|
| ส่ง | รอจนมีคนรับถึงมือ | ผ่านเลยถ้ายังไม่เต็ม · เต็มแล้วรอ |
| รับ | รอจนมีคนส่ง | ได้เลยถ้ามีของ · ว่างแล้วรอ |
| ใช้เมื่อ | ต้องการ นัดเจอ / จับจังหวะ | ต้องการลดการรอ |
เมื่อ channel ถูกปิดแล้ว
| ทำอะไร | ผล |
|---|---|
| รับ ตอนยังมีของ | ได้ของนั้น |
| รับ ตอนไม่มีของ | zero value ทันที ไม่ block · ok = false |
for range | วนจนของหมดแล้วออกจาก loop |
| ส่ง | 💥 panic: send on closed channel |
close ซ้ำ | 💥 panic: close of closed channel |
คนส่งเป็นคนปิด ไม่ใช่คนรับ · มีคนส่งหลายคน → ให้ตัวที่รอทุกคนเสร็จเป็นคนปิด ·
ไม่ปิดเลยก็ได้ถ้าไม่มีใคร range (ไม่ใช่ memory leak)
Directional channel
func producer(ch chan<- int) // ส่งอย่างเดียว — ลูกศรหลัง chan
func consumer(ch <-chan int) // รับอย่างเดียว — ลูกศรหน้า chan
// consumer สั่ง close ไม่ได้:
// cannot close receive-only channel ch (variable of type <-chan int)
5 · select
select {
case msg := <-messages: // รับ
case ch <- value: // ส่ง — ใส่ได้เหมือนกัน
case <-time.After(2*time.Second): // timeout
case <-ctx.Done(): // ยกเลิก
default: // ไม่มีใครพร้อม → ทำทันที ไม่รอ
}
| สถานการณ์ | ไม่มี default | มี default |
|---|---|---|
| มี case พร้อม 1 อัน | ทำอันนั้น | |
| พร้อมหลายอัน | สุ่มเลือก (วัดได้ ~50/50 จาก 1000 รอบ) | |
| ไม่มีใครพร้อม | รอ (ไม่กิน CPU) | ทำ default ทันที |
⚠️ default ใน for เปล่า ๆ = busy loop
(วัดได้ 95.7 ล้านรอบใน 0.3 วิ · กิน CPU เต็ม core) —
เอา default ออกให้ select block แทน
// ✅ default ที่ใช้ถูก — มีอย่างอื่นให้ทำจริง
select {
case metrics <- stat:
default:
droppedCount++ // buffer เต็ม ยอมทิ้ง
}
6 · Mutex & Race Detector
var mu sync.Mutex
mu.Lock()
defer mu.Unlock() // ← บรรทัดถัดไปเสมอ
counter++
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(k string) (string, bool) {
c.mu.RLock(); defer c.mu.RUnlock() // อ่าน — หลายคนพร้อมกันได้
v, ok := c.data[k]; return v, ok
}
func (c *Cache) Set(k, v string) {
c.mu.Lock(); defer c.mu.Unlock() // เขียน — คนเดียว
c.data[k] = v
}
วัดจริง — 2000 goroutine อ่านพร้อมกัน:
Mutex 12.4ms · RWMutex 0.8ms → เร็วขึ้น ~16 เท่า
Race Detector
go run -race main.go
go test -race ./...
go build -race
// เจอ race → WARNING: DATA RACE (บอกบรรทัดที่อ่าน/เขียน/สร้าง goroutine)
// Found N data race(s)
// exit status 66 ← CI แดงเองอัตโนมัติ
ฟ้อง = มีจริง 100% · ไม่ฟ้อง ≠ ไม่มี (ตรวจเฉพาะเส้นทางที่รันจริง)
เก้ากฎความปลอดภัย
- ไม่ share ก็ไม่ต้อง lock
- ของกลางต้องมีเจ้าของ — ใครเขียนได้ · ใครอ่านได้ · sync ด้วยอะไร
- Lock ให้แคบ แต่ครบ — อย่าครอบ network/DB call
- อย่า copy
Mutex/RWMutex/WaitGroupหลังเริ่มใช้ defer Unlock()เสมอ- ทุก goroutine ต้องตอบได้ว่าจะจบเมื่อไหร่
- คิดเรื่องยกเลิกตั้งแต่ออกแบบ
- รัน race detector เป็นนิสัย
- "ทำ concurrent ได้" ≠ "ควรทำ"
7 · Context & Graceful Shutdown
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // ← ทุกครั้ง ไม่มีข้อยกเว้น
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
doChunkOfWork()
}
}
}
ctx.Err() | แปลว่า | HTTP status |
|---|---|---|
nil | ยังทำงานต่อได้ | — |
context.Canceled | มีคนเรียก cancel() · หรือได้ SIGTERM | 499 (ผู้ใช้ไปแล้ว) |
context.DeadlineExceeded | หมดเวลาตาม timeout | 504 |
ใช้ errors.Is ไม่ใช่ == ·
ctx เป็น parameter ตัวแรก เสมอ ·
ห้ามเก็บใน struct · ห้ามส่ง nil (ใช้ context.TODO())
⚠️ cancel() ไม่หยุด goroutine — แค่ปิด channel
ตัวที่ไม่เช็ค ctx.Done() จะทำงานต่อไปเรื่อย ๆ
context = บอกให้หยุด · WaitGroup = รอจนหยุดจริง — ต้องมีคู่กัน
context ซ้อนกัน → ตัวที่หมดก่อนชนะ · child ขยายเวลาเกิน parent ไม่ได้
Graceful Shutdown
ctx, stop := signal.NotifyContext(
context.Background(), os.Interrupt, syscall.SIGTERM,
)
defer stop()
<-ctx.Done() // ค้างจนได้สัญญาณ → ctx.Err() = context canceled
srv.Shutdown(shutdownCtx) // เก็บงานค้างให้จบ
8 · Worker Pool & GOMAXPROCS
func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs { results <- process(job) }
}
for w := 1; w <= numWorkers; w++ { wg.Add(1); go worker(w, jobs, results, &wg) }
for _, j := range allJobs { jobs <- j }
close(jobs)
go func() { wg.Wait(); close(results) }() // ← ต้องอยู่ใน goroutine!
for r := range results { ... }
คนที่ รอ กับคนที่ รับ ต้องไม่ใช่ goroutine เดียวกัน —
ไม่งั้น deadlock! [sync.WaitGroup.Wait]
| งานแบบไหน | ตั้ง worker เท่าไหร่ | ตัวจำกัดจริง |
|---|---|---|
| CPU-bound (คำนวณ) | runtime.GOMAXPROCS(0) | จำนวน core |
| I/O-bound (API, ไฟล์) | สูงได้ 50–500 | bandwidth · rate limit ปลายทาง |
| Database | ≤ ขนาด connection pool | connection ที่ DB รับได้ |
วัดจริง 60 งาน บนเครื่อง GOMAXPROCS = 10:
| worker | 1 | 2 | 5 | 10 | 20 | 60 |
|---|---|---|---|---|---|---|
I/O (Sleep) | 6.09s | 3.05s | 1.22s | 610ms | 310ms | 100ms |
| CPU (คำนวณ) | 10.16s | 5.38s | 2.57s | 1.74s | 1.71s | 1.60s |
งาน CPU ตันที่ GOMAXPROCS · งาน I/O ดีขึ้นเรื่อย ๆ จนกว่า worker จะเท่าจำนวนงาน
⚠️ ใน container: Go ก่อน 1.25 เห็น core ของเครื่องแม่ ไม่ใช่ CPU limit
9 · Project Layout
myapp/
├── cmd/api/main.go ← จุดเริ่ม ประกอบทุกอย่าง (มีได้หลายตัว)
├── internal/
│ ├── user/ ← จัดตามโดเมน ไม่ใช่ตามชนิดไฟล์
│ │ ├── handler.go HTTP ↔ JSON · ไม่รู้จัก database
│ │ ├── service.go กฎธุรกิจ · ไม่รู้จัก HTTP
│ │ ├── repository.go SQL อย่างเดียว
│ │ ├── model.go struct ที่ตรงกับตาราง
│ │ └── dto.go struct request/response (มี json tag)
│ └── platform/database/ ← ของกลางที่ทุกโดเมนใช้
├── go.mod
└── go.sum
internal/ — module อื่น import ไม่ได้จริง:
use of internal package ... not allowed
แยก dto.go จาก model.go เพื่อไม่ให้
PasswordHash หลุดโดยลืมใส่ json:"-"
ลูกศรชี้ทางเดียว: handler → service → repository ·
ctx เดินทางไปด้วยทุกชั้น
10 · GORM
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go mod tidy ← ลืมแล้วจะเจอ missing go.sum entry
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Email string `gorm:"unique"`
}
// User → ตาราง "users" · OrderItem → ตาราง "order_items"
db.Create(&user) // เขียน ID กลับเข้า struct (ต้องมี &)
db.First(&user, 1) // หา id = 1
db.Find(&users) // ทั้งหมด
db.Where("name = ?", name).Find(&users) // ใช้ ? เสมอ — กัน SQL injection
db.Model(&user).Update("Name", "Somsri")
db.Delete(&user, 1)
db.WithContext(ctx).Find(&users) // ส่ง ctx ต่อถึง database
⚠️ GORM คืน *gorm.DB ไม่ใช่ (value, error) —
ต้องเช็ค .Error เอง compiler ไม่บังคับ
| สถานการณ์ | ผลจริงที่รันได้ |
|---|---|
First ไม่เจอ | gorm.ErrRecordNotFound — ใช้ errors.Is |
Find ไม่เจอ | .Error = nil · RowsAffected = 0 ← ไม่ error |
Update/Delete ไม่มี Where | WHERE conditions required (gorm.ErrMissingWhereClause) |
มี DeletedAt gorm.DeletedAt | Delete = soft delete · Unscoped() ถึงจะเห็น |
| soft delete แล้วสร้าง email เดิม | 💥 ชน unique constraint — แถวเดิมยังอยู่จริง |
Transaction
db.Transaction(func(tx *gorm.DB) error {
tx.Model(...).Update("balance", gorm.Expr("balance - ?", 50))
return nil // nil = commit · error = rollback
})
ใช้ tx ข้างใน ไม่ใช่ db — เผลอใช้ db = คำสั่งนั้นอยู่นอก transaction
11 · อ่าน error ของ concurrency
เวลาเจอ fatal error: all goroutines are asleep - deadlock!
ให้ดูคำในวงเล็บเหลี่ยมของบรรทัด goroutine N [...] เป็นอันดับแรก
| ในวงเล็บเขียนว่า | แปลว่าค้างที่ | สาเหตุที่พบบ่อย |
|---|---|---|
[sync.WaitGroup.Wait] | wg.Wait() | Add เกิน Done · ส่ง wg แบบไม่ใช่ pointer |
[chan send] | ch <- x | ไม่มีใครรับ · buffer เต็ม |
[chan receive] | <-ch หรือ for range ch | ลืม close · ไม่มีใครส่ง |
[chan receive (nil chan)] | channel ที่ยังไม่ make | var ch chan int เฉย ๆ |
[sync.Mutex.Lock] / [sync.RWMutex.RLock] | ขอ lock | ลืม Unlock · ขอ lock ซ้อนตัวเอง |
sync.Mutex ของ Go ไม่ reentrant —
method ที่ถือ lock อยู่แล้วเรียก method อื่นที่ขอ lock เดิม = ค้างตลอดกาล
ทางแก้: แยก method ที่ไม่ล็อก ออกมาเป็นตัว unexported
คำสั่งที่ควรรันก่อน commit ทุกครั้ง
go vet ./... // จับ copy lock · lostcancel · misuse ต่าง ๆ
go test -race ./... // จับ data race
12 · ตารางกับดักรวม
| เขียนแบบนี้ | เกิดอะไรขึ้น |
|---|---|
var total any แล้ว total.(int) | 💥 interface conversion: interface {} is nil, not int |
constraint int | int64 กับ type Score int | ❌ possibly missing ~ for int in Integer |
comparable แล้วใช้ > | ❌ type parameter T cannot use operator > |
[]int เป็น comparable | ❌ []int does not satisfy comparable |
Stack{} ไม่ระบุ type | ❌ cannot use generic type ... without instantiation |
| method มี type parameter ของตัวเอง | ❌ method must have no type parameters |
ลืม make map ใน generic struct | 💥 panic: assignment to entry in nil map |
go f() แล้ว main จบเลย | ⚠️ goroutine ไม่ได้ทำงาน — เงียบสนิท |
wg.Add(1) อยู่ใน goroutine | ⚠️ งานหายบางส่วน (วัดได้ 90–96 จาก 100) — ไม่ error |
ส่ง wg sync.WaitGroup ไม่มีดาว | ค้าง — go vet: passes lock by value |
| ส่งเข้า unbuffered channel ใน goroutine เดียว | 💥 deadlock! [chan send] |
for range ch โดยไม่ close | 💥 deadlock! [chan receive] |
| ส่งเข้า channel ที่ปิดแล้ว | 💥 panic: send on closed channel |
select + default ใน for เปล่า | ⚠️ busy loop — CPU 100% แบบไม่มี error |
| timeout แล้วปล่อย goroutine ค้างส่ง | ⚠️ goroutine leak — แก้ด้วย buffer ขนาด 1 |
counter++ หลาย goroutine | ⚠️ ค่าหาย (988/947/956/…) — ไม่ error |
| เขียน map หลาย goroutine | 💥 fatal error: concurrent map writes |
| value receiver บน struct ที่มี Mutex | ⚠️ ล็อกสำเนา = ไม่ได้ล็อก — go vet จับ |
| method ที่ถือ lock เรียก method ที่ขอ lock เดิม | 💥 ค้างตลอดกาล — Mutex ไม่ reentrant |
ctx, _ := context.WithTimeout(...) | ⚠️ context leak — go vet: cancel ... should be called |
goroutine ที่ไม่เช็ค ctx.Done() | ⚠️ cancel() ไม่มีผลกับมันเลย |
wg.Wait(); close(results) ไม่อยู่ใน goroutine | 💥 deadlock! [sync.WaitGroup.Wait] |
เช็คแต่ .Error หลัง GORM Find | ⚠️ ไม่เจอข้อมูลก็ไม่ error — ต้องดู RowsAffected |
| soft delete แล้วสร้างซ้ำด้วย email เดิม | 💥 duplicate key value violates unique constraint |
ใช้ db แทน tx ใน transaction | ⚠️ คำสั่งนั้น rollback ไม่ได้ |
go get แล้วไม่ go mod tidy | ❌ missing go.sum entry |