نشر المشروع وتحسينه

الدرس 28: نشر المشروع وتحسينه

تشبيه من 현실

تخترق أنك فتحت مطعمًا. الوصفات مكتوبة (الانتهاء من الكود)، لكن لبدء العمل فعليًا، تحتاج إلى:

المفاهيم الأساسية

المفهوم الوصف
الترجمة المتقاطعة تجميع ملفات تنفيذية لمنصة واحدة على منصة أخرى
البناء متعدد المراحل بناء Docker يفصل بين بيئة التجميع وبيئة التشغيل، يقلل حجم الصورة
متغيرات البيئة تكوين سلوك البرنامج دون تعديل الكود
التسجيل المنظم استخدام log/slog لإخراج تنسيقات سجلات قابلة للتحليل الآلي
تحليل الأداء استخدام pprof لتحديد اختناقات CPU والذاكرة
الإغلاق السلس بعد استقبال إشارة الإنهاء، ينتظر اكتمال الطلبات قيد التنفيذ قبل الخروج

بنية الجملة والاستخدام الأساسي

1. الترجمة المتقاطعة

تدعم Go الترجمة المتقاطعة بشكل أصلي، فقط قم بتعيين متغيرات البيئة GOOS وGOARCH:

BASH
# تجميع نسخة Linux amd64 (تعمل على Windows/Mac)
GOOS=linux GOARCH=amd64 go build -o myapp-linux .

# تجميع نسخة Windows (تعمل على Linux/Mac)
GOOS=windows GOARCH=amd64 go build -o myapp.exe .

# تجميع نسخة macOS ARM (M1/M2)
GOOS=darwin GOARCH=arm64 go build -o myapp-darwin .
💡 نصيحة: أثناء الترجمة المتقاطعة، CGO معطلة افتراضيًا. إذا كنت تعتمد على مكتبات C، تحتاج إلى تثبيت سلاسل ترجمة متقاطعة.

2. بناء Docker متعدد المراحل

DOCKERFILE
# ---- مرحلة البناء ----
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

# ---- مرحلة التشغيل ----
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
💡 نصيحة: الصورة النهائية تحتوي فقط على الملف الثنائي المُجمّع وبيئة تشغيل أدنى، مما يقلل الحجم من 1 جيجابايت إلى أقل من 20 ميجابايت.

3. تكوين متغيرات البيئة

GO
package main

import (
	"fmt"
	"os"
	"strconv"
)

func main() {
	// قراءة متغيرات البيئة مع قيم افتراضية
	port := getEnv("APP_PORT", "8080")
	dbHost := getEnv("DB_HOST", "localhost")
	debug := getEnv("DEBUG", "false")

	fmt.Printf("تكوين بدء التشغيل: المنفذ=%s، قاعدة البيانات=%s، التصحيح=%s\n", port, dbHost, debug)
}

// getEnv يحصل على متغير بيئة، يُرجع القيمة الافتراضية إذا لم يتم تعيينه
func getEnv(key, defaultVal string) string {
	if val := os.Getenv(key); val != "" {
		return val
	}
	return defaultVal
}
💡 نصيحة: للإنتاج، يُوصى باستخدام ملفات .env مع مكتبة godotenv، لكن لا تُدرج ملفات .env في التحكم بالإصدار.

4. التسجيل المنظم (log/slog)

قدمت Go 1.21 المكتبة القياسية log/slog، التي تدعم إخراج تنسيق JSON:

GO
package main

import (
	"log/slog"
	"os"
)

func main() {
	// إنشاء معالج تنسيق JSON
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	// استخدام التسجيل المنظم
	logger.Info("بدأت الخدمة",
		"port", 8080,
		"env", "production",
	)

	logger.Error("فشل الاتصال بقاعدة البيانات",
		"host", "db.example.com",
		"error", "connection refused",
	)
}
💡 نصيحة: يدعم slog تعيين مُسجّل افتراضي عالمي عبر slog.SetDefault()، والذي يمكن استخدامه بشكل موحد عبر المشروع بأكمله.

5. تحليل الأداء (pprof)

GO
package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof" // الاستيراد يُسجّل مسارات pprof
	"time"
)

