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

🗄️

เจาะลึก Database + API Design ฉบับภาษาง่าย

pool, transaction, handler/store, migration

7 บท
10 ข้อสอบ
1. ต่อ DB ด้วย database/sql
go
import (
    "database/sql"
    _ "github.com/jackc/pgx/v5/stdlib" // driver postgres (ตัวอย่าง)
)
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil { return err }
defer db.Close()
if err := db.PingContext(ctx); err != nil { return err }

sql.Open แค่จองยังไม่ต่อจริง ต้อง Ping เช็ก อย่าลืม defer Close()

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


2. Pool + Context + Transaction

ตั้ง pool กันล่ม:

go
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

ทุก query ส่ง ctx ไปด้วย (timeout/yกเลิกได้):

go
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=$1", id)

เงินโอนต้อง transaction (ทั้งหมดหรือไม่เลย):

go
tx, _ := db.BeginTx(ctx, nil)
defer tx.Rollback() // พังก็ถอย
_, _ = tx.ExecContext(ctx, "UPDATE acc SET bal=bal-100 WHERE id=$1", from)
_, _ = tx.ExecContext(ctx, "UPDATE acc SET bal=bal+100 WHERE id=$1", to)
tx.Commit()

ใช้ ? หรือ $1 ตาม driver อย่าเอา string มาต่อ SQL เอง (โดน injection)

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


3. ออกแบบ API: handler/store แยกกัน
code
myapp/
  main.go (ต่อของ + graceful)
  api/user.go (รับ request/ตอบ JSON อย่างเดียว)
  store/user.go (คุย DB อย่างเดียว)
go
// store: คืนของจริง + error
func (s *Store) GetUser(ctx context.Context, id string) (User, error)
// api: แปล error เป็น status
user, err := s.GetUser(ctx, id)
if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r); return }

handler อย่าคุย DB ตรง ให้ผ่าน store จะได้เทสต์โดยใช้ store ปลอมได้

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


4. Validate + Error -> status ให้ถูก
go
if id == "" { http.Error(w, "id จำเป็น", 400); return } // 400 = client ผิด
if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r); return } // 404
slog.Error("db fail", "err", err)
http.Error(w, "ระบบขัดข้อง", 500) // 500 อย่าโชว์ err ดิบให้ user

ตอบ JSON ด้วย encoding/json เดิม (1.27 เร็วขึ้นเอง) อยากเข้มค่อยไป json/v2 (ดูเล่ม testing-http)

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


5. Migration + เทสต์ DB
  • เปลี่ยนโครง DB ใช้ migration (เช่น golang-migrate) ไม่ใช่แก้ด้วยมือ
  • เทสต์ handler ใช้ httptest.NewTestServer (ใหม่ 1.27 in-memory) + เทสต์ concurrent ใช้ synctest จะได้ไม่ flaky
  • เทสต์ store ใช้ DB จริงใน docker แยก + t.Cleanup ล้างข้อมูลทุกครั้ง

เช็กลิสต์: pool ตั้งแล้ว, ทุก query มี ctx timeout, tx มี rollback, ไม่ต่อ SQL เอง, ไม่ log รหัส

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


ภาคผนวก: ตัวอย่างเพิ่ม + Best Practices
ตัวอย่างเพิ่ม 1: ตั้ง pool ให้ถูก (ห้ามเปิดไม่จำกัด)
go
db, err := sql.Open("postgres", dsn)
if err != nil {
    return err
}
db.SetMaxOpenConns(25)                 // อย่าเปิดไม่จำกัด DB ตาย
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute) // หมุนเวียน connection เก่า
if err := db.PingContext(ctx); err != nil { // sql.Open ยังไม่ต่อจริง ต้อง Ping
    return err
}
ตัวอย่างเพิ่ม 2: helper ทำ transaction ปลอดภัย
go
func inTx(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) (err error) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer func() {
        if err != nil {
            tx.Rollback() // พัง = ยกเลิกหมด
        }
    }()
    if err = fn(tx); err != nil {
        return err
    }
    return tx.Commit() // ผ่านหมด = บันทึก
}
ตัวอย่างเพิ่ม 3: query แบบมี timeout ทุกครั้ง
go
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
var name string
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=$1", id).Scan(&name)

ใช้ placeholder ($1) เสมอ ห้ามต่อ string เอง กัน SQL injection

Best practices เช็กลิสต์
  • sql.Open ยังไม่ต่อจริง ต้อง Ping เช็กก่อนใช้
  • ทุก query ผ่าน ...Context + timeout กันค้างทั้งระบบ
  • เขียน SQL ด้วย placeholder ($1/?) ห้ามต่อ string เอง
  • tx: เริ่ม-ทำ-commit เป็นชุดเดียว พังตรงไหน rollback หมด (ใช้ helper ข้างบน)
  • pool ตั้ง 3 ค่าเสมอ (MaxOpen/MaxIdle/Lifetime)
  • แยก handler (รับ request) กับ store (คุย DB) อย่าปนกัน เทสต์ง่ายกว่าเยอะ
  • ไม่ log dsn/รหัสผ่าน เปลี่ยนโครง DB ใช้ migration ไม่ใช่แก้ด้วยมือ
แบบฝึกหัด
  • เขียน inTx โอนแต้ม user A ไป B แล้วเทสต์ว่าพังกลางทางยอดไม่หาย
  • ใส่ timeout ทุก query ใน handler ของตัวเองแล้วลองยิง request ดู

อ้างอิง: std database/sql, Go 1.27 NewTestServer/synctest/json