نشر المشروع وتحسينه
الدرس 28: نشر المشروع وتحسينه
تشبيه من 현실
تخترق أنك فتحت مطعمًا. الوصفات مكتوبة (الانتهاء من الكود)، لكن لبدء العمل فعليًا، تحتاج إلى:
- الموقع والتجديد — الترجمة المتقاطعة تجعل نفس الوصفة تعمل في مدن مختلفة (التشغيل على منصات مختلفة)
- تصميم المطبخ — بناء Docker متعدد المراحل مثل أدوات الطبخ فقط في المطبخ، وليس أثاثًا إضافيًا
- دليل الموظف — تكوين متغيرات البيئة مثل فروع مختلفة تستخدم ساعات عمل مختلفة، لكن تنسيق الدليل هو نفسه
- كاميرات الأمان — إدارة السجلات تسجل كل عملية إعداد طبق، قابلة للتتبع عند ظهور المشاكل
- تقرير الفحص الصحي — تحليل الأداء مثل الفحص الدوري، يجد أي جزء بطيء
- إجراء الإغلاق — الإغلاق السلس مثل عدم طرد العملاء الذين لا يزالون يأكلون وقت الإغلاق، ينتظرهم حتى ينتهيون قبل الإغلاق
المفاهيم الأساسية
| المفهوم | الوصف |
|---|---|
| الترجمة المتقاطعة | تجميع ملفات تنفيذية لمنصة واحدة على منصة أخرى |
| البناء متعدد المراحل | بناء Docker يفصل بين بيئة التجميع وبيئة التشغيل، يقلل حجم الصورة |
| متغيرات البيئة | تكوين سلوك البرنامج دون تعديل الكود |
| التسجيل المنظم | استخدام log/slog لإخراج تنسيقات سجلات قابلة للتحليل الآلي |
| تحليل الأداء | استخدام pprof لتحديد اختناقات CPU والذاكرة |
| الإغلاق السلس | بعد استقبال إشارة الإنهاء، ينتظر اكتمال الطلبات قيد التنفيذ قبل الخروج |
بنية الجملة والاستخدام الأساسي
1. الترجمة المتقاطعة
تدعم Go الترجمة المتقاطعة بشكل أصلي، فقط قم بتعيين متغيرات البيئة GOOS وGOARCH:
# تجميع نسخة 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 متعدد المراحل
# ---- مرحلة البناء ----
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"]
3. تكوين متغيرات البيئة
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:
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)
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. الإغلاق السلس
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() يتوقف عن قبول اتصالات جديدة وينتظر حتى تنتهي الاتصالات الحالية من المعالجة قبل الإرجاع.
مثال: سكربت ترجمة متقاطعة أساسي (الصعوبة ⭐)
أنشئ سكربتًا يُجمع ملفات ثنائية لعدة منصات دفعة واحدة:
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("اكتملت جميع عمليات التجميع!")
}
التشغيل:
go run build.go
هيكل دليل الإخراج:
dist/
├── myapp-darwin-amd64
├── myapp-darwin-arm64
├── myapp-linux-amd64
├── myapp-linux-arm64
└── myapp-windows-amd64.exe
مثال: خدمة HTTP مع تكوين متغيرات البيئة (الصعوبة ⭐⭐)
يُظهر مخطط تكوين متغيرات البيئة الكامل:
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("أُوقفت الخدمة")
}
البدء:
# استخدام التكوين الافتراضي
go run main.go
# تكوين مخصص
APP_PORT=3000 APP_ENV=production LOG_LEVEL=debug go run main.go
اختبار الطلبات:
# فحص الصحة
curl http://localhost:8080/health
# الصفحة الرئيسية
curl http://localhost:8080/
مثال: حل نشر Docker كامل (الصعوبة ⭐⭐⭐)
حل نشر كامل يتضمن Dockerfile وdocker-compose وMakefile:
هيكل المشروع:
myproject/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── Makefile
└── .env.example
main.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:
# ========== مرحلة البناء ==========
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:
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:
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:
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
ENABLE_PPROF=false
PPROF_PORT=6060
سير عمل البناء والنشر:
# 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: التسجيل والمراقبة في الإنتاج
في خدمة ويب حقيقية، يجب جمع السجلات في نظام تسجيل مركزي:
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()
}
بعد التشغيل، يُخرج كل طلب سجلات منظمة:
{"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 للتشخيص:
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 للتحليل:
# 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:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp .
إذا كنت تحتاج فعليًا إلى CGO، تحتاج إلى تثبيت سلاسل ترجمة متقاطعة للمنصة المستهدفة (مثل gcc-aarch64-linux-gnu).
س2: صورة Docker كبيرة جدًا، كيف نقلل الحجم بشكل أكبر؟
استخدم scratch كصورة أساسية (بدون أدوات نظام على الإطلاق):
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)، تحتاج إلى إدارة تجمع الاتصالات بنفسك:
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 ذلك بشكل أصلي — فقط قم بتعيين
GOOSوGOARCH - بناء Docker متعدد المراحل يفصل بيئة التجميع عن بيئة التشغيل، مما يقلل بشكل كبير حجم الصورة مع تحسين الأمان
- تكوين متغيرات البيئة هو أفضل ممارسات تطبيق العوامل الـ12، يتيح لك نفس الكود التكيف مع بيئات مختلفة
log/slogيوفر تسجيلًا منظمًا على مستوى المكتبة القياسية، بتنسيق JSON لسهولة تحليل نظام جمع السجلاتpprofهو أداة تحليل الأداء المدمجة في Go، القادرة على تحديد مشاكل CPU والذاكرة وgoroutine وغيرها- الإغلاق السلس يضمن أن بعد استقبال إشارة الإنهاء، تنتظر الخدمة اكتمال الطلبات قيد التنفيذ قبل الخروج، متجنبة فقدان البيانات
بعد إتقان هذه المهارات، يمكنك نشر مشاريع Go في بيئات الإنتاج بأمان وكفاءة.
📝 تمارين
التمرين 1: كتابة سكربت ترجمة متقاطعة
اكتب برنامج Go يُجمع تلقائيًا المشروع الحالي لمنصات linux/amd64 وdarwin/arm64 وwindows/amd64، مع أسماء ملفات الإخراج تحتوي على معلومات الإصدار والمنصة.
التمرين 2: إضافة وسائط تسجيل الطلبات
أضف وسائط تسجيل إلى خدمة HTTP تسجل لكل طلب: الأسلوب، المسار، رمز الحالة، وقت الاستجابة، عنوان IP العميل. استخدم log/slog لإخراج تنسيق JSON.
التمرين 3: نشر مشروعك على Docker
اكتب Dockerfile وdocker-compose.yml كاملين لأي مشروع كتبتَه في الدروس السابقة (مثل Todo API)، بالمتطلبات التالية:
- استخدام بناء متعدد المراحل
- الصورة النهائية مبنية على
alpineأوscratch - تتضمن فحوصات صحة
- التشغيل كمستخدم غير جذري
الدرس التالي
بعد إكمال هذا الدرس، أتقنتَ المهارات الأساسية لنشر مشاريع Go. سننتقل الآن إلى مرحلة المشاريع العملية: الدرس 29: المشروع العملي (الجزء 1)



