Go: عمليات قواعد البيانات في Go

توفر حزمة database/sql في لغة Go واجهة موحدة لعمليات قواعد البيانات التي تعمل بلغة SQL — حيث يمكنك التبديل بين MySQL وSQLite وPostgreSQL بمجرد تغيير برنامج التشغيل.

عندما تحتاج إلى التعامل مع قواعد البيانات بأمان — لمنع هجمات حقن SQL، وإدارة المعاملات، وتكوين مجموعات الاتصالات — توفر المكتبة القياسية database/sql مجموعة كاملة وآمنة من الأدوات.

1. ستتعلم



2. قصة حقيقية لمهندس برمجيات الخلفية

(1) المشكلة: أدى ربط السلاسل في لغة SQL إلى حذف قاعدة البيانات عبر هجوم حقن نفذه أحد المتسللين

بوب هو مهندس برمجيات الخلفية في منصة للتجارة الإلكترونية. وهو بحاجة إلى تنفيذ واجهة برمجة تطبيقات (API) للبحث عن المنتجات:

"استخدمت fmt.Sprintf لإنشاء استعلام SQL للمنتجات: SELECT * FROM products WHERE name LIKE '%" + search + "%'. في اليوم الثاني بعد الإطلاق، أدخل أحدهم ' OR 1=1; DROP TABLE products;-- في مربع البحث. فاختفى جدول المنتجات الخاص بي. فسألني مديري: 'أين ذهبت الـ 100,000 منتج الموجودة في قاعدة البيانات؟'"

GO
// 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 المُجمَّع مسبقًا

GO
// 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 فيتضمنانه بشكل افتراضي

GO
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

GO
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

▶ مثال: إنشاء جدول وإدخال البيانات

GO
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)
}
▶ جرّب الكود

▶ مثال: الاستعلام عن البيانات

GO 📖 للعرض فقط
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)
    }
}
46 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

(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

▶ مثال: الإدراج الدفعي المُعد مسبقًا

GO
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. المعاملات

▶ مثال: تحويل المعاملة

GO 📖 للعرض فقط
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!")
    }
}
53 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
100%
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. تكوين مجموعة الاتصالات

GO
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

GO
// 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)
    }
}
⚠️ ملاحظة: لا يدعم SQLite FOR UPDATE قفل الصفوف — حيث يتم تجاهل هذه العبارة دون إشعار. لا يحقق الكود أعلاه قفلًا حقيقيًّا للصفوف إلا في MySQL/PostgreSQL؛ أما في SQLite، فتتم ضمان الأمان عبر mu.Lock() الموتكس. ولضمان التوافق بين قواعد البيانات المختلفة، استخدم التحديثات الشرطية (UPDATE ... WHERE stock >= ?) بدلاً من أقفال الصفوف.



❓ أسئلة شائعة