func main() {
	// بدء خدمة pprof على منفذ منفصل
	go func() {
		fmt.Println("pprof يستمع على :6060")
		http.ListenAndServe(":6060", nil)
	}()

	// منطق عملك الرئيسي
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(10 * time.Millisecond)
		fmt.Fprintln(w, "مرحبًا بالعالم!")
	})

	http.ListenAndServe(":8080", nil)
}
💡 نصيحة: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 يمكنه جمع بيانات CPU لمدة 30 ثانية.

6. الإغلاق السلس

GO
package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(2 * time.Second) // محاكاة طلب طويل
		fmt.Fprintln(w, "اكتملت المعالجة")
	})

	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	// بدء الخادم في goroutine
	go func() {
		fmt.Println("الخادم يستمع على :8080")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			fmt.Printf("خرج الخادم بشكل غير طبيعي: %v\n", err)
		}
	}()

	// انتظار إشارة المقاطعة
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	fmt.Println("\nتم استقبال إشارة الإغلاق، جاري الإغلاق السلس...")

	// إعطاء الطلبات قيد التنفيذ حتى 10 ثوانٍ للإكمال
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		fmt.Printf("إغلاق قسري: %v\n", err)
	} else {
		fmt.Println("أُغلق الخادم بشكل سلس")
	}
}
💡 نصيحة: server.Shutdown() يتوقف عن قبول اتصالات جديدة وينتظر حتى تنتهي الاتصالات الحالية من المعالجة قبل الإرجاع.


مثال: سكربت ترجمة متقاطعة أساسي (الصعوبة ⭐)

أنشئ سكربتًا يُجمع ملفات ثنائية لعدة منصات دفعة واحدة:

GO
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// قائمة المنصات المستهدفة
type Target struct {
	OS   string
	Arch string
}

func main() {
	targets := []Target{
		{"linux", "amd64"},
		{"linux", "arm64"},
		{"darwin", "amd64"},
		{"darwin", "arm64"},
		{"windows", "amd64"},
	}

	outputDir := "dist"
	os.MkdirAll(outputDir, 0755)

	for _, t := range targets {
		outputName := fmt.Sprintf("myapp-%s-%s", t.OS, t.Arch)
		if t.OS == "windows" {
			outputName += ".exe"
		}

		outputPath := filepath.Join(outputDir, outputName)
		fmt.Printf("جاري التجميع: %s/%s -> %s\n", t.OS, t.Arch, outputPath)

		cmd := exec.Command("go", "build", "-o", outputPath, ".")
		cmd.Env = append(os.Environ(),
			"GOOS="+t.OS,
			"GOARCH="+t.Arch,
			"CGO_ENABLED=0",
		)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr

		if err := cmd.Run(); err != nil {
			fmt.Printf("فشل التجميع %s/%s: %v\n", t.OS, t.Arch, err)
		}
	}

	fmt.Println("اكتملت جميع عمليات التجميع!")
}
▶ جرّب الكود

التشغيل:

BASH
go run build.go

هيكل دليل الإخراج:

TEXT
dist/
├── myapp-darwin-amd64
├── myapp-darwin-arm64
├── myapp-linux-amd64
├── myapp-linux-arm64
└── myapp-windows-amd64.exe

مثال: خدمة HTTP مع تكوين متغيرات البيئة (الصعوبة ⭐⭐)

يُظهر مخطط تكوين متغيرات البيئة الكامل:

GO
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"
)

// Config هيكل تكوين التطبيق
type Config struct {
	Port        int    `json:"port"`
	Env         string `json:"env"`
	LogLevel    string `json:"log_level"`
	DBHost      string `json:"db_host"`
	DBPort      int    `json:"db_port"`
	IdleTimeout int    `json:"idle_timeout"` // ثوانٍ
}

// LoadConfig يحمّل التكوين من متغيرات البيئة
func LoadConfig() Config {
	return Config{
		Port:        getEnvInt("APP_PORT", 8080),
		Env:         getEnvStr("APP_ENV", "development"),
		LogLevel:    getEnvStr("LOG_LEVEL", "info"),
		DBHost:      getEnvStr("DB_HOST", "localhost"),
		DBPort:      getEnvInt("DB_PORT", 5432),
		IdleTimeout: getEnvInt("IDLE_TIMEOUT", 30),
	}
}

