pool, transaction, handler/store, migration
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()
ตั้ง pool กันล่ม:
ทุก query ส่ง ctx ไปด้วย (timeout/yกเลิกได้):
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=$1", id)เงินโอนต้อง transaction (ทั้งหมดหรือไม่เลย):
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)
myapp/
main.go (ต่อของ + graceful)
api/user.go (รับ request/ตอบ JSON อย่างเดียว)
store/user.go (คุย DB อย่างเดียว)// 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 ปลอมได้
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)
httptest.NewTestServer (ใหม่ 1.27 in-memory) + เทสต์ concurrent ใช้ synctest จะได้ไม่ flakyt.Cleanup ล้างข้อมูลทุกครั้งเช็กลิสต์: pool ตั้งแล้ว, ทุก query มี ctx timeout, tx มี rollback, ไม่ต่อ SQL เอง, ไม่ log รหัส
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
}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() // ผ่านหมด = บันทึก
}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
sql.Open ยังไม่ต่อจริง ต้อง Ping เช็กก่อนใช้...Context + timeout กันค้างทั้งระบบ$1/?) ห้ามต่อ string เองinTx โอนแต้ม user A ไป B แล้วเทสต์ว่าพังกลางทางยอดไม่หายอ้างอิง: std database/sql, Go 1.27 NewTestServer/synctest/json