Pointers vs. Values في Go: كيف لا تقتل الأداء

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

Pointers vs. Values في Go: الحد الفاصل بين الأداء والأمان

\n\n

تخيل موقفًا: أنت تكتب بلغة Go، كل شيء يسير بسلاسة، لكن فجأة تلاحظ أن التطبيق يبدأ في "التباطؤ" عند معالجة كميات كبيرة من البيانات. تفتح أداة التنميط (profiler) وترى أن allocs/op (الذاكرة المخصصة لكل عملية) مرتفعة جدًا. هل هذا مألوف؟ على الأرجح، المشكلة تكمن في كيفية تمرير البيانات إلى الدوال وإرجاعها. سؤال "Pointers vs. values in parameters and return values" هو واحد من أكثر الأسئلة إثارة للجدل على Stack Overflow (آلاف المشاهدات!)، وسنقوم اليوم بتحليله بالعربية، بشكل حيوي ومع أمثلة حقيقية.

\n\n

Go هي لغة تعشق البساطة. لكن هذه البساطة خادعة: الاختيار بين تمرير القيمة (value) والمؤشر (pointer) يمكن أن يغير بشكل جذري سرعة تشغيل البرنامج واستهلاكه للذاكرة. دعونا نفهم متى نستخدم كل منهما، بحيث يكون كودك سريعًا وليس بطيئًا.

\n\n

لماذا هذا الموضوع مهم جدًا؟

\n\n

على عكس Java أو Python، حيث كل شيء هو مرجع لكائن، في Go تختار بوضوح: إما تمرير نسخة من الهيكل أو عنوانه. قد يبدو هذا أمرًا بسيطًا. لكنه في الواقع قرار أساسي يؤثر على:

\n\n
    \n
  • سرعة التنفيذ — نسخ الهياكل الكبيرة يستغرق وقتًا.
  • \n
  • استهلاك الذاكرة — النسخ الزائدة = تخصيص ذاكرة زائد.
  • \n
  • أمان البيانات — المؤشرات تسمح بتعديل الأصل، القيم لا تسمح بذلك.
  • \n
  • التعامل مع الواجهات (interfaces) — الواجهات في Go هي آلية معقدة تعمل بشكل مختلف مع المؤشرات والقيم.
  • \n
\n\n

دعنا ننتقل مباشرة إلى العمل ونلقي نظرة على الكود.

\n\n

مثال رقم 1: هيكل صغير

\n\n

لنبدأ بشيء بسيط. لنفترض أن لدينا هيكل Point بإحداثيتين:

\n\n
type Point struct {\n    X, Y float64\n}\n\n// تمرير بالقيمة\nfunc MoveByValue(p Point, dx, dy float64) Point {\n    p.X += dx\n    p.Y += dy\n    return p\n}\n\n// تمرير بمؤشر\nfunc MoveByPointer(p *Point, dx, dy float64) {\n    p.X += dx\n    p.Y += dy\n}
\n\n

في الحالة الأولى، ننشئ نسخة من الهيكل، نعدلها، ونعيد نسخة جديدة. في الحالة الثانية، نعدل الأصل مباشرة. أي الخيارين أسرع؟ لمثل هذا الهيكل الصغير جدًا (16 بايت) — لا يوجد فرق تقريبًا. نسخ 16 بايت هي عملية ذرية، و GC (جامع القمامة) لا يخلق حملًا إضافيًا. بل إن التمرير بالقيمة قد يكون أسرع، لأن المؤشر يتطلب إلغاء مرجعية إضافي (dereference).

\n\n

الخلاصة: للهياكل الصغيرة (حتى 4-8 حقول من الأنواع الأساسية)، استخدم القيم بثقة. هذا أكثر أمانًا ولا يسبب أي حمل زائد.

\n\n

مثال رقم 2: هيكل كبير

\n\n

والآن، لنتخيل أننا نعمل مع بيانات المستخدم:

\n\n
type User struct {\n    ID        int64\n    Name      string\n    Email     string\n    Avatar    []byte // صورة بحجم 1 ميجابايت\n    CreatedAt time.Time\n    UpdatedAt time.Time\n}
\n\n

هنا، Avatar هو شريحة (slice)، وهي في Go نوع مرجعي (رأس: مؤشر للبيانات، طول، سعة). لكن الهيكل نفسه عند نسخه سينسخ فقط رأس الشريحة (24 بايت)، وليس البايتات نفسها. ومع ذلك، إذا كان الهيكل يحتوي على العديد من الحقول، فإن نسخ 100+ بايت في كل استدعاء دالة يصبح ملحوظًا.

\n\n

لنقم بتشغيل معيار أداء بسيط:

\n\n
func BenchmarkPassByValue(b *testing.B) {\n    u := User{ID: 1, Name: "John", Avatar: make([]byte, 1024*1024)}\n    for i := 0; i < b.N; i++ {\n        _ = processUserByValue(u)\n    }\n}\n\nfunc BenchmarkPassByPointer(b *testing.B) {\n    u := &User{ID: 1, Name: "John", Avatar: make([]byte, 1024*1024)}\n    for i := 0; i < b.N; i++ {\n        _ = processUserByPointer(u)\n    }\n}
\n\n

النتيجة (على جهاز حقيقي):

\n
    \n
  • PassByValue: ~120 ns/op, 0 تخصيصات ذاكرة
  • \n
  • PassBy

المدونات

توصيات الكتب