كيف تولد سلسلة عشوائية في Go دون أن تصاب بالجنون؟
\n\nهل سبق لك أن واجهت موقفًا تحتاج فيه إلى إنشاء معرف فريد بسرعة، أو كلمة مرور مؤقتة، أو رمز مميز لـ API؟ إذا كنت تكتب بلغة Go، فستواجه عاجلاً أم آجلاً السؤال: "كيف يمكن توليد سلسلة عشوائية بطول ثابت؟". هذا السؤال حصد آلاف المشاهدات على Stack Overflow، وليس من دون سبب - فالمهمة التي تبدو بسيطة تخفي في طياتها عدة مطبات.
\n\nفي هذه المقالة، سنشرح توليد سلسلة عشوائية في Go بدءًا من الحلول البسيطة وصولاً إلى الأساليب الجاهزة للإنتاج. ستتعرف على لماذا قد يكون math/rand خطيرًا، وكيفية استخدام crypto/rand للأمان، وكيفية كتابة كود أنيق دون تبعيات غير ضرورية.
لماذا لا يجب استخدام math/rand لكلمات المرور؟
\n\nلنبدأ بالحل الأكثر شيوعًا، ولكنه ليس دائمًا صحيحًا. يكتب العديد من المبتدئين شيئًا كهذا:
\n\npackage main\n\nimport (\n "math/rand"\n "time"\n)\n\nconst letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"\n\nfunc RandString(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = letters[rand.Intn(len(letters))]\n }\n return string(b)\n}\n\nfunc main() {\n rand.Seed(time.Now().UnixNano())\n println(RandString(10))\n}\n\nهذا الكود يعمل، لكنه يعاني من عيب خطير: math/rand يستخدم خوارزمية حتمية. إذا عرف المهاجم الـ seed (أو وقت التوليد)، فسيتمكن من إعادة إنتاج التسلسل بأكمله. هذا مقبول لبيانات الاختبار أو أسماء الملفات، لكنه مرفوض تمامًا للرموز السرية وكلمات المرور.
استخدام crypto/rand: توليد سلسلة عشوائية آمنة في Go
\n\nللبيئات الإنتاجية، الخيار الصحيح هو حزمة crypto/rand. فهي تستخدم مولد أرقام عشوائية آمنًا تشفيريًا من نظام التشغيل. إليك كيف يبدو توليد سلسلة عشوائية في Go باستخدام crypto/rand:
package main\n\nimport (\n "crypto/rand"\n "math/big"\n)\n\nconst charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()"\n\nfunc SecureRandomString(length int) (string, error) {\n result := make([]byte, length)\n for i := range result {\n num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))\n if err != nil {\n return "", err\n }\n result[i] = charset[num.Int64()]\n }\n return string(result), nil\n}\n\nfunc main() {\n str, err := SecureRandomString(16)\n if err != nil {\n panic(err)\n }\n println(str)\n}\n\nلاحظ أننا نستخدم rand.Int مع rand.Reader - وهذا يضمن اختيار كل حرف بشكل عشوائي وغير قابل للتنبؤ. بالنسبة لسلسلة عشوائية في Go، هذا هو المعيار الذهبي.
تحسين الأداء: كيفية تسريع التوليد
\n\nقد تكون الحلقة مع استدعاء rand.Int لكل حرف بطيئة عند توليد مئات الآلاف من السلاسل. الحل هو توليد جميع البايتات العشوائية دفعة واحدة، ثم تعيينها إلى أبجديتنا. هذه الحيلة غالبًا ما تُناقش على Stack Overflow في سياق Golang random string:
package main\n\nimport (\n "crypto/rand"\n "encoding/binary"\n)\n\nconst charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"\n\nfunc FastRandomString(length int) string {\n result := make([]byte, length)\n // توليد أرقام عشوائية 64 بت\n buf := make([]byte, 8*length)\n if _, err := rand.Read(buf); err != nil {\n panic(err)\n }\n for i := 0; i < length; i++ {\n // أخذ 8 بايت لكل حرف\n val := binary.LittleEndian.Uint64(buf[i*8 : (i+1)*8])\n // تجنب الانحياز (bias) باستخدام المعامل\n result[i] = charset[val%uint64(len(charset))]\n }\n return string(result)\n}\n\nfu