س كيف أختار بين استخدام قاعدة البيانات/SQL و ORM (مثل GORM)؟
ج يُعد GORM أسرع في عمليات CRUD البسيطة. استخدم database/sql عندما تحتاج إلى تحكم دقيق في SQL، أو تزامن عالٍ، أو استعلامات معقدة. التوصية: استخدم GORM للمشاريع الصغيرة وdatabase/sql + أداة إنشاء الاستعلامات (مثل squirrel) للمشاريع الكبيرة.
س لماذا يمنع التجميع المسبق هجمات حقن SQL؟
ج يعمل التجميع المسبق على فصل بنية SQL عن المعلمات أثناء الإرسال. تقوم قاعدة البيانات أولاً بتجميع قالب SQL (لتحديد البنية النحوية)، ثم تربط المعلمات كبيانات أولية. ولا يتم تحليل المعلمات أبدًا على أنها بناء جملة SQL — لذا فإن ' OR 1=1 هي مجرد سلسلة نصية ولن تتحول إلى جملة WHERE.
س كيف يتم ضمان التوافق مع معايير ACID للمعاملات في لغة Go؟
ج استخدم tx.Rollback() للتراجع عن جميع التغييرات في حالة حدوث خطأ. defer tx.Rollback() + tx.Commit() هو النمط القياسي — إذا نجحت Commit، فإن Rollback تكون عملية لا تؤدي إلى أي تغيير؛ وإذا فشلت Commit، يتم التراجع عن المعاملة تلقائيًا. وهذا يضمن الترابطية.
س كيف يمكنني تكوين معلمات تجمع الاتصالات؟
ج المعلمات الرئيسية هي MaxOpenConns (الحد الأقصى لعدد الاتصالات المتزامنة) وMaxIdleConns (الحد الأقصى لعدد الاتصالات الخاملة). نوصي بتعيين MaxOpenConns على 2–3 أضعاف عدد الاتصالات المتزامنة، وMaxIdleConns على MaxOpenConns/2. قم بتعيين SetConnMaxLifetime على 5 دقائق لمنع إنهاء الاتصالات بواسطة البرمجيات الوسيطة لقاعدة البيانات.
س هل يتم استخدام SQLite وMySQL بنفس الطريقة في لغة Go؟
ج بشكل أساسي، نعم — حيث يتم تشغيل كليهما من خلال واجهة database/sql. الاختلافات: (1) العناصر النائبة: يستخدم SQLite ?، ويستخدم MySQL ?، ويستخدم PostgreSQL $1؛ (2) الاختلافات في صيغة SQL (مثل صيغة المفاتيح الأساسية ذات التزايد التلقائي)؛ (3) أداء التزامن في SQLite ليس بنفس جودة أداء MySQL.
س هل ما زلت بحاجة إلى استدعاء rows.Close() بعد الانتهاء من تكرار rows.Next()؟
ج نعم. حتى بعد الانتهاء من تكرار جميع الصفوف، لا يزال rows يحتفظ باتصال قاعدة البيانات. أما rows.Close() فيقوم بإعادة الاتصال إلى المجمع. استخدم defer rows.Close() لتسجيل ذلك فور إنشاء rows، لضمان تحرير الموارد في أي سيناريو.
س كيف يمكنني حل مشكلة بطء استعلامات SQL؟
ج (1) قم بتعيين db.SetMaxOpenConns وdb.SetMaxIdleConns في طبقة قاعدة البيانات/SQL لضمان ألا يصبح تجمع الاتصالات عنق زجاجة؛ (2) استخدم سجل الاستعلامات البطيئة المدمج في قاعدة البيانات؛ (3) استخدم EXPLAIN ANALYZE لتحليل خطة الاستعلام؛ (4) ضع في اعتبارك إضافة برمجيات وسيطة لتسجيل SQL لتسجيل وقت تنفيذ جميع الاستعلامات.

📖 ملخص


📝 تمارين

  1. المشكلة الأساسية (الصعوبة ⭐): أنشئ قاعدة بيانات SQLite وأنشئ جدولًا باسم tasks (id، title، done، created_at). قم بتنفيذ ثلاث دوال: InsertTask، ListTasks (مرتبة حسب التاريخ)، وMarkDone (تحديث حسب المعرّف).

  2. مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ طبقة قاعدة البيانات لنظام مدونة. المتطلبات: (1) جدول posts (id، title، content، author_id، created_at)؛ (2) دعم الاستعلامات المقسمة إلى صفحات (LIMIT/OFFSET)؛ (3) استخدام العبارات المعدة مسبقًا لعمليات الإدراج لمنع هجمات الحقن؛ (4) المعاملات: تحديث عدد منشورات المؤلف عند نشر منشور؛ (5) استخدام -race للتحقق من أمان التزامن.

  3. التحدي (الصعوبة: ⭐⭐⭐): تنفيذ عمليات آمنة من حيث التزامن لنظام إدارة المخزون. المتطلبات: (1) 100 goroutine تقوم بتقديم الطلبات في وقت واحد (كل منها تخصم من مخزون صنف مختلف)؛ (2) استخدم معاملات database/sql + mutexes على مستوى طبقة التطبيق لمنع البيع الزائد؛ (3) قم بتحويل جميع عبارات SQL مسبقًا؛ (4) قم بتكوين تجمع الاتصالات بشكل مناسب؛ (5) احسب عدد الطلبات الناجحة والفاشلة واحسب المخزون النهائي.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%