Go: عمليات قواعد البيانات في Go
توفر حزمة
database/sqlفي لغة Go واجهة موحدة لعمليات قواعد البيانات التي تعمل بلغة SQL — حيث يمكنك التبديل بين MySQL وSQLite وPostgreSQL بمجرد تغيير برنامج التشغيل.
عندما تحتاج إلى التعامل مع قواعد البيانات بأمان — لمنع هجمات حقن SQL، وإدارة المعاملات، وتكوين مجموعات الاتصالات — توفر المكتبة القياسية database/sql مجموعة كاملة وآمنة من الأدوات.
1. ستتعلم
database/sql: تسجيل برامج التشغيل وفتح الاتصالات- ثلاث طرق للاستعلام:
db.Query،db.QueryRow، وdb.Exec db.Prepare: منع هجمات حقن SQL باستخدام التجميع المسبق- المعاملة:
Begin/Commit/Rollback - تكوين مجموعة الاتصالات:
SetMaxOpenConns/SetMaxIdleConns - استخدام برامج تشغيل SQLite و MySQL
2. قصة حقيقية لمهندس برمجيات الخلفية
(1) المشكلة: أدى ربط السلاسل في لغة SQL إلى حذف قاعدة البيانات عبر هجوم حقن نفذه أحد المتسللين
بوب هو مهندس برمجيات الخلفية في منصة للتجارة الإلكترونية. وهو بحاجة إلى تنفيذ واجهة برمجة تطبيقات (API) للبحث عن المنتجات:
"استخدمت
fmt.Sprintfلإنشاء استعلام SQL للمنتجات:SELECT * FROM products WHERE name LIKE '%" + search + "%'. في اليوم الثاني بعد الإطلاق، أدخل أحدهم' OR 1=1; DROP TABLE products;--في مربع البحث. فاختفى جدول المنتجات الخاص بي. فسألني مديري: 'أين ذهبت الـ 100,000 منتج الموجودة في قاعدة البيانات؟'"
// Bad code: string concatenation SQL, fatal vulnerability
func searchProducts(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("q")
// If search = "' OR 1=1; DROP TABLE products;--"
// Final SQL: SELECT * FROM products WHERE name LIKE '%' OR 1=1; DROP TABLE products;--%'
query := fmt.Sprintf("SELECT * FROM products WHERE name LIKE '%%%s%%'", search)
rows, err := db.Query(query) // Nightmare begins
}
(2) حل Go: PreparedStatement المُجمَّع مسبقًا
// Good code: precompiled, parameters automatically escaped
func searchProducts(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("q")
// ? is a placeholder; the database driver automatically escapes parameters
// The search value is always treated as a string, never as part of SQL syntax
stmt, err := db.Prepare("SELECT * FROM products WHERE name LIKE ?")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer stmt.Close()
rows, err := stmt.Query("%" + search + "%")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
// Process results...
}
(3) الأداء: ربط السلاسل مقابل لغة SQL المُعدة مسبقًا
| البعد | تسلسل السلاسل | PreparedStatement |
|---|---|---|
| حقن SQL | ❌ مخاطر عالية | ✅ التهرب التلقائي |
| الأداء | تحليل SQL لكل عملية تنفيذ | ✅ يتم تحليله مرة واحدة فقط؛ أسرع في عمليات التنفيذ اللاحقة |
| سهولة القراءة | مربك | ✅ واضح |
| نوع المعلمة | تحويل الكل إلى سلاسل | ✅ الحفاظ على النوع |
3. الاتصال بقاعدة البيانات
▶ مثال: الاتصال بـ SQLite
⚙️ المتطلب المسبق: تشغيل
go get github.com/mattn/go-sqlite3⚠️ ملاحظة: يتطلب go-sqlite3 وجود CGO؛ ويحتاج نظام Windows إلى gcc (MinGW-w64)، أما نظاما macOS وLinux فيتضمنانه بشكل افتراضي
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3" // Import driver (_ means only init is executed, not directly used)
)
func main() {
// Open database (actual connection is established on first query)
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Test if connection is reachable
err = db.Ping()
if err != nil {
log.Fatal("Cannot connect to database:", err)
}
fmt.Println("Database connected successfully")
}
▶ مثال: الاتصال بـ MySQL
⚙️ المتطلب المسبق: تشغيل
go get github.com/go-sql-driver/mysql
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// DSN format: user:password@tcp(host:port)/dbname?params
dsn := "root:password@tcp(127.0.0.1:3306)/shop?charset=utf8mb4&parseTime=true"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Connection pool configuration
db.SetMaxOpenConns(25) // Maximum open connections
db.SetMaxIdleConns(10) // Maximum idle connections
db.SetConnMaxLifetime(5 * time.Minute) // Maximum connection lifetime
db.SetConnMaxIdleTime(2 * time.Minute) // Maximum idle connection lifetime
if err = db.Ping(); err != nil {
log.Fatal("Cannot connect to database:", err)
}
fmt.Println("MySQL connected successfully")
}
(3) الطرق الرئيسية في قواعد البيانات/SQL
| الطريقة | الغرض | النتيجة |
|---|---|---|
sql.Open(driver, dsn) |
يفتح قاعدة البيانات (التحميل المؤجل) | *DB، error |
db.Ping() |
يختبر ما إذا كان من الممكن فعليًّا الوصول إلى الاتصال | error |
db.Close() |
إغلاق قاعدة البيانات | error |
db.Exec(sql, args...) |
تنفيذ أوامر INSERT/UPDATE/DELETE | Result، error |
db.Query(sql, args...) |
ينفذ عبارة SELECT ويعرض عدة صفوف | *Rows، error |
db.QueryRow(sql, args...) |
ينفذ عبارة SELECT ويعيد صفًا واحدًا | *Row |
db.Prepare(sql) |
تجميع SQL مسبقًا | *Stmt، error |
sql.Open فعليًّا بإنشاء اتصال — بل تكتفي بالتحقق من صحة تنسيق DSN. يتم إنشاء الاتصال الفعلي أثناء تنفيذ أول Ping أو Query / Exec. لذلك، فإن حقيقة أن sql.Open تُرجع nil error لا تعني أن قاعدة البيانات متاحة.
4. عمليات CRUD
▶ مثال: إنشاء جدول وإدخال البيانات
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./shop.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create table
createSQL := `
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
_, err = db.Exec(createSQL)
if err != nil {
log.Fatal(err)
}
fmt.Println("Table created successfully")
// Insert data (Exec returns Result)
result, err := db.Exec(
"INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
"Laptop", 999.99, 10,
)
if err != nil {
log.Fatal(err)
}
id, _ := result.LastInsertId()
affected, _ := result.RowsAffected()
fmt.Printf("Inserted: id=%d, rows affected=%d\n", id, affected)
}
▶ مثال: الاستعلام عن البيانات
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
type Product struct {
ID int
Name string
Price float64
Stock int
}
func main() {
db, err := sql.Open("sqlite3", "./shop.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// QueryRow: query a single row
var p Product
err = db.QueryRow("SELECT id, name, price, stock FROM products WHERE id = ?", 1).
Scan(&p.ID, &p.Name, &p.Price, &p.Stock)
if err == sql.ErrNoRows {
fmt.Println("Product not found")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Printf("Product: %+v\n", p)
}
// Query: query multiple rows
rows, err := db.Query("SELECT id, name, price, stock FROM products WHERE price < ?", 500)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var pr Product
err := rows.Scan(&pr.ID, &pr.Name, &pr.Price, &pr.Stock)
if err != nil {
log.Fatal(err)
}
fmt.Printf(" Product: %+v\n", pr)
}
// Check if iteration encountered an error
if err = rows.Err(); err != nil {
log.Fatal(err)
}
}
(3) مقارنة بين طرق CRUD
| العملية | الطريقة | القيمة المرجعة | حالة الاستخدام |
|---|---|---|---|
| إنشاء | db.Exec(INSERT...) |
Result (LastInsertId + RowsAffected) |
إدراج/تحديث/حذف |
| قراءة صف واحد | db.QueryRow(SELECT...).Scan() |
*Row + الإغلاق التلقائي |
استعلام صف واحد |
| قراءة عدة صفوف | db.Query(SELECT...); rows.Scan() |
*Rows (يتطلب التكرار + الإغلاق) |
استعلام متعدد الصفوف |
| تحديث | db.Exec(UPDATE...) |
Result (الصفوف المتأثرة) |
تحديث البيانات |
| حذف | db.Exec(DELETE...) |
Result (الصفوف المتأثرة) |
حذف البيانات |
rows.Close() — حتى لو كنت قد قمت بالفعل بالتكرار عبر جميع الصفوف في for rows.Next(). سيؤدي عدم إغلاقه إلى حدوث تسرب في الاتصال (لن يتم إعادة الاتصال إلى المجمع). استخدم defer rows.Close() لضمان إعادته إلى المجمع.
5. PreparedStatement
▶ مثال: الإدراج الدفعي المُعد مسبقًا
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
type Product struct {
Name string
Price float64
Stock int
}
func main() {
db, err := sql.Open("sqlite3", "./shop.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Precompile SQL statement
stmt, err := db.Prepare("INSERT INTO products (name, price, stock) VALUES (?, ?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// Batch insert (SQL compiled only once)
products := []Product{
{"Mouse", 29.99, 100},
{"Keyboard", 79.99, 50},
{"Monitor", 299.99, 20},
{"USB-C Hub", 49.99, 200},
}
for _, p := range products {
result, err := stmt.Exec(p.Name, p.Price, p.Stock)
if err != nil {
log.Printf("Insert failed %s: %v", p.Name, err)
continue
}
id, _ := result.LastInsertId()
fmt.Printf("Inserted successfully: %s (id=%d)\n", p.Name, id)
}
}
(2) لغة SQL المُجمَّعة مسبقًا مقابل لغة SQL المُربَّطة
| المقارنة | التجميع المسبق Prepare |
التسلسل في SQL |
|---|---|---|
| حقن SQL | ✅ التهرب التلقائي من المعلمات | ❌ مخاطر عالية |
| الأداء (التنفيذات المتعددة) | ✅ التحليل مرة واحدة فقط | ❌ التحليل في كل مرة |
| سهولة قراءة الكود | واضحة (تستخدم المعلمات الرمز «?» كعناصر بديلة) | مربكة (علامات اقتباس متداخلة) |
| أمان الأنواع | ✅ الحفاظ على أنواع المعلمات | ❌ تحويل كل شيء إلى سلاسل |
| السيناريوهات القابلة للتطبيق | جميع مدخلات المستخدم | لغة SQL الثابتة فقط (أسماء الجداول/أسماء الأعمدة، وليس مدخلات المستخدم) |
6. المعاملات
▶ مثال: تحويل المعاملة
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func transferFunds(db *sql.DB, fromID, toID int, amount float64) error {
// Begin transaction
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
// Rollback on transaction failure
defer tx.Rollback() // If Commit succeeds, Rollback is a no-op
// 1. Deduct from fromID
result, err := tx.Exec(
"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
amount, fromID, amount,
)
if err != nil {
return fmt.Errorf("deduction failed: %w", err)
}
affected, _ := result.RowsAffected()
if affected == 0 {
return fmt.Errorf("insufficient balance or account does not exist")
}
// 2. Add to toID
result, err = tx.Exec(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, toID,
)
if err != nil {
return fmt.Errorf("credit failed: %w", err)
}
affected, _ = result.RowsAffected()
if affected == 0 {
return fmt.Errorf("recipient account does not exist")
}
// Commit transaction
return tx.Commit()
}
func main() {
db, err := sql.Open("sqlite3", "./bank.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Initialize accounts
db.Exec("CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY, balance REAL)")
db.Exec("INSERT OR IGNORE INTO accounts VALUES (1, 1000.00)")
db.Exec("INSERT OR IGNORE INTO accounts VALUES (2, 500.00)")
// Transfer 200 from account 1 to 2
err = transferFunds(db, 1, 2, 200)
if err != nil {
log.Printf("Transfer failed: %v\n", err)
} else {
fmt.Println("Transfer successful!")
}
}
sequenceDiagram
participant App as Application
participant DB as Database
App->>DB: BEGIN
DB-->>App: OK
App->>DB: UPDATE SET balance = balance - ? WHERE id = 1
DB-->>App: 1 row affected
App->>DB: UPDATE SET balance = balance + ? WHERE id = 2
DB-->>App: 1 row affected
App->>DB: COMMIT
DB-->>App: OK (persisted)
Note over App,DB: If error midway → ROLLBACK<br/>All changes reverted
defer tx.Rollback() في المعاملة نمطًا آمنًا — فإذا نجحت Commit، فإن استدعاء Rollback يكون آمنًا (عملية لا تؤدي إلى أي تغيير). أما إذا فشلت Commit، فإن defer تقوم تلقائيًا بإلغاء المعاملة. لا تحذف Rollback أبدًا!
7. تكوين مجموعة الاتصالات
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
func configurePool(db *sql.DB) {
// Maximum open connections (new requests queue when this limit is reached)
db.SetMaxOpenConns(25)
// Maximum idle connections (kept open but unused)
db.SetMaxIdleConns(10)
// Maximum connection lifetime (prevents long-running connections from being dropped by the database)
db.SetConnMaxLifetime(5 * time.Minute)
// Maximum idle connection lifetime
db.SetConnMaxIdleTime(2 * time.Minute)
}
func main() {
db, _ := sql.Open("mysql", "user:pass@/dbname")
configurePool(db)
fmt.Println("Connection pool configured")
}
(1) معلمات تجمع الاتصالات
| المعلمة | القيمة الافتراضية | القيمة الموصى بها | الوصف |
|---|---|---|---|
SetMaxOpenConns |
0 (غير محدود) | 25–100 | الحد الأقصى لعدد الاتصالات المتزامنة |
SetMaxIdleConns |
2 | 10–25 | الحد الأقصى لعدد الاتصالات الخاملة (≤ MaxOpenConns) |
SetConnMaxLifetime |
0 (لا تنتهي صلاحيتها أبدًا) | 5–30 دقيقة | أقصى مدة للاتصال |
SetConnMaxIdleTime |
0 (لا تنتهي صلاحيتها أبدًا) | 2–5 دقائق | مهلة انتظار الاتصال في حالة الخمول |
MaxIdleConns أكبر من MaxOpenConns — حيث ستقوم قاعدة البيانات/SQL بتعديل ذلك تلقائيًا. كما يجب عدم تعيين MaxOpenConns=0 (غير محدود) — ففي ظل التزامن العالي، سيؤدي ذلك إلى إنشاء عدد كبير من الاتصالات وإثقال كاهل قاعدة البيانات. لذا، يجب دائمًا تعيين حد أقصى معقول.
8. مثال كامل: إدارة المخزون في التجارة الإلكترونية
⚙️ المتطلب المسبق: تشغيل
go get github.com/mattn/go-sqlite3
// inventory.go
package main
import (
"database/sql"
"fmt"
"log"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
)
// ---------- Store ----------
type InventoryStore struct {
db *sql.DB
mu sync.RWMutex
}
func NewInventoryStore(dbPath string) (*InventoryStore, error) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Connection pool configuration
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
store := &InventoryStore{db: db}
if err := store.initSchema(); err != nil {
return nil, fmt.Errorf("failed to initialize schema: %w", err)
}
return store, nil
}
func (s *InventoryStore) initSchema() error {
schema := `
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
total REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id)
);`
_, err := s.db.Exec(schema)
return err
}
// ---------- Product Operations ----------
func (s *InventoryStore) AddProduct(name string, price float64, stock int) (int64, error) {
result, err := s.db.Exec(
"INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
name, price, stock,
)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func (s *InventoryStore) GetProduct(id int) (product, error) {
var p product
err := s.db.QueryRow(
"SELECT id, name, price, stock, created_at FROM products WHERE id = ?", id,
).Scan(&p.ID, &p.Name, &p.Price, &p.Stock, &p.CreatedAt)
if err == sql.ErrNoRows {
return p, fmt.Errorf("product not found")
}
return p, err
}
type product struct {
ID int
Name string
Price float64
Stock int
CreatedAt time.Time
}
// ---------- Order Operations (transaction) ----------
type OrderRequest struct {
ProductID int
Quantity int
}
func (s *InventoryStore) PlaceOrder(req OrderRequest) (int64, error) {
s.mu.Lock() // Prevent overselling: only one order deduction at a time
defer s.mu.Unlock()
tx, err := s.db.Begin()
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback() // Safe rollback
// 1. Query product and lock row (⚠️ SQLite does not support FOR UPDATE; use MySQL/PostgreSQL for true row locking)
var price float64
var stock int
err = tx.QueryRow(
"SELECT price, stock FROM products WHERE id = ? FOR UPDATE",
req.ProductID,
).Scan(&price, &stock)
if err == sql.ErrNoRows {
return 0, fmt.Errorf("product not found")
}
if err != nil {
return 0, err
}
// 2. Check stock
if stock < req.Quantity {
return 0, fmt.Errorf("insufficient stock: need %d, have %d", req.Quantity, stock)
}
// 3. Deduct stock
result, err := tx.Exec(
"UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?",
req.Quantity, req.ProductID, req.Quantity,
)
if err != nil {
return 0, err
}
affected, _ := result.RowsAffected()
if affected == 0 {
return 0, fmt.Errorf("concurrent stock shortage")
}
// 4. Create order
total := price * float64(req.Quantity)
result, err = tx.Exec(
"INSERT INTO orders (product_id, quantity, total) VALUES (?, ?, ?)",
req.ProductID, req.Quantity, total,
)
if err != nil {
return 0, err
}
orderID, _ := result.LastInsertId()
// 5. Commit
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("failed to commit transaction: %w", err)
}
return orderID, nil
}
// ---------- Report ----------
func (s *InventoryStore) LowStockReport(threshold int) ([]product, error) {
rows, err := s.db.Query(
"SELECT id, name, price, stock, created_at FROM products WHERE stock < ? ORDER BY stock ASC",
threshold,
)
if err != nil {
return nil, err
}
defer rows.Close()
var products []product
for rows.Next() {
var p product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock, &p.CreatedAt); err != nil {
return nil, err
}
products = append(products, p)
}
return products, rows.Err()
}
// ---------- Main ----------
func main() {
store, err := NewInventoryStore("./inventory.db")
if err != nil {
log.Fatal(err)
}
// Add products
laptopID, _ := store.AddProduct("Laptop", 999.99, 10)
mouseID, _ := store.AddProduct("Mouse", 29.99, 100)
fmt.Printf("Products added: laptop=%d, mouse=%d\n", laptopID, mouseID)
// Place order (transaction-safe)
orderID, err := store.PlaceOrder(OrderRequest{ProductID: int(laptopID), Quantity: 2})
if err != nil {
log.Printf("Order failed: %v\n", err)
} else {
fmt.Printf("Order placed successfully: order=%d\n", orderID)
}
// Low stock report
lowStock, _ := store.LowStockReport(20)
fmt.Printf("Low stock products (%d items):\n", len(lowStock))
for _, p := range lowStock {
fmt.Printf(" %s: stock=%d\n", p.Name, p.Stock)
}
}
FOR UPDATE قفل الصفوف — حيث يتم تجاهل هذه العبارة دون إشعار. لا يحقق الكود أعلاه قفلًا حقيقيًّا للصفوف إلا في MySQL/PostgreSQL؛ أما في SQLite، فتتم ضمان الأمان عبر mu.Lock() الموتكس. ولضمان التوافق بين قواعد البيانات المختلفة، استخدم التحديثات الشرطية (UPDATE ... WHERE stock >= ?) بدلاً من أقفال الصفوف.
❓ أسئلة شائعة
database/sql + أداة إنشاء الاستعلامات (مثل squirrel) للمشاريع الكبيرة.' OR 1=1 هي مجرد سلسلة نصية ولن تتحول إلى جملة WHERE.tx.Rollback() للتراجع عن جميع التغييرات في حالة حدوث خطأ. defer tx.Rollback() + tx.Commit() هو النمط القياسي — إذا نجحت Commit، فإن Rollback تكون عملية لا تؤدي إلى أي تغيير؛ وإذا فشلت Commit، يتم التراجع عن المعاملة تلقائيًا. وهذا يضمن الترابطية.MaxOpenConns على 2–3 أضعاف عدد الاتصالات المتزامنة، وMaxIdleConns على MaxOpenConns/2. قم بتعيين SetConnMaxLifetime على 5 دقائق لمنع إنهاء الاتصالات بواسطة البرمجيات الوسيطة لقاعدة البيانات.database/sql. الاختلافات: (1) العناصر النائبة: يستخدم SQLite ?، ويستخدم MySQL ?، ويستخدم PostgreSQL $1؛ (2) الاختلافات في صيغة SQL (مثل صيغة المفاتيح الأساسية ذات التزايد التلقائي)؛ (3) أداء التزامن في SQLite ليس بنفس جودة أداء MySQL.rows.Close() بعد الانتهاء من تكرار rows.Next()؟rows يحتفظ باتصال قاعدة البيانات. أما rows.Close() فيقوم بإعادة الاتصال إلى المجمع. استخدم defer rows.Close() لتسجيل ذلك فور إنشاء rows، لضمان تحرير الموارد في أي سيناريو.db.SetMaxOpenConns وdb.SetMaxIdleConns في طبقة قاعدة البيانات/SQL لضمان ألا يصبح تجمع الاتصالات عنق زجاجة؛ (2) استخدم سجل الاستعلامات البطيئة المدمج في قاعدة البيانات؛ (3) استخدم EXPLAIN ANALYZE لتحليل خطة الاستعلام؛ (4) ضع في اعتبارك إضافة برمجيات وسيطة لتسجيل SQL لتسجيل وقت تنفيذ جميع الاستعلامات.📖 ملخص
sql.Openيفتح اتصالاً (التحميل المؤجل) +db.Pingيتحقق من الاتصال- الأنواع الثلاثة من الاستعلامات:
db.Exec،db.Query، وdb.QueryRow Prepare: منع هجمات حقن SQL من خلال التجميع المسبق- المعاملة:
Begin→Commit/Rollback defer tx.Rollback()الوضع الآمن- مجموعة الاتصالات:
SetMaxOpenConns/SetMaxIdleConns/SetConnMaxLifetime - يستخدم SQLite
?، ويستخدم MySQL?، ويستخدم PostgreSQL$1
📝 تمارين
-
المشكلة الأساسية (الصعوبة ⭐): أنشئ قاعدة بيانات SQLite وأنشئ جدولًا باسم
tasks(id، title، done، created_at). قم بتنفيذ ثلاث دوال: InsertTask، ListTasks (مرتبة حسب التاريخ)، وMarkDone (تحديث حسب المعرّف). -
مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ طبقة قاعدة البيانات لنظام مدونة. المتطلبات: (1) جدول
posts(id،title،content،author_id،created_at)؛ (2) دعم الاستعلامات المقسمة إلى صفحات (LIMIT/OFFSET)؛ (3) استخدام العبارات المعدة مسبقًا لعمليات الإدراج لمنع هجمات الحقن؛ (4) المعاملات: تحديث عدد منشورات المؤلف عند نشر منشور؛ (5) استخدام-raceللتحقق من أمان التزامن. -
التحدي (الصعوبة: ⭐⭐⭐): تنفيذ عمليات آمنة من حيث التزامن لنظام إدارة المخزون. المتطلبات: (1) 100 goroutine تقوم بتقديم الطلبات في وقت واحد (كل منها تخصم من مخزون صنف مختلف)؛ (2) استخدم معاملات database/sql + mutexes على مستوى طبقة التطبيق لمنع البيع الزائد؛ (3) قم بتحويل جميع عبارات SQL مسبقًا؛ (4) قم بتكوين تجمع الاتصالات بشكل مناسب؛ (5) احسب عدد الطلبات الناجحة والفاشلة واحسب المخزون النهائي.