func getEnvStr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func getEnvInt(key string, fallback int) int {
	if v := os.Getenv(key); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			return n
		}
	}
	return fallback
}

func (c Config) String() string {
	data, _ := json.MarshalIndent(c, "", "  ")
	return string(data)
}

func main() {
	cfg := LoadConfig()

	// تكوين مستوى السجل بناءً على البيئة
	var level slog.Level
	switch cfg.LogLevel {
	case "debug":
		level = slog.LevelDebug
	case "warn":
		level = slog.LevelWarn
	case "error":
		level = slog.LevelError
	default:
		level = slog.LevelInfo
	}

	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: level,
	}))
	slog.SetDefault(logger)

	logger.Info("تم تحميل تكوين التطبيق", "config", cfg.String())

	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"status": "ok",
			"env":    cfg.Env,
		})
	})

	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		logger.Info("تم استقبال الطلب", "method", r.Method, "path", r.URL.Path)
		fmt.Fprintf(w, "مرحبًا من بيئة %s!", cfg.Env)
	})

	server := &http.Server{
		Addr:        fmt.Sprintf(":%d", cfg.Port),
		Handler:     mux,
		IdleTimeout: time.Duration(cfg.IdleTimeout) * time.Second,
	}

	go func() {
		logger.Info("بدأت الخدمة", "port", cfg.Port, "env", cfg.Env)
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			logger.Error("خطأ في الخدمة", "error", err)
			os.Exit(1)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	logger.Info("جاري إيقاف الخدمة...")
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	server.Shutdown(ctx)
	logger.Info("أُوقفت الخدمة")
}
▶ جرّب الكود

البدء:

BASH
# استخدام التكوين الافتراضي
go run main.go

# تكوين مخصص
APP_PORT=3000 APP_ENV=production LOG_LEVEL=debug go run main.go

اختبار الطلبات:

BASH
# فحص الصحة
curl http://localhost:8080/health

# الصفحة الرئيسية
curl http://localhost:8080/

مثال: حل نشر Docker كامل (الصعوبة ⭐⭐⭐)

حل نشر كامل يتضمن Dockerfile وdocker-compose وMakefile:

هيكل المشروع:

TEXT
myproject/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── Makefile
└── .env.example
▶ جرّب الكود

main.go:

GO
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	_ "net/http/pprof"
	"os"
	"os/signal"
	"runtime"
	"syscall"
	"time"
)

type AppConfig struct {
	Port      string
	Env       string
	LogLevel  string
	PprofPort string
}

func loadConfig() AppConfig {
	return AppConfig{
		Port:      getEnv("APP_PORT", "8080"),
		Env:       getEnv("APP_ENV", "production"),
		LogLevel:  getEnv("LOG_LEVEL", "info"),
		PprofPort: getEnv("PPROF_PORT", "6060"),
	}
}

func getEnv(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

func setupLogger(level string) *slog.Logger {
	var lvl slog.Level
	switch level {
	case "debug":
		lvl = slog.LevelDebug
	case "warn":
		lvl = slog.LevelWarn
	case "error":
		lvl = slog.LevelError
	default:
		lvl = slog.LevelInfo
	}
	return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}))
}

func main() {
	cfg := loadConfig()
	logger := setupLogger(cfg.LogLevel)
	slog.SetDefault(logger)

	// بدء pprof (فقط في غير الإنتاج أو عند التمكين الصريح)
	if cfg.Env != "production" || os.Getenv("ENABLE_PPROF") == "true" {
		go func() {
			logger.Info("بدأت خدمة pprof", "port", cfg.PprofPort)
			http.ListenAndServe(":"+cfg.PprofPort, nil)
		}()
	}

	mux := http.NewServeMux()

	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{
			"status":    "ok",
			"env":       cfg.Env,
			"goroutine": runtime.NumGoroutine(),
			"uptime":    time.Since(startTime).String(),
		})
	})

	mux.HandleFunc("GET /api/info", func(w http.ResponseWriter, r *http.Request) {
		logger.Info("طلب معلومات", "remote", r.RemoteAddr)
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"app":      "myproject",
			"version":  version,
			"go":       runtime.Version(),
			"env":      cfg.Env,
		})
	})

	mux.HandleFunc("POST /api/data", func(w http.ResponseWriter, r *http.Request) {
		// محاكاة معالجة البيانات
		time.Sleep(100 * time.Millisecond)
		logger.Info("اكتملت معالجة البيانات", "content_length", r.ContentLength)
		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(map[string]string{"result": "created"})
	})

	server := &http.Server{
		Addr:         ":" + cfg.Port,
		Handler:      mux,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	// منطق الإغلاق السلس
	done := make(chan struct{})
	go func() {
		quit := make(chan os.Signal, 1)
		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
		sig := <-quit
		logger.Info("تم استقبال إشارة الإغلاق", "signal", sig)

		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		// إيقاف pprof
		// ملاحظة: pprof في المكتبة القياسية ليس له Shutdown، يعتمد على خروج العملية

		if err := server.Shutdown(ctx); err != nil {
			logger.Error("فشل إغلاق الخادم", "error", err)
		}
		close(done)
	}()

	logger.Info("بدأت الخدمة",
		"port", cfg.Port,
		"env", cfg.Env,
		"version", version,
	)

	if err := server.ListenAndServe(); err != http.ErrServerClosed {
		logger.Error("خرج الخادم بشكل غير طبيعي", "error", err)
		os.Exit(1)
	}

	<-done
	logger.Info("أُغلقت الخدمة بالكامل")
}

var (
	startTime = time.Now()
	version   = "1.0.0"
)

Dockerfile:

DOCKERFILE
# ========== مرحلة البناء ==========
FROM golang:1.22-alpine AS builder

# تثبيت git (قد يحتاجه go mod)
RUN apk add --no-cache git

WORKDIR /build

# نسخ ملفات التبعية أولاً، الاستفادة من طبقات ذاكرة التخزين المؤقت Docker
COPY go.mod go.sum ./
RUN go mod download

# نسخ المصدر والتجميع
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o /app/server .

# ========== مرحلة التشغيل ==========
FROM alpine:3.19

# تثبيت شهادات CA (لطلبات HTTPS) وبيانات المنطقة الزمنية
RUN apk --no-cache add ca-certificates tzdata

# إنشاء مستخدم غير جذري
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# نسخ الملف الثنائي من مرحلة البناء
COPY --from=builder /app/server .
COPY --from=builder /build/.env.example .env.example

# التشغيل كمستخدم غير جذري
USER appuser

EXPOSE 8080

# فحص الصحة
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget -qO- http://localhost:8080/health || exit 1

CMD ["./server"]

docker-compose.yml:

YAML
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_ENV=production
      - LOG_LEVEL=info
      - APP_PORT=8080
      - ENABLE_PPROF=false
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.5'
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Makefile:

MAKEFILE
APP_NAME := myproject
VERSION  := 1.0.0

.PHONY: build run clean docker docker-run docker-stop

# البناء المحلي
build:
	CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME) .

# التشغيل المحلي
run: build
	./bin/$(APP_NAME)

# التنظيف
clean:
	rm -rf bin/

# بناء صورة Docker
docker:
	docker build -t $(APP_NAME):$(VERSION) .

# تشغيل حاوية Docker
docker-run:
	docker-compose up -d

# إيقاف حاوية Docker
docker-stop:
	docker-compose down

# ترجمة متقاطعة لجميع المنصات
cross-build:
	GOOS=linux   GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-linux-amd64 .
	GOOS=darwin  GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-darwin-amd64 .
	GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-windows-amd64.exe .

.env.example:

TEXT
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
ENABLE_PPROF=false
PPROF_PORT=6060

سير عمل البناء والنشر:

BASH
# 1. اختبار محلي
make run

# 2. بناء صورة Docker
make docker

# 3. تشغيل الحاوية
make docker-run

# 4. اختبار الخدمة
curl http://localhost:8080/health
curl http://localhost:8080/api/info

# 5. عرض السجلات
docker-compose logs -f

# 6. إيقاف الخدمة
make docker-stop

السيناريو 1: التسجيل والمراقبة في الإنتاج

في خدمة ويب حقيقية، يجب جمع السجلات في نظام تسجيل مركزي:

GO
package main

import (
	"context"
	"log/slog"
	"net/http"
	"os"
	"time"
)

// RequestLogger وسائط: تسجيل معلومات مفصلة لكل طلب
func RequestLogger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// التفاف ResponseWriter لالتقاط رمز الحالة
		rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
		next.ServeHTTP(rw, r)

		slog.Info("طلب HTTP",
			"method", r.Method,
			"path", r.URL.Path,
			"status", rw.statusCode,
			"duration_ms", time.Since(start).Milliseconds(),
			"remote_addr", r.RemoteAddr,
			"user_agent", r.UserAgent(),
		)
	})
}

type responseWriter struct {
	http.ResponseWriter
	statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
	rw.statusCode = code
	rw.ResponseWriter.WriteHeader(code)
}

// RecoveryMiddleware يلتقط عمليات الذعر ويسجلها
func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				slog.Error("ذعر في معالجة الطلب",
					"error", err,
					"path", r.URL.Path,
					"method", r.Method,
				)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	// الإنتاج يستخدم تنسيق JSON لسهولة تحليل نظام جمع السجلات
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))
	slog.SetDefault(logger)

	mux := http.NewServeMux()
	mux.HandleFunc("GET /api/users", func(w http.ResponseWriter, r *http.Request) {
		// محاكاة منطق الأعمال
		ctx := r.Context()
		slog.DebugContext(ctx, "جاري استعلام قائمة المستخدمين", "page", 1)
		// ...
	})

	// سلسلة الوسائط: الاستعادة -> المسجّل -> الموجه
	handler := RecoveryMiddleware(RequestLogger(mux))

	server := &http.Server{Addr: ":8080", Handler: handler}

	slog.Info("بدأت الخدمة", "addr", server.Addr)
	server.ListenAndServe()
}

بعد التشغيل، يُخرج كل طلب سجلات منظمة:

JSON
{"time":"2024-01-15T10:30:00Z","level":"INFO","msg":"طلب HTTP","method":"GET","path":"/api/users","status":200,"duration_ms":5,"remote_addr":"127.0.0.1","user_agent":"curl/7.68.0"}

السيناريو 2: استخدام pprof لتحديد اختناقات الأداء

عندما تبطئ استجابة الخدمة، استخدم pprof للتشخيص:

GO
package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"strings"
	"time"
)

func main() {
	// الخدمة الرئيسية
	mux := http.NewServeMux()

	// محاكاة نقطة نهاية بها مشكلة أداء
	mux.HandleFunc("GET /api/search", func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query().Get("q")
		result := slowSearch(query) // دالة بطيئة عن قصد
		fmt.Fprint(w, result)
	})

	// pprof يُسجّل على http.DefaultServeMux الافتراضي، يحتاج منفذًا منفصلًا
	go func() {
		fmt.Println("pprof: http://localhost:6060/debug/pprof/")
		http.ListenAndServe(":6060", nil)
	}()

	fmt.Println("الخدمة الرئيسية: http://localhost:8080")
	http.ListenAndServe(":8080", mux)
}

// slowSearch تُحاكي دالة كثيفة الاستخدام لـ CPU مع مشكلة تخصيص ذاكرة
func slowSearch(query string) string {
	var results []string
	// تخصيص ذاكرة متكرر عن قصد في حلقة
	for i := 0; i < 100000; i++ {
		data := fmt.Sprintf("item_%d_%s", i, query) // تخصيص سلسلة جديدة لكل تكرار
		if strings.Contains(data, query) {
			results = append(results, data)
		}
	}
	// محاكاة تأخير إضافي
	time.Sleep(50 * time.Millisecond)
	return fmt.Sprintf("تم العثور على %d نتيجة", len(results))
}

استخدام pprof للتحليل:

BASH
# 1. تثبيت أداة pprof (إذا لم تكن مثبتة)
go install github.com/google/pprof@latest

# 2. جمع بيانات CPU لمدة 30 ثانية
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# 3. عرض في واجهة pprof التفاعلية
(pprof) top          # عرض الدوال الأكثر استهلاكًا لـ CPU
(pprof) list slowSearch  # عرض تكاليف أسطر الكود المحددة
(pprof) web          # إنشاء تصور مرئي (يتطلب graphviz)

# 4. عرض تخصيص الذاكرة
go tool pprof http://localhost:6060/debug/pprof/heap

# 5. عرض عدد goroutine (كشف التسريبات)
go tool pprof http://localhost:6060/debug/pprof/goroutine

# 6. استخدام المتصفح لعرض جميع المقاييس
# افتح http://localhost:6060/debug/pprof/

❓ أسئلة شائعة

س1: ماذا أفعل عند rencontrer cgo: exec gcc: exec: "gcc": not found أثناء الترجمة المتقاطعة؟

هذا لأن الكود يستخدم CGO. إذا كنت لا تحتاج إلى تبعيات مكتبة C، قم بتعيين CGO_ENABLED=0:

BASH
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp .

إذا كنت تحتاج فعليًا إلى CGO، تحتاج إلى تثبيت سلاسل ترجمة متقاطعة للمنصة المستهدفة (مثل gcc-aarch64-linux-gnu).

س2: صورة Docker كبيرة جدًا، كيف نقلل الحجم بشكل أكبر؟

استخدم scratch كصورة أساسية (بدون أدوات نظام على الإطلاق):

DOCKERFILE
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
CMD ["/server"]

مع إزالة معلومات التصحيح أثناء التجميع: go build -ldflags="-s -w". -s يزيل جدول الرموز، -w يزيل معلومات تصحيح DWARF، عادةً يقلل الحجم بنسبة 20-30%.

س3: كيف يقارن log/slog بمكتبات تسجيل الطرف الثالث (مثل zap وzerolog)؟

ميزة log/slog هي كونها مكتبة قياسية بدون تبعيات إضافية، وأدائها كافٍ لمعظم السيناريوهات. إذا كان تطبيقك يحتوي على حجم سجلات ضخم جدًا (عشرات الآلاف في الثانية)، سيكون تصميم zap الذي لا يُخصص ذاكرة أسرع. للمشاريع الجديدة، ابدأ بـ slog؛ فكّر في التبديل فقط عند rencontrer اختناقات في الأداء.

س4: أثناء الإغلاق السلس، كيف نضمن التعامل الصحيح مع اتصالات WebSocket؟

http.Server.Shutdown() يتعامل فقط مع طلبات HTTP. للاتصالات طويلة العمر (WebSocket)، تحتاج إلى إدارة تجمع الاتصالات بنفسك:

GO
var connections sync.Map // تخزين الاتصالات النشطة

// التسجيل عند وصول اتصال جديد
connections.Store(conn, true)

// عند الإغلاق، تكرار جميع الاتصالات وإرسال إطارات إغلاق
connections.Range(func(key, value any) bool {
    wsConn := key.(*websocket.Conn)
    wsConn.WriteMessage(websocket.CloseMessage, closeMsg)
    wsConn.Close()
    return true
})

📖 ملخص

في هذا الدرس تعلمنا سير العمل الكامل من التطوير إلى النشر لمشاريع Go:

بعد إتقان هذه المهارات، يمكنك نشر مشاريع Go في بيئات الإنتاج بأمان وكفاءة.


📝 تمارين

التمرين 1: كتابة سكربت ترجمة متقاطعة

اكتب برنامج Go يُجمع تلقائيًا المشروع الحالي لمنصات linux/amd64 وdarwin/arm64 وwindows/amd64، مع أسماء ملفات الإخراج تحتوي على معلومات الإصدار والمنصة.

التمرين 2: إضافة وسائط تسجيل الطلبات

أضف وسائط تسجيل إلى خدمة HTTP تسجل لكل طلب: الأسلوب، المسار، رمز الحالة، وقت الاستجابة، عنوان IP العميل. استخدم log/slog لإخراج تنسيق JSON.

التمرين 3: نشر مشروعك على Docker

اكتب Dockerfile وdocker-compose.yml كاملين لأي مشروع كتبتَه في الدروس السابقة (مثل Todo API)، بالمتطلبات التالية:


الدرس التالي

بعد إكمال هذا الدرس، أتقنتَ المهارات الأساسية لنشر مشاريع Go. سننتقل الآن إلى مرحلة المشاريع العملية: الدرس 29: المشروع العملي (الجزء 1)

Web-Tutorial.com

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

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

100%