Reference · เก็บไว้เปิดตอนเขียนโค้ด

Go Day 6 — Cheat Sheet

Generics · Concurrency · Project Layout · GORM · ตัวอย่างทุกอันรันจริงบน Go 1.26.5 · GORM v1.31.2 กับ PostgreSQL 17 · พิมพ์ลงกระดาษได้

สารบัญ

  1. Generics — type parameter & constraint
  2. Generic struct & เมื่อไหร่ไม่ควรใช้
  3. Goroutine & WaitGroup
  4. Channel
  5. select
  6. Mutex & Race Detector
  7. Context & Graceful Shutdown
  8. Worker Pool & GOMAXPROCS
  9. Project Layout
  10. GORM
  11. อ่าน error ของ concurrency
  12. ตารางกับดักรวม

1 · Generics — type parameter & constraint

func Sum[T Number](values []T) T { ... }
//       └── type parameter ── ┘  └ ค่า ┘  └ return

// [] เหลี่ยม = ส่ง type   ·   () กลม = ส่งค่า
Constraintมาจากไหนให้ทำอะไรได้
anybuilt-inส่งต่อ/เก็บเท่านั้น — บวก/เทียบไม่ได้
comparablebuilt-in== != · map key — ไม่ให้ <
cmp.Orderedimport "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 > Donedeadlock! · [sync.WaitGroup.Wait]เห็นชัด
Done > Addpanic: 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)
UnbufferedBuffered (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% · ไม่ฟ้อง ≠ ไม่มี (ตรวจเฉพาะเส้นทางที่รันจริง)

เก้ากฎความปลอดภัย

  1. ไม่ share ก็ไม่ต้อง lock
  2. ของกลางต้องมีเจ้าของ — ใครเขียนได้ · ใครอ่านได้ · sync ด้วยอะไร
  3. Lock ให้แคบ แต่ครบ — อย่าครอบ network/DB call
  4. อย่า copy Mutex/RWMutex/WaitGroup หลังเริ่มใช้
  5. defer Unlock() เสมอ
  6. ทุก goroutine ต้องตอบได้ว่าจะจบเมื่อไหร่
  7. คิดเรื่องยกเลิกตั้งแต่ออกแบบ
  8. รัน race detector เป็นนิสัย
  9. "ทำ 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() · หรือได้ SIGTERM499 (ผู้ใช้ไปแล้ว)
context.DeadlineExceededหมดเวลาตาม timeout504

ใช้ 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–500bandwidth · rate limit ปลายทาง
Database≤ ขนาด connection poolconnection ที่ DB รับได้

วัดจริง 60 งาน บนเครื่อง GOMAXPROCS = 10:

worker125102060
I/O (Sleep)6.09s3.05s1.22s610ms310ms100ms
CPU (คำนวณ)10.16s5.38s2.57s1.74s1.71s1.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 ไม่มี WhereWHERE conditions required (gorm.ErrMissingWhereClause)
มี DeletedAt gorm.DeletedAtDelete = 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 ที่ยังไม่ makevar 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

อ้างอิง