json.Unmarshal مقابل NewDecoder.Decode: أيهما أسرع في Go؟

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

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

ابدأ الدورة

أخوان بهلوانان: Unmarshal و Decode

\n\n

إذا كنت تكتب بلغة Go وتتعامل مع JSON، فستواجه عاجلاً أم آجلاً خيارًا: استخدام json.Unmarshal أم json.NewDecoder.Decode؟ السؤال شائع جدًا لدرجة أنه حصد آلاف المشاهدات وعشرات الأصوات على Stack Overflow. لكن الإجابة التي ستجدها هناك غالبًا ما تتلخص في "يعتمد على المهمة".

\n\n

اليوم لن نقدم إجابة مجردة فقط. سنقوم بتحقيقنا الخاص: سنشرح آلية عمل كلتا الطريقتين داخليًا، ونكتب اختبارات أداء (benchmarks)، ونعرض أمثلة حقيقية للكود، ونستخلص قواعد واضحة لمتى نستخدم أيهما. استعدوا - سيكون الأمر مثيرًا!

\n\n

باختصار عن الأساسيات: ما هما Unmarshal و Decode؟

\n\n

كلا الأداتين تقومان بنفس الشيء - تحويل JSON إلى هيكل (struct) في Go. لكنهما تفعلان ذلك بطرق مختلفة.

\n\n
    \n
  • json.Unmarshal - تأخذ []byte ومؤشرًا إلى الهيكل. كل شيء دفعة واحدة: البايتات موجودة، الهيكل جاهز، انطلق. تعمل "بشكل مباشر" - تقوم بإلغاء تسلسل (deserialize) مصفوفة البيانات بأكملها في الذاكرة.
  • \n
  • json.NewDecoder.Decode - تعمل مع التدفق (io.Reader). تقوم بإنشاء مفكك تشفير (decoder)، وتمرير قارئ (reader) إليه (مثل os.File أو http.Response.Body) ثم تستدعي Decode. يقرأ المفكك البيانات على دفعات، دون تحميل كل شيء في الذاكرة مرة واحدة.
  • \n
\n\n

يبدو الفرق واضحًا: للبيانات الكبيرة - استخدم Decoder، وللصغيرة - استخدم Unmarshal. لكن الشيطان، كما هو الحال دائمًا، يكمن في التفاصيل.

\n\n

آلية العمل الداخلية: كيف تعملان تحت الغطاء

\n\n

json.Unmarshal - طلقة قناص

\n\n

Unmarshal هي دالة تقوم بـ:

\n
    \n
  1. استقبال شريحة (slice) من البايتات.
  2. \n
  3. تخصيص ذاكرة للهيكل الناتج.
  4. \n
  5. تحليل (parse) JSON بالكامل في مسار واحد وملء الهيكل.
  6. \n
\n\n

كل شيء بسيط وأنيق. لكن هناك فارق بسيط: إذا قمت بتمرير JSON كبير (مثل 100 ميجابايت) إلى Unmarshal، فيجب عليك أولاً قراءة هذه الـ 100 ميجابايت في الذاكرة. ثم سيقوم Unmarshal بتخصيص ذاكرة إضافية للهيكل. النتيجة - استهلاك مزدوج للذاكرة (RAM).

\n\n

json.NewDecoder.Decode - سباك

\n\n

يعمل Decoder بشكل مختلف:

\n
    \n
  1. يتم إنشاء مفكك تشفير جديد يغلف io.Reader.
  2. \n
  3. يقرأ المفكك البيانات بمخازن مؤقتة (buffers) (افتراضيًا 4 كيلوبايت).
  4. \n
  5. أثناء القراءة، يقوم بتحليل JSON رمزًا (token) تلو الآخر ويملأ الهيكل.
  6. \n
\n\n

الميزة - أنك لا تخزن JSON بالكامل في الذاكرة. العيب - تكاليف إضافية طفيفة لإدارة المخزن المؤقت والقراءة التدفقية.

\n\n

التجربة: اختبارات الأداء لا تكذب

\n\n

دعنا نتحقق من النظرية عمليًا. سنكتب اختبار أداء بسيط لثلاثة سيناريوهات:

\n
    \n
  • JSON صغير (1 كيلوبايت)
  • \n
  • JSON متوسط (100 كيلوبايت)
  • \n
  • JSON كبير (10 ميجابايت)
  • \n
\n\n
package main\n\nimport (\n    "bytes"\n    "encoding/json"\n    "testing"\n)\n\ntype Data struct {\n    ID    int    `json:"id"`\n    Name  string `json:"name"`\n    Items []int  `json:"items"`\n}\n\nfunc generateJSON(size int) []byte {\n    data := Data{\n        ID:    1,\n        Name:  "test",\n        Items: make([]int, size),\n    }\n    for i := 0; i < size; i++ {\n        data.Items[i] = i\n    }\n    b, _ := json.Marshal(data)\n    return b\n}\n\nfunc BenchmarkUnmarshal(b *testing.B) {\n    jsonData := generateJSON(1000) // 1 KB\n    var result Data\n    for i := 0; i < b.N; i++ {\n        json.Unmarshal(jsonData, &result)\n    }\n}\n\nfunc BenchmarkDecoder(b *testing.B) {\n    jsonData := generateJSON(1000) // 1 KB\n    var result Data\n    for i := 0; i < b.N; i++ {\n        reader := bytes.NewReader(jsonData)\n        json.NewDecoder(reader).Decode(&result)\n    }\n}
\n\n

النتائج (على جهاز الكمبيوتر المحمول الخاص بي، Go 1.22):

\n\n\n \n
الحجم

المدونات

توصيات الكتب