Go เป็นภาษาที่เรียนรู้ง่าย แต่เขียนให้ดีนั้นยากกว่าที่คิด ผมทำงานกับ Go มาหลายปีในทีม backend และสิ่งที่เจอบ่อยที่สุดไม่ใช่ bug — มันคือ code ที่อ่านยาก, error ที่ไม่มีความหมาย, และ goroutine ที่ไม่มีใครรู้ว่ามันทำอะไรอยู่
นี่คือสิ่งที่ผมอยากบอกตัวเองตอนเริ่มต้น
error handling ที่มีความหมาย
สิ่งแรกที่คนมักทำผิดคือ wrap error แบบไม่มีบริบท
// แย่ — ไม่รู้เกิดอะไรขึ้นที่ไหน
if err != nil {
return err
}
// ดีกว่า — มีบริบทให้ trace ได้
if err != nil {
return fmt.Errorf("fetchUser %s: %w", userID, err)
}
ใช้ %w เสมอถ้าคุณต้องการให้ caller ใช้ errors.Is() หรือ errors.As() ได้ ถ้าใช้ %v แล้ว error chain จะขาด
struct patterns ที่อ่านออกหลัง 6 เดือน
อย่าใส่ทุกอย่างลง struct เดียวแล้วหวังว่าจะจำได้
// แย่ — สารพัด field ไม่รู้ว่าใครใช้อะไร
type Service struct {
db *sql.DB
cache *redis.Client
cfg Config
logger *zap.Logger
timeout time.Duration
retries int
secret string
}
// ดีกว่า — แยก concerns ออกจากกัน
type Service struct {
db *sql.DB
cache *redis.Client
logger *zap.Logger
cfg ServiceConfig
}
type ServiceConfig struct {
Timeout time.Duration
Retries int
Secret string
}
struct ที่ดีคือ struct ที่คุณอ่านแล้วรู้ทันทีว่า dependency มาจากไหน และ config คืออะไร
context ไม่ใช่ของเล่น
หลายคนส่ง context.Background() ทุกที่เพราะมันง่าย แต่นั่นหมายความว่า request จะไม่มีวัน timeout เว้นแต่ server จะตาย
// อย่าทำแบบนี้ใน production handler
func (s *Service) GetUser(id string) (*User, error) {
return s.db.QueryRow(context.Background(), ...) // ☠️
}
// ทำแบบนี้แทน — รับ context มาจาก caller เสมอ
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
return s.db.QueryRow(ctx, ...)
}
rule ง่ายๆ: ถ้า function ทำ I/O ต้องรับ ctx context.Context เป็น argument แรกเสมอ ไม่มีข้อยกเว้น
goroutine leak ที่เจอบ่อย
// leak — channel ไม่มีใครอ่าน goroutine จะ block ตลอดไป
func process(items []string) {
ch := make(chan result)
for _, item := range items {
go func(i string) {
ch <- doWork(i) // block ตลอดถ้าไม่มีคนอ่าน
}(item)
}
// ลืม drain channel
}
// ดีกว่า — ใช้ errgroup หรือ WaitGroup
func process(ctx context.Context, items []string) error {
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item
g.Go(func() error {
return doWork(ctx, item)
})
}
return g.Wait()
}
สรุป
Go ไม่ใช่ภาษาที่ซับซ้อน แต่มันให้อิสระมากพอที่จะเขียน code ห่วยได้ง่ายมาก สิ่งที่ทำให้ code ดีคือ discipline ไม่ใช่ feature ของภาษา
- wrap error ทุกครั้งด้วย
%wและบริบทที่มีความหมาย - แยก config ออกจาก dependency ใน struct
- ส่ง context ทุกที่ที่มี I/O
- ใช้
errgroupแทนการจัดการ goroutine เอง
เขียนให้คนอ่านได้ก่อน แล้วค่อย optimize ทีหลัง