الأسئلة الشائعة عن Go › الأسئلة الشائعة عن Go 1 / 1

الأسئلة الشائعة عن Go

  1. التوثيق
  2. الأسئلة الشائعة (FAQ)

الأسئلة الشائعة (FAQ)

النشأة

ما الغرض من المشروع؟

في زمن نشأة Go عام 2007 كان عالم البرمجة مختلفًا عن اليوم. كانت البرمجيات الإنتاجية تُكتب عادةً بلغة C++ أو Java، ولم يكن GitHub موجودًا، ولم تكن معظم الحواسيب بعد متعددة المعالجات، وبصرف النظر عن Visual Studio وEclipse لم يكن هناك سوى عدد قليل من بيئات التطوير المتكاملة (IDEs) أو غيرها من الأدوات عالية المستوى، ناهيك عن كونها مجانية على الإنترنت.

وفي الوقت نفسه، أصبحنا محبطين بسبب التعقيد المفرط المطلوب لبناء مشاريع برمجية كبيرة باللغات التي كنا نستخدمها وأنظمة البناء (build) المرتبطة بها. كانت الحواسيب قد أصبحت أسرع بكثير منذ تطوير لغات مثل C وC++ وJava، لكن فعل البرمجة نفسه لم يتقدم بالقدر نفسه. كذلك كان واضحًا أن المعالجات المتعددة أصبحت شائعة، لكن معظم اللغات لم تقدم سوى مساعدة قليلة لبرمجتها بكفاءة وأمان.

قررنا التراجع خطوة والتفكير في المسائل الكبرى التي ستسيطر على هندسة البرمجيات في السنوات المقبلة مع تطور التقنية، وكيف يمكن للغة جديدة أن تساعد في معالجتها. على سبيل المثال، أدى صعود وحدات المعالجة المركزية متعددة الأنوية (multicore CPUs) إلى القول بأن اللغة ينبغي أن توفر دعمًا من الدرجة الأولى لشكل من أشكال التزامن (concurrency) أو التوازي (parallelism). ولجعل إدارة الموارد ممكنة في برنامج متزامن كبير، كان لا بد من جمع النفايات (garbage collection)، أو على الأقل شكل من أشكال إدارة الذاكرة التلقائية الآمنة.

قادت هذه الاعتبارات إلى سلسلة من المناقشات التي نشأت منها لغة Go، أولًا كمجموعة من الأفكار والمتطلبات المرغوبة، ثم كلغة. وكان الهدف الشامل أن تفعل Go أكثر لمساعدة المبرمج العامل من خلال تمكين الأدوات، وأتمتة المهام الروتينية مثل تنسيق الشيفرة، وإزالة العقبات أمام العمل على قواعد شيفرة كبيرة.

يوجد وصف أكثر توسعًا لأهداف Go وكيفية تحقيقها، أو على الأقل الاقتراب منها، في مقالة Go في Google: تصميم اللغة في خدمة هندسة البرمجيات.

ما تاريخ المشروع؟

بدأ Robert Griesemer وRob Pike وKen Thompson في رسم أهداف لغة جديدة على السبورة في 21 سبتمبر 2007. وخلال أيام قليلة استقرت الأهداف في خطة لفعل شيء ما وفكرة واضحة إلى حد كبير عما ستكون عليه. واستمر التصميم بدوام جزئي بالتوازي مع أعمال أخرى. وبحلول يناير 2008، بدأ Ken العمل على مُصرِّف (compiler) لاستكشاف الأفكار؛ وكان ينتج شيفرة C كمخرجات له. وبحلول منتصف العام أصبحت اللغة مشروعًا بدوام كامل واستقرت بما يكفي لمحاولة بناء مُصرِّف إنتاجي. وفي مايو 2008، بدأ Ian Taylor بشكل مستقل العمل على واجهة أمامية (front end) لـ GCC للغة Go باستخدام المسودة الأولية للمواصفات. وانضم Russ Cox في أواخر 2008 وساعد في نقل اللغة والمكتبات من النموذج الأولي إلى الواقع.

أصبحت Go مشروعًا مفتوح المصدر علنيًا في 10 نوفمبر 2009. وقد ساهم عدد لا يحصى من الأشخاص في المجتمع بالأفكار والمناقشات والشيفرة.

يوجد الآن ملايين مبرمجي Go — الـ gophers — حول العالم، ويزداد عددهم كل يوم. وقد تجاوز نجاح Go توقعاتنا بكثير.

ما أصل تميمة الـ gopher؟

صممت التميمة والشعار Renée French، التي صممت أيضًا Glenda، أرنب Plan 9. وتشرح تدوينة عن الـ gopher كيف اشتُق من تصميم استخدمته لقميص WFMU قبل بضع سنوات. والشعار والتميمة مشمولان بترخيص Creative Commons Attribution 4.0.

للـ gopher ورقة نموذج توضح خصائصه وكيفية تمثيلها بشكل صحيح. وقد عُرضت ورقة النموذج أول مرة في محاضرة لـ Renée في Gophercon عام 2016. وله سمات فريدة؛ فهو gopher الخاص بـ Go، وليس أي gopher عادي.

هل اللغة تُسمى Go أم Golang؟

اللغة تُسمى Go. نشأ لقب golang لأن الموقع الإلكتروني كان في الأصل golang.org. (لم يكن هناك نطاق .dev حينها.) لكن كثيرين يستخدمون اسم golang، وهو مناسب كتسمية. على سبيل المثال، وسم وسائل التواصل الاجتماعي للغة هو "#golang". ومع ذلك، فاسم اللغة هو Go فقط.

ملاحظة جانبية: رغم أن الشعار الرسمي يتضمن حرفين كبيرين، يُكتب اسم اللغة Go وليس GO.

لماذا أنشأتم لغة جديدة؟

وُلدت Go من الإحباط إزاء اللغات والبيئات الموجودة للعمل الذي كنا نقوم به في Google. أصبحت البرمجة صعبة للغاية وكان لاختيار اللغات دور في ذلك. كان على المرء أن يختار بين تصريف (compilation) فعّال، أو تنفيذ فعّال، أو سهولة البرمجة؛ ولم تكن الثلاثة متاحة في اللغة السائدة نفسها. وكان المبرمجون القادرون يختارون السهولة على حساب الأمان والكفاءة بالانتقال إلى لغات ديناميكية الأنواع مثل Python وJavaScript بدلًا من C++ أو Java بدرجة أقل.

لم نكن وحدنا في مخاوفنا. فبعد سنوات من مشهد هادئ إلى حد كبير للغات البرمجة، كانت Go من أوائل عدة لغات جديدة — Rust وElixir وSwift وغيرها — أعادت تطوير لغات البرمجة ليصبح مجالًا نشطًا وشبه سائد.

عالجت Go هذه المسائل بمحاولة الجمع بين سهولة برمجة لغة مُفسَّرة ديناميكية الأنواع، وكفاءة وأمان لغة مُصرَّفة ساكنة الأنواع. كما كانت تهدف إلى أن تكون أكثر تكيفًا مع العتاد الحالي، بدعم الحوسبة الشبكية ومتعددة الأنوية. وأخيرًا، يُقصد أن يكون العمل مع Go سريعًا: ينبغي ألا يستغرق بناء ملف تنفيذي كبير على حاسوب واحد سوى ثوانٍ قليلة. وقد قادنا تحقيق هذه الأهداف إلى إعادة التفكير في بعض أساليب البرمجة من لغاتنا الحالية، مما أدى إلى: نظام أنواع تركيبي بدلًا من هرمي؛ ودعم للتزامن وجمع النفايات؛ وتحديد صارم للاعتماديات؛ وهكذا. لا يمكن للمكتبات أو الأدوات معالجة هذه الأمور جيدًا؛ فقد كان لا بد من لغة جديدة.

تناقش مقالة Go في Google الخلفية والدافع وراء تصميم لغة Go، وتقدم المزيد من التفاصيل حول كثير من الأجوبة الواردة في هذه الأسئلة الشائعة.

ما أسلاف Go؟

تنتمي Go في معظمها إلى عائلة C (الصيغة الأساسية)، مع إسهام كبير من عائلة Pascal/Modula/Oberon (التعريفات والحزم (packages))، إضافة إلى بعض الأفكار من لغات مستوحاة من CSP لـ Tony Hoare، مثل Newsqueak وLimbo (التزامن). لكنها لغة جديدة من كل الأوجه. وقد صُممت اللغة في كل جوانبها بالتفكير في ما يفعله المبرمجون وكيفية جعل البرمجة، أو على الأقل نوع البرمجة الذي نمارسه، أكثر فعالية، أي أكثر متعة.

ما المبادئ الموجهة في التصميم؟

عندما صُممت Go، كانت Java وC++ اللغتين الأكثر استخدامًا لكتابة الخوادم، على الأقل في Google. ورأينا أن هاتين اللغتين تتطلبان قدرًا كبيرًا جدًا من الأعمال المكتبية (bookkeeping) والتكرار. وقد تفاعل بعض المبرمجين بالتحول نحو لغات أكثر ديناميكية ومرونة مثل Python، على حساب الكفاءة وأمان الأنواع. ورأينا أنه ينبغي أن يكون من الممكن الجمع بين الكفاءة والأمان والمرونة في لغة واحدة.

تحاول Go تقليل مقدار الكتابة بالمعنيين. خلال تصميمها كله، حاولنا تقليل الفوضى والتعقيد. لا توجد تعريفات مسبقة ولا ملفات رؤوس؛ فكل شيء يُعرَّف مرة واحدة فقط. التهيئة معبّرة وتلقائية وسهلة الاستخدام. والصيغة نظيفة وقليلة الكلمات المفتاحية. ويُقلَّل التكرار (foo.Foo* myFoo = new(foo.Foo)) باستنتاج نوع بسيط باستخدام تركيب التعريف والتهيئة :=. وربما الأكثر جذرية، لا يوجد تدرج هرمي للأنواع: فالأنواع ببساطة توجد، وليس عليها الإعلان عن علاقاتها. وتسمح هذه التبسيطات لـ Go بأن تكون معبّرة ومفهومة معًا دون التضحية بالإنتاجية.

مبدأ آخر مهم هو إبقاء المفاهيم متعامدة. يمكن تنفيذ الدوال (methods) لأي نوع؛ فالبُنَى (structs) تمثل البيانات بينما تمثل الواجهات (interfaces) التجريد؛ وهكذا. والتعامد يجعل من الأسهل فهم ما يحدث عند دمج الأشياء.

الاستخدام

هل تستخدم Google لغة Go داخليًا؟

نعم. تُستخدم Go على نطاق واسع في الإنتاج داخل Google. ومن الأمثلة خادم التنزيلات الخاص بـ Google، dl.google.com، الذي يوزّع ملفات Chrome التنفيذية وغيرها من الملفات القابلة للتثبيت الكبيرة مثل حزم apt-get.

ليست Go اللغة الوحيدة المستخدمة في Google، ولا حتى قريبة من ذلك، لكنها لغة أساسية في عدد من المجالات، منها هندسة موثوقية المواقع (SRE) ومعالجة البيانات على نطاق واسع. وهي أيضًا جزء أساسي من البرمجيات التي تشغّل Google Cloud.

ما الشركات الأخرى التي تستخدم Go؟

يتزايد استخدام Go في العالم كله، خصوصًا في مجال الحوسبة السحابية وإن لم يكن حصرًا. ومن مشاريع البنية التحتية السحابية الكبرى المكتوبة بلغة Go مشروعا Docker وKubernetes، لكن هناك الكثير غيرها.

وهي ليست في السحابة فقط، كما ترى من قائمة الشركات على موقع go.dev الإلكتروني إلى جانب بعض قصص النجاح. كذلك يتضمن Go Wiki صفحة تُحدَّث بانتظام وتسرد بعض الشركات الكثيرة التي تستخدم Go.

ولدى الـ Wiki أيضًا صفحة تحتوي على روابط لمزيد من قصص النجاح عن شركات ومشاريع تستخدم اللغة.

هل ترتبط برامج Go ببرامج C/C++؟

من الممكن استخدام C وGo معًا في فضاء العناوين نفسه، لكن ذلك ليس توافقًا طبيعيًا وقد يتطلب برمجيات واجهة خاصة. كذلك فإن ربط C بشيفرة Go يتخلى عن خصائص أمان الذاكرة وإدارة المكدّس التي توفرها Go. أحيانًا يكون استخدام مكتبات C ضروريًا تمامًا لحل مشكلة ما، لكن فعل ذلك يقدم دائمًا عنصر خطر غير موجود في شيفرة Go الخالصة، فافعلوه بحذر.

إذا كنت بحاجة فعلًا إلى استخدام C مع Go، فإن كيفية المضي تعتمد على تنفيذ مُصرِّف Go. المُصرِّف «القياسي»، وهو جزء من سلسلة أدوات (toolchain) Go التي يدعمها فريق Go في Google، يُسمى gc. إضافة إلى ذلك، يوجد مُصرِّف قائم على GCC (gccgo) ومُصرِّف قائم على LLVM (gollvm)، فضلًا عن قائمة متنامية من المُصرِّفات غير المعتادة التي تخدم أغراضًا مختلفة، وأحيانًا تصرّف مجموعات فرعية من اللغة، مثل TinyGo.

يستخدم Gc اصطلاح نداء ورابطًا مختلفين عن C، ولذلك لا يمكن نداؤه مباشرة من برامج C أو العكس. ويوفر برنامج cgo آلية «واجهة الدوال الأجنبية» (foreign function interface) للسماح بالنداء الآمن لمكتبات C من شيفرة Go. ويوسّع SWIG هذه القدرة لتشمل مكتبات C++.

يمكنك أيضًا استخدام cgo وSWIG مع gccgo وgollvm. وبما أنها تستخدم ABI تقليديًا، فمن الممكن أيضًا، بحذر شديد، ربط الشيفرة الناتجة من هذه المُصرِّفات مباشرة ببرامج C أو C++ المُصرَّفة بـ GCC/LLVM. غير أن فعل ذلك بأمان يتطلب فهم اصطلاحات النداء لكل اللغات المعنية، فضلًا عن الانتباه إلى حدود المكدّس عند نداء C أو C++ من Go.

ما بيئات التطوير المتكاملة التي تدعمها Go؟

لا يتضمن مشروع Go بيئة تطوير متكاملة مخصصة، لكن اللغة والمكتبات صُممتا لتسهيل تحليل الشيفرة المصدرية. ونتيجة لذلك، تدعم معظم المحررات وبيئات التطوير المتكاملة المعروفة لغة Go جيدًا، إما مباشرة أو عبر إضافة.

يدعم فريق Go أيضًا خادم لغة Go لبروتوكول LSP، يُسمى gopls. ويمكن للأدوات التي تدعم LSP استخدام gopls لدمج دعم خاص باللغة.

تشمل قائمة بيئات التطوير المتكاملة والمحررات المعروفة التي تقدم دعمًا جيدًا لـ Go كلاً من Emacs وVim وVSCode وAtom وEclipse وSublime وIntelliJ (عبر نسخة مخصصة تُسمى GoLand) وغيرها الكثير. ومن المرجح أن تكون بيئتك المفضلة بيئة منتجة للبرمجة بلغة Go.

هل تدعم Go بروتوكول Protocol Buffers الخاص بـ Google؟

يوفر مشروع مفتوح المصدر منفصل إضافة المُصرِّف والمكتبة اللازمة. وهو متاح على github.com/golang/protobuf/.

التصميم

هل تمتلك Go بيئة تشغيل؟

تمتلك Go مكتبة تشغيل واسعة، غالبًا ما تُسمى ببساطة بيئة التشغيل (runtime)، وهي جزء من كل برنامج Go. تنفذ هذه المكتبة جمع النفايات والتزامن وإدارة المكدّس وغيرها من الخصائص الحيوية للغة Go. ومع أنها أكثر مركزية في اللغة، فإن بيئة تشغيل Go مماثلة لـ libc، مكتبة C.

من المهم أن نفهم، مع ذلك، أن بيئة تشغيل Go لا تتضمن آلة افتراضية، كما هو الحال في بيئة تشغيل Java. فبرامج Go تُصرَّف مسبقًا إلى شيفرة آلة أصلية (أو إلى JavaScript أو WebAssembly في بعض التنفيذات البديلة). وهكذا، رغم أن المصطلح يُستخدم غالبًا لوصف البيئة الافتراضية التي يعمل فيها البرنامج، فإن كلمة «runtime» في Go ليست سوى اسم المكتبة التي تقدم خدمات اللغة الحيوية.

ما شأن معرّفات Unicode؟

عند تصميم Go، أردنا التأكد من أنها ليست متمركزة حول ASCII بإفراط، وهو ما يعني توسيع فضاء المعرّفات خارج حدود ASCII ذي 7 بتات. وقاعدة Go — أن تكون أحرف المعرّف حروفًا أو أرقامًا كما يعرّفها Unicode — سهلة الفهم والتنفيذ لكن لها قيود. فالأحرف المدمجة مستبعدة بحكم التصميم، مثلًا، وهذا يستبعد بعض اللغات مثل الديفاناغارية.

ولهذه القاعدة نتيجة مؤسفة أخرى. فبما أن المعرّف المصدَّر يجب أن يبدأ بحرف كبير، فإن المعرّفات المكوَّنة من أحرف في بعض اللغات لا يمكن، بحكم التعريف، تصديرها. والسبيل الوحيد حاليًا هو استخدام شيء مثل X ، وهو حل غير مُرضٍ بوضوح.

منذ أقدم نسخة من اللغة، كان هناك تفكير كبير في أفضل طريقة لتوسيع فضاء المعرّفات لاستيعاب المبرمجين الذين يستخدمون لغات أم أخرى. وما ينبغي فعله بالضبط لا يزال موضوع نقاش نشط، وقد تكون نسخة مستقبلية من اللغة أكثر تحررًا في تعريفها للمعرّف. فقد تتبنى مثلًا بعض الأفكار من توصيات منظمة Unicode بشأن المعرّفات. ومهما حدث، يجب أن يتم ذلك بشكل متوافق مع الحفاظ (أو ربما توسيع) الطريقة التي تحدد بها حالة الأحرف ظهور المعرّفات، وهي لا تزال إحدى سماتنا المفضلة في Go.

في الوقت الحالي، لدينا قاعدة بسيطة يمكن توسيعها لاحقًا دون كسر البرامج، وهي قاعدة تتجنب الأخطاء التي ستنشأ حتمًا عن قاعدة تسمح بمعرّفات غامضة.

لماذا لا تمتلك Go الميزة X؟

كل لغة تحتوي على ميزات جديدة وتغفل ميزة يحبها شخص ما. صُممت Go مع مراعاة جودة البرمجة وسرعة التصريف وتعامد المفاهيم والحاجة إلى دعم ميزات مثل التزامن وجمع النفايات. وقد تكون ميزتك المفضلة غائبة لأنها لا تناسب التصميم، أو لأنها تؤثر على سرعة التصريف أو وضوح التصميم، أو لأنها ستجعل نموذج النظام الأساسي معقدًا أكثر من اللازم.

إذا كان يزعجك أن Go تفتقد الميزة X، فاغفر لنا وابحث في الميزات التي تمتلكها Go فعلًا. فقد تجد أنها تعوّض عن غياب X بطرق مثيرة للاهتمام.

متى حصلت Go على الأنواع العامة؟

أضاف إصدار (release) Go 1.18 وسائط الأنواع (type parameters) إلى اللغة. وهذا يسمح بشكل من البرمجة متعددة الأشكال أو العامة (generic). راجع مواصفات اللغة والمقترح للتفاصيل.

لماذا صدرت Go أول مرة دون الأنواع العامة؟

كانت Go مقصودة لتكون لغة لكتابة برامج الخوادم يسهل صيانتها مع الوقت. (راجع هذه المقالة لمزيد من الخلفية.) ركّز التصميم على أمور مثل قابلية التوسع والقراءة والتزامن. ولم تكن البرمجة متعددة الأشكال تبدو أساسية لأهداف اللغة حينها، ولذلك استُبعدت في البداية للتبسيط.

الأنواع العامة ملائمة لكنها تأتي بتكلفة من التعقيد في نظام الأنواع ووقت التشغيل. وقد استغرق الأمر وقتًا لتطوير تصميم نعتقد أنه يقدم قيمة تتناسب مع التعقيد.

لماذا لا تمتلك Go الاستثناءات؟

نعتقد أن ربط الاستثناءات ببنية تحكم، كما في اصطلاح try-catch-finally، يُنتج شيفرة ملتوية. كما يميل إلى تشجيع المبرمجين على وسم أخطاء عادية كثيرة، مثل الفشل في فتح ملف، بأنها استثنائية.

تتبع Go نهجًا مختلفًا. ففي معالجة الأخطاء (error handling) العادية، تجعل الإرجاعات متعددة القيم في Go من السهل الإبلاغ عن خطأ دون تحميل قيمة الإرجاع أعباء زائدة. ونوع خطأ قانوني، إلى جانب ميزات Go الأخرى، يجعل معالجة الأخطاء ممتعة لكن مختلفة تمامًا عما هي عليه في لغات أخرى.

كما تمتلك Go دالتين مدمجتين للإشارة إلى الظروف الاستثنائية فعلًا والتعافي منها. ولا تُنفَّذ آلية التعافي إلا كجزء من تفكيك حالة دالة بعد خطأ، وهو ما يكفي لمعالجة الكوارث لكنه لا يتطلب بنى تحكم إضافية، ويمكن أن يؤدي، عند استخدامه جيدًا، إلى شيفرة نظيفة لمعالجة الأخطاء.

راجع مقالة Defer, Panic, and Recover للتفاصيل. كذلك تصف تدوينة الأخطاء قيم نهجًا لمعالجة الأخطاء بشكل نظيف في Go بإظهار أنه، بما أن الأخطاء مجرد قيم، يمكن تسخير كامل قوة Go في معالجة الأخطاء.

لماذا لا تمتلك Go التأكيدات؟

لا توفر Go التأكيدات (assertions). فهي ملائمة بلا شك، لكن تجربتنا كانت أن المبرمجين يستخدمونها عكازًا لتجنب التفكير في معالجة الأخطاء والإبلاغ عنها بشكل صحيح. معالجة الأخطاء الصحيحة تعني أن تستمر الخوادم في العمل بدلًا من الانهيار بعد خطأ غير قاتل. والإبلاغ الصحيح عن الأخطاء يعني أن تكون الأخطاء مباشرة وفي الصميم، ما يوفّر على المبرمج تفسير أثر انهيار كبير. والأخطاء الدقيقة مهمة بشكل خاص عندما لا يكون المبرمج الذي يرى الأخطاء على دراية بالشيفرة.

نحن نفهم أن هذه نقطة خلاف. فهناك أمور كثيرة في لغة Go ومكتباتها تختلف عن الممارسات الحديثة، ببساطة لأننا نرى أنه يستحق أحيانًا تجربة نهج مختلف.

لماذا بُني التزامن على أفكار CSP؟

اكتسب التزامن والبرمجة متعددة الخيوط مع الوقت سمعة بالصعوبة. ونعتقد أن هذا يرجع جزئيًا إلى تصاميم معقدة مثل pthreads وجزئيًا إلى الإفراط في التركيز على تفاصيل منخفضة المستوى مثل mutexes والمتغيرات الشرطية وحواجز الذاكرة. وتتيح الواجهات عالية المستوى شيفرة أبسط بكثير، حتى لو كانت لا تزال هناك mutexes وأمور مثلها تحت الغطاء.

يأتي أحد أنجح النماذج لتقديم دعم لغوي عالي المستوى للتزامن من العمليات التسلسلية المتواصلة (Communicating Sequential Processes) لهوير، أو CSP. وOccam وErlang لغتان معروفتان تنحدران من CSP. وتستمد بدائيات التزامن في Go أصلها من جزء مختلف من شجرة العائلة، وإسهامه الرئيسي هو فكرة القنوات (channels) ككائنات من الدرجة الأولى. وقد أظهرت التجربة مع عدة لغات سابقة أن نموذج CSP يتلاءم جيدًا مع إطار لغة إجرائية.

لماذا كوروتينات Go بدلًا من الخيوط؟

تُعد كوروتينات Go (goroutines) جزءًا من جعل التزامن سهل الاستخدام. والفكرة، التي وُجدت منذ زمن، هي تعدد إرسال دوال تُنفَّذ بشكل مستقل — ألا وهي الكوروتينات — على مجموعة من الخيوط. وعندما يحجب كوروتين، مثلًا عبر نداء نظام حاجب، تنقل بيئة التشغيل تلقائيًا الكوروتينات الأخرى على نفس خيط نظام التشغيل إلى خيط آخر قابل للتنفيذ حتى لا تُحجب. ولا يرى المبرمج شيئًا من هذا، وهذا هو المقصود. والنتيجة، التي نسميها كوروتينات Go، يمكن أن تكون رخيصة جدًا: فليس لها سوى عبء قليل يتجاوز ذاكرة المكدّس، وهي بضعة كيلوبايتات فقط.

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

لماذا لا تُعرَّف عمليات الخرائط بأنها ذرّية؟

بعد نقاش طويل، تقرر أن الاستخدام النموذجي للخرائط (maps) لا يتطلب وصولًا آمنًا من كوروتينات متعددة، وأن الخريطة في الحالات التي يتطلب فيها ذلك كانت على الأرجح جزءًا من بنية بيانات أو عملية حسابية أكبر ومتزامنة أصلًا. ولذلك فإن إلزام جميع عمليات الخرائط بالتقاط mutex سيُبطئ معظم البرامج ويضيف الأمان لعدد قليل. ولم يكن هذا قرارًا سهلًا، مع ذلك، لأنه يعني أن الوصول غير المنضبط إلى الخريطة يمكن أن يُنهي البرنامج.

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

لا يكون الوصول إلى الخريطة غير آمن إلا عندما تحدث تحديثات. فطالما كانت جميع الكوروتينات تقرأ فقط — بالبحث عن عناصر في الخريطة، بما في ذلك التكرار عبرها باستخدام حلقة for range — ولا تغيّر الخريطة بإسناد قيم إلى العناصر أو بحذفها، فمن الآمن لها الوصول إلى الخريطة بالتزامن دون مزامنة.

وللمساعدة في الاستخدام الصحيح للخرائط، تحتوي بعض تنفيذات اللغة على فحص خاص يُبلّغ تلقائيًا في وقت التشغيل عندما تُعدَّل خريطة بشكل غير آمن بتنفيذ متزامن. كذلك يوجد في مكتبة sync نوع يُسمى sync.Map يعمل جيدًا لأنماط استخدام معينة مثل ذاكرات التخزين المؤقت الثابتة، مع أنه ليس بديلًا عامًا مناسبًا لنوع الخريطة المدمج.

هل ستقبلون تغييري على اللغة؟

يقترح الناس غالبًا تحسينات على اللغة — وتحتوي القائمة البريدية على تاريخ غني بمثل هذه المناقشات — لكن قليلًا جدًا من هذه التغييرات قُبل.

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

وحتى لو كان اقتراحك متوافقًا مع مواصفات Go 1، فقد لا يكون في روح أهداف تصميم Go. وتشرح مقالة Go في Google: تصميم اللغة في خدمة هندسة البرمجيات أصول Go والدافع وراء تصميمها.

الأنواع

هل Go لغة كائنية التوجه؟

نعم ولا. فرغم أن Go تمتلك أنواعًا ودوالًا (methods) وتسمح بأسلوب برمجة كائني التوجه، فلا يوجد تدرج هرمي للأنواع. ويوفر مفهوم «الواجهة» في Go نهجًا مختلفًا نعتقد أنه سهل الاستخدام وأكثر عمومية من بعض النواحي. وهناك أيضًا طرق لتضمين (embedding) الأنواع في أنواع أخرى لتوفير شيء مماثل — لكن غير مطابق — للوراثة الفرعية. علاوة على ذلك، الدوال في Go أكثر عمومية من C++ أو Java: يمكن تعريفها لأي صنف من البيانات، حتى الأنواع المدمجة مثل الأعداد الصحيحة العادية «غير المغلفة». وهي غير مقصورة على البنى (الصنوف).

كما أن غياب التدرج الهرمي للأنواع يجعل «الكائنات» في Go تبدو أخف بكثير مما هي عليه في لغات مثل C++ أو Java.

كيف أحصل على إرسال ديناميكي للدوال؟

السبيل الوحيد لامتلاك دوال تُرسَل ديناميكيًا هو عبر واجهة. أما الدوال على بنية أو أي نوع ملموس آخر فتُحلّ دائمًا بشكل ساكن.

لماذا لا توجد وراثة الأنواع؟

البرمجة كائنية التوجه، في أشهر اللغات على الأقل، تنطوي على نقاش مفرط حول العلاقات بين الأنواع، وهي علاقات يمكن غالبًا استنتاجها تلقائيًا. وتتبع Go نهجًا مختلفًا.

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

من الممكن استخدام هذه الأفكار لبناء شيء مماثل لأنابيب Unix الآمنة الأنواع. على سبيل المثال، انظر كيف يتيح fmt.Fprintf الطباعة المنسّقة إلى أي مخرج، وليس إلى ملف فقط، أو كيف يمكن لحزمة bufio أن تكون منفصلة تمامًا عن إدخال/إخراج الملفات، أو كيف تولّد حزم image ملفات صور مضغوطة. كل هذه الأفكار تنبع من واجهة واحدة (io.Writer) تمثل دالة واحدة (Write). وهذا ليس سوى خدش للسطح. فللواجهات Go تأثير عميق على كيفية بناء البرامج.

يحتاج الأمر إلى بعض التعوّد، لكن هذا الأسلوب الضمني لاعتماد الأنواع من أكثر الأمور إنتاجية في Go.

لماذا len دالة وليست دالة مرتبطة بنوع؟

تجادلنا في هذه المسألة لكننا قررنا أن تنفيذ len وأخواتها كدوال عادية كان جيدًا في الممارسة ولم يعقّد الأسئلة حول الواجهة (بالمعنى النوعي في Go) للأنواع الأساسية.

لماذا لا تدعم Go تحميل الدوال والعوامل؟

يُبسَّط إرسال الدوال إذا لم يكن بحاجة إلى مطابقة الأنواع أيضًا. وقد أخبرتنا التجربة مع لغات أخرى أن وجود مجموعة متنوعة من الدوال بالاسم نفسه لكن بتواقيع مختلفة كان مفيدًا أحيانًا، لكنه قد يكون أيضًا مربكًا وهشًا في الممارسة. وكانت المطابقة بالاسم فقط مع اشتراط الاتساق في الأنواع قرارًا مبسّطًا كبيرًا في نظام أنواع Go.

أما بالنسبة لتحميل العوامل (operator overloading)، فيبدو أقرب إلى الراحة من كونه متطلبًا مطلقًا. ومرة أخرى، الأمور أبسط بدونه.

لماذا لا تمتلك Go تصريحات «implements»؟

يفي نوع Go بواجهة عبر تنفيذ دوال تلك الواجهة، لا أكثر. وتسمح هذه الخاصية بتعريف الواجهات واستخدامها دون الحاجة إلى تعديل الشيفرة القائمة. وهي تمكّن نوعًا من الكتابة البنيوية يعزز فصل الاهتمامات ويحسّن إعادة استخدام الشيفرة، ويسهّل البناء على الأنماط التي تظهر مع تطور الشيفرة. ودلالات الواجهات أحد الأسباب الرئيسية للطابع الرشيق والخفيف لـ Go.

راجع سؤال وراثة الأنواع لمزيد من التفاصيل.

كيف يمكنني ضمان أن نوعي يفي بواجهة؟

يمكنك أن تطلب من المُصرِّف التحقق من أن النوع T ينفذ الواجهة I عبر محاولة إسناد باستخدام القيمة الصفرية لـ T أو مؤشّر (pointer) إلى T، حسب الاقتضاء:

type T struct{}
var _ I = T{}       // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I.

إذا لم ينفذ T (أو *T، وفقًا لذلك) الواجهة I، فسيُلتقط الخطأ في وقت التصريف.

وإذا أردت أن يعلن مستخدمو الواجهة صراحة أنهم ينفذونها، فيمكنك إضافة دالة باسم وصفي إلى مجموعة دوال الواجهة (method set). على سبيل المثال:

type Fooer interface {
    Foo()
    ImplementsFooer()
}

عندئذٍ يجب على النوع تنفيذ الدالة ImplementsFooer ليكون Fooer، موثقًا الحقيقة بوضوح ومعلنًا إياها في مخرجات go doc.

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

لا تستفيد معظم الشيفرة من مثل هذه القيود، لأنها تحد من فائدة فكرة الواجهة. لكنها ضرورية أحيانًا لحل الغموض بين واجهات متشابهة.

لماذا لا يفي النوع T بواجهة Equal؟

تأمل هذه الواجهة البسيطة لتمثيل كائن يمكنه مقارنة نفسه بقيمة أخرى:

type Equaler interface {
    Equal(Equaler) bool
}

وهذا النوع، T:

type T int
func (t T) Equal(u T) bool { return t == u } // does not satisfy Equaler

خلافًا للحالة المماثلة في بعض أنظمة الأنواع متعددة الأشكال، لا ينفذ T الواجهة Equaler. فنوع وسيط T.Equal هو T، وليس النوع المطلوب حرفيًا Equaler.

في Go، لا يرقّي نظام الأنواع وسيط Equal؛ فتلك مسؤولية المبرمج، كما يوضح النوع T2 الذي ينفذ فعلًا الواجهة Equaler:

type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) }  // satisfies Equaler

لكن هذا أيضًا ليس مثل أنظمة أنواع أخرى، لأن أي نوع في Go يفي بـ Equaler يمكن تمريره كوسيط إلى T2.Equal، وعلينا في وقت التشغيل التحقق من أن الوسيط من النوع T2. وقد ترتب بعض اللغات ضمان ذلك في وقت التصريف.

وهناك مثال ذو صلة يسير في الاتجاه المعاكس:

type Opener interface {
   Open() Reader
}

func (t T3) Open() *os.File

في Go، لا يفي T3 بالواجهة Opener، مع أنه قد يفي بها في لغة أخرى.

مع صحة أن نظام أنواع Go يقدم للمبرمج أقل في مثل هذه الحالات، فإن غياب الأنواع الفرعية (subtyping) يجعل قواعد تحقق الواجهة سهلة الصياغة جدًا: هل أسماء الدالة وتواقيعها مطابقة تمامًا لتلك الموجودة في الواجهة؟ كما أن قاعدة Go سهلة التنفيذ بكفاءة. ونرى أن هذه الفوائد تعوّض غياب الترقية التلقائية للأنواع.

هل يمكنني تحويل []T إلى []interface{}؟

ليس مباشرة. فاللغة تمنعه لأن النوعين ليس لهما التمثيل نفسه في الذاكرة. ولا بد من نسخ العناصر فردًا فردًا إلى الشريحة (slice) الهدف. يحوّل هذا المثال شريحة من int إلى شريحة من interface{}:

t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
    s[i] = v
}

هل يمكنني تحويل []T1 إلى []T2 إذا كان T1 وT2 لهما النوع الأساسي نفسه؟

السطر الأخير من نموذج الشيفرة هذا لا يُصرَّف.

type T1 int
type T2 int
var t1 T1
var x = T2(t1) // OK
var st1 []T1
var sx = ([]T2)(st1) // NOT OK

في Go، ترتبط الأنواع ارتباطًا وثيقًا بالدوال، بمعنى أن لكل نوع مسمّى مجموعة دوال (قد تكون فارغة). والقاعدة العامة أنك تستطيع تغيير اسم النوع الذي يُحوَّل (وبالتالي ربما تغيير مجموعة دواله) لكن لا يمكنك تغيير اسم (ومجموعة دوال) عناصر نوع مركّب. وتطالبك Go بأن تكون صريحًا بشأن تحويلات الأنواع.

لماذا لا تساوي قيمة الخطأ nil لديّ nil؟

تحت الغطاء، تُنفَّذ الواجهات بعنصرين: نوع T وقيمة V. وV قيمة ملموسة مثل int أو بنية أو مؤشّر، وليست واجهة نفسها أبدًا، ونوعها T. على سبيل المثال، إذا خزّنا القيمة int 3 في واجهة، فإن قيمة الواجهة الناتجة تكون، بشكل تخطيطي، (T=int، V=3). وتُعرف القيمة V أيضًا بالقيمة الديناميكية للواجهة، لأن متغير واجهة معينًا قد يحمل قيمًا V مختلفة (وأنواعًا T مقابلة) أثناء تنفيذ البرنامج.

تكون قيمة الواجهة nil فقط إذا كان V وT كلاهما غير مضبوط، (T=nil، وV غير مضبوطة). وعلى وجه الخصوص، فإن واجهة nil ستحمل دائمًا نوعًا nil. وإذا خزّنا مؤشّرًا nil من النوع *int داخل قيمة واجهة، فسيكون النوع الداخلي *int بصرف النظر عن قيمة المؤشّر: (T=*int، V=nil). ولذلك ستكون قيمة الواجهة هذه غير nil حتى عندما تكون قيمة المؤشّر V بداخلها nil.

قد يكون هذا الوضع مربكًا، وينشأ عندما تُخزَّن قيمة nil داخل قيمة واجهة مثل إرجاع error:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Will always return a non-nil error.
}

إذا سار كل شيء على ما يرام، ترجع الدالة p بقيمة nil، فتكون قيمة الإرجاع قيمة واجهة error تحمل (T=*MyError، V=nil). وهذا يعني أنه إذا قارن المستدعي الخطأ المُرجَع بـ nil، فسيبدو الأمر دائمًا وكأن هناك خطأ حتى لو لم يحدث شيء سيئ. ولإرجاع error بـ nil بشكل صحيح إلى المستدعي، يجب أن ترجع الدالة nil صريحًا:

func returnsError() error {
    if bad() {
        return ErrBad
    }
    return nil
}

من الجيد أن تستخدم الدوال التي ترجع أخطاءً نوع error دائمًا في توقيعها (كما فعلنا أعلاه) بدلًا من نوع ملموس مثل *MyError، للمساعدة في ضمان إنشاء الخطأ بشكل صحيح. وعلى سبيل المثال، ترجع os.Open قيمة error رغم أنها، إن لم تكن nil، دائمًا من النوع الملموس *os.PathError.

قد تنشأ مواقف مشابهة لتلك الموصوفة هنا كلما استُخدمت الواجهات. فقط تذكّر أنه إذا خُزّنت أي قيمة ملموسة في الواجهة، فلن تكون الواجهة nil. لمزيد من المعلومات، راجع قوانين الانعكاس (The Laws of Reflection).

لماذا تتصرف الأنواع صفرية الحجم بغرابة؟

تدعم Go الأنواع صفرية الحجم، مثل بنية بلا حقول (struct{}) أو مصفوفة (array) بلا عناصر ([0]byte). لا يمكنك تخزين أي شيء في نوع صفري الحجم، لكن هذه الأنواع مفيدة أحيانًا عندما لا تكون هناك حاجة إلى قيمة، كما في map[int]struct{} أو نوع له دوال لكن بلا قيمة.

قد تُوضع متغيرات مختلفة من نوع صفري الحجم في الموقع نفسه في الذاكرة. وهذا آمن لأنه لا يمكن تخزين أي قيمة في تلك المتغيرات.

علاوة على ذلك، لا تقدم اللغة أي ضمانات بشأن ما إذا كانت المؤشّرات إلى متغيرين مختلفين صفريي الحجم ستُقارن متساوية أم لا. بل قد ترجع هذه المقارنات true في نقطة من البرنامج ثم ترجع false في نقطة أخرى، حسب كيفية تصريف البرنامج وتنفيذه بالضبط.

وثمة مسألة منفصلة تتعلق بالأنواع صفرية الحجم، هي أن المؤشّر إلى حقل بنية صفري الحجم يجب ألا يتداخل مع مؤشّر إلى كائن آخر في الذاكرة. فقد يسبب ذلك ارتباكًا في جامع النفايات. وهذا يعني أنه إذا كان الحقل الأخير في بنية صفري الحجم، فستُحشى البنية لضمان ألا يتداخل المؤشّر إلى الحقل الأخير مع الذاكرة التي تلي البنية مباشرة. وهكذا، هذا البرنامج:

func main() {
    type S struct {
        f1 byte
        f2 struct{}
    }
    fmt.Println(unsafe.Sizeof(S{}))
}

سيطبع 2، وليس 1، في معظم تنفيذات Go.

لماذا لا توجد اتحادات بلا وسوم، كما في C؟

ستنتهك الاتحادات بلا وسوم (untagged unions) ضمانات أمان الذاكرة في Go.

لماذا لا تمتلك Go أنواعًا متغيرة؟

توفر الأنواع المتغيرة (variant types)، المعروفة أيضًا بالأنواع الجبرية، طريقة لتحديد أن قيمة قد تتخذ أحد مجموعة من الأنواع الأخرى، لكن تلك الأنواع فقط. ومن الأمثلة الشائعة في برمجة الأنظمة تحديد أن الخطأ هو، مثلًا، خطأ شبكة أو خطأ أمني أو خطأ تطبيق، وتمكين المستدعي من تمييز مصدر المشكلة بفحص نوع الخطأ. ومثال آخر شجرة صيغة يمكن أن تكون كل عقدة فيها نوعًا مختلفًا: تعريف أو عبارة أو إسناد وهكذا.

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

كما أن بعض ما تعالجه الأنواع المتغيرة مغطى أصلًا باللغة. فمثال الخطأ سهل التعبير باستخدام قيمة واجهة لحمل الخطأ ومبدّل نوع (type switch) لتمييز الحالات. ومثال شجرة الصيغة ممكن أيضًا، وإن لم يكن بنفس الأناقة.

لماذا لا تمتلك Go أنواع نتائج متغايرة؟

أنواع النتائج المتغايرة تعني أن واجهة مثل

type Copyable interface {
    Copy() interface{}
}

ستُحقق بواسطة الدالة

func (v Value) Copy() Value

لأن Value ينفذ الواجهة الفارغة. في Go يجب أن تطابق أنواع الدوال تمامًا، ولذلك لا ينفذ Value الواجهة Copyable. وتفصل Go بين فكرة ما يفعله النوع — أي دواله — وتنفيذ النوع. وإذا رجعت دالتان نوعين مختلفين، فهما لا تفعلان الشيء نفسه. والمبرمجون الذين يريدون أنواع نتائج متغايرة (covariant result types) يحاولون غالبًا التعبير عن تدرج هرمي للأنواع عبر الواجهات. وفي Go، من الطبيعي أكثر أن يكون هناك فصل نظيف بين الواجهة والتنفيذ.

القيم

لماذا لا توفر Go تحويلات رقمية ضمنية؟

راحة التحويل التلقائي بين الأنواع الرقمية في C تفوقها الإرباك الذي تسببه. متى يكون التعبير غير مُوقَّع؟ ما حجم القيمة؟ هل تفيض؟ هل النتيجة قابلة للنقل، مستقلة عن الآلة التي تُنفَّذ عليها؟ كما أن ذلك يعقّد المُصرِّف؛ فـ «تحويلات الحساب المعتادة» في C ليست سهلة التنفيذ وغير متسقة بين المعماريات. ولأسباب تتعلق بقابلية النقل، قررنا جعل الأمور واضحة ومباشرة على حساب بعض التحويلات الصريحة في الشيفرة. غير أن تعريف الثوابت في Go — قيم بدقة عشوائية خالية من إشارات وتوصيفات حجم — يخفف الأمور كثيرًا.

وثمة تفصيل ذو صلة هو أنه، خلافًا لـ C، يُعد int وint64 نوعين متميزين حتى لو كان int نوعًا بعرض 64 بت. ونوع int عام؛ وإذا كنت تهتم بعدد البتات التي يحملها عدد صحيح، تشجعك Go على أن تكون صريحًا.

كيف تعمل الثوابت في Go؟

رغم أن Go صارمة بشأن التحويل بين متغيرات من أنواع رقمية مختلفة، فإن الثوابت في اللغة أكثر مرونة بكثير. فالثوابت الحرفية مثل 23 و3.14159 وmath.Pi تشغل نوعًا من فضاء الأعداد المثالي، بدقة عشوائية وبلا فيض أو نقصان. على سبيل المثال، قيمة math.Pi محددة بـ 63 رقمًا عشريًا في الشيفرة المصدرية، والتعبيرات الثابتة التي تتضمن القيمة تحافظ على دقة تتجاوز ما يمكن لـ float64 حمله. وفقط عندما يُسند الثابت أو التعبير الثابت إلى متغير — أي موقع ذاكرة في البرنامج — يصبح عددًا «حاسوبيًا» بخصائص ودقة الفاصلة العائمة المعتادة.

كذلك، ولأنها مجرد أعداد وليست قيمًا ذات أنواع، يمكن استخدام الثوابت في Go بحرية أكبر من المتغيرات، ما يخفف بعض الصعوبة حول قواعد التحويل الصارمة. فيمكن للمرء كتابة تعبيرات مثل

sqrt2 := math.Sqrt(2)

دون اعتراض من المُصرِّف لأن العدد المثالي 2 يمكن تحويله بأمان ودقة إلى float64 للنداء إلى math.Sqrt.

وتستكشف تدوينة بعنوان الثوابت هذا الموضوع بمزيد من التفصيل.

لماذا الخرائط مدمجة؟

للسبب نفسه الذي جعل السلاسل النصية مدمجة: فهي بنية بيانات قوية ومهمة جدًا، وتوفير تنفيذ ممتاز واحد مع دعم صياغي يجعل البرمجة أكثر متعة. ونعتقد أن تنفيذ Go للخرائط قوي بما يكفي لخدمة الغالبية العظمى من الاستخدامات. وإذا كان تطبيق معين يمكن أن يستفيد من تنفيذ مخصص، فمن الممكن كتابته لكنه لن يكون ملائمًا صياغيًا بالقدر نفسه؛ ويبدو هذا مقايضة معقولة.

لماذا لا تسمح الخرائط بالشرائح كمفاتيح؟

يتطلب البحث في الخريطة معامل تساوٍ، وهو ما لا تنفذه الشرائح. وهي لا تنفذ التساوي لأن التساوي غير محدد جيدًا في مثل هذه الأنواع؛ فهناك اعتبارات متعددة تتعلق بالمقارنة السطحية مقابل العميقة، ومقارنة المؤشّرات مقابل القيم، وكيفية التعامل مع الأنواع التعاودية، وهكذا. قد نعيد النظر في هذه المسألة — ولن يُبطل تنفيذ التساوي للشرائح أي برامج قائمة — لكن من دون فكرة واضحة عما ينبغي أن يعنيه تساوي الشرائح، كان من الأبسط استبعاده في الوقت الحالي.

التساوي معرّف للبنى والمصفوفات، ولذلك يمكن استخدامها كمفاتيح خرائط.

لماذا تُعد الخرائط والشرائح والقنوات مراجع بينما المصفوفات قيم؟

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

كتابة الشيفرة

كيف تُوثَّق المكتبات؟

للوصول إلى التوثيق من سطر الأوامر، يمتلك أداة go أمرًا فرعيًا هو doc يوفر واجهة نصية لتوثيق التعريفات والملفات والحزم وما إلى ذلك.

صفحة اكتشاف الحزم العالمية pkg.go.dev/pkg/. تشغّل خادمًا يستخرج توثيق الحزم من شيفرة Go المصدرية في أي مكان على الويب ويقدمه بصيغة HTML مع روابط إلى التعريفات والعناصر ذات الصلة. وهي أسهل طريقة للتعلم عن مكتبات Go الموجودة.

في الأيام الأولى للمشروع، كان هناك برنامج مشابه، godoc، يمكن تشغيله أيضًا لاستخراج التوثيق للملفات على الجهاز المحلي؛ وpkg.go.dev/pkg/ هو في جوهره سليل له. وثمة سليل آخر هو الأمر pkgsite الذي يمكن، مثل godoc، تشغيله محليًا، مع أنه ليس بعد مدمجًا في النتائج التي يعرضها go doc.

هل يوجد دليل أسلوب لبرمجة Go؟

لا يوجد دليل أسلوب صريح، رغم أن هناك بالتأكيد «أسلوب Go» يمكن التعرف عليه.

وضعت Go اصطلاحات توجه القرارات المتعلقة بالتسمية والتخطيط وتنظيم الملفات. وتحتوي وثيقة Effective Go على بعض النصائح حول هذه الموضوعات. وبشكل أكثر مباشرة، فإن برنامج gofmt أداة تنسيق (pretty-printer) غرضها فرض قواعد التخطيط؛ فهو يستبدل المجموعة المعتادة من الأفعال والمحظورات التي تفسح مجالًا للتأويل. وقد مُرّرت كل شيفرة Go في المستودع، والغالبية العظمى في عالم المصادر المفتوحة، عبر gofmt.

والوثيقة المعنونة Go Code Review Comments مجموعة من المقالات القصيرة جدًا حول تفاصيل أسلوب Go التي يغفل عنها المبرمجون غالبًا. وهي مرجع مفيد لمن يراجعون الشيفرة في مشاريع Go.

كيف أرسل رقعًا إلى مكتبات Go؟

توجد مصادر المكتبة في دليل src في المستودع. وإذا أردت إجراء تغيير كبير، فيرجى المناقشة في القائمة البريدية قبل البدء.

راجع وثيقة المساهمة في مشروع Go لمزيد من المعلومات حول كيفية المضي.

لماذا يستخدم «go get» بروتوكول HTTPS عند استنساخ مستودع؟

كثيرًا ما تسمح الشركات بحركة المرور الصادرة فقط على منفذي TCP القياسيين 80 (HTTP) و443 (HTTPS)، وتمنع الحركة الصادرة على المنافذ الأخرى، بما فيها منفذ TCP رقم 9418 (git) ومنفذ TCP رقم 22 (SSH). وعند استخدام HTTPS بدلًا من HTTP، يفرض git التحقق من الشهادة افتراضيًا، ما يوفر حماية من هجمات الوسيط والتنصت والتلاعب. ولذلك يستخدم الأمر go get بروتوكول HTTPS من أجل الأمان.

يمكن ضبط Git للمصادقة عبر HTTPS أو لاستخدام SSH بدلًا من HTTPS. وللمصادقة عبر HTTPS، يمكنك إضافة سطر إلى ملف $HOME/.netrc الذي يستشيره git:

machine github.com login *USERNAME* password *APIKEY*

وبالنسبة لحسابات GitHub، يمكن أن تكون كلمة المرور رمز وصول شخصيًا.

يمكن أيضًا ضبط Git لاستخدام SSH بدلًا من HTTPS لعناوين URL المطابقة لبادئة معينة. على سبيل المثال، لاستخدام SSH لكل وصول إلى GitHub، أضف هذه الأسطر إلى ~/.gitconfig:

[url "ssh://git@github.com/"]
    insteadOf = https://github.com/

عند العمل مع وحدات خاصة، لكن باستخدام وكيل وحدات عام للاعتماديات، قد تحتاج إلى ضبط GOPRIVATE. راجع الوحدات الخاصة للتفاصيل والإعدادات الإضافية.

كيف ينبغي أن أدير إصدارات الحزم باستخدام «go get»؟

تمتلك سلسلة أدوات Go نظامًا مدمجًا لإدارة مجموعات مرتبطة ومُصدَّرة من الحزم، تُعرف بالوحدات (modules). أُدخلت الوحدات في Go 1.11 وأصبحت جاهزة للاستخدام الإنتاجي منذ 1.14.

لإنشاء مشروع باستخدام الوحدات، شغّل go mod init. ينشئ هذا الأمر ملف go.mod يتتبع إصدارات الاعتماديات.

go mod init example/project

لإضافة اعتمادية أو ترقيتها أو تخفيضها، شغّل go get:

go get golang.org/x/text@v0.3.5

راجع البرنامج التعليمي «إنشاء وحدة» لمزيد من المعلومات حول البدء.

راجع دليل «تطوير الوحدات» للحصول على إرشادات حول إدارة الاعتماديات بالوحدات.

ينبغي أن تحافظ الحزم داخل الوحدات على التوافق الخلفي مع تطورها، وفقًا لـ قاعدة توافق الاستيراد:

إذا كانت لحزمة قديمة وحزمة جديدة مسار الاستيراد نفسه، فيجب أن تكون الحزمة الجديدة متوافقة خلفيًا مع الحزمة القديمة.

وتُعد إرشادات توافق Go 1 مرجعًا جيدًا هنا: لا تحذف الأسماء المصدَّرة، وشجّع الاستخدامات الحرفية المركّبة الموسومة، وهكذا. وإذا لزمت وظيفة مختلفة، فأضف اسمًا جديدًا بدلًا من تغيير اسم قديم.

وتُقنّن الوحدات ذلك بـ الإصدار الدلالي وترقيم الاستيراد الدلالي. وإذا لزم انقطاع في التوافق، فأصدر وحدة بإصدار رئيسي جديد. وتتطلب الوحدات ذات الإصدار الرئيسي 2 أو أعلى لاحقة إصدار رئيسي كجزء من مسارها (مثل /v2). وهذا يحافظ على قاعدة توافق الاستيراد: فالحزم في إصدارات رئيسية مختلفة من وحدة لها مسارات متمايزة.

المؤشّرات والتخصيص

متى تُمرَّر وسائط الدوال حسب القيمة؟

كما في كل لغات عائلة C، يُمرَّر كل شيء في Go حسب القيمة. أي أن الدالة تحصل دائمًا على نسخة من الشيء المُمرَّر، كأن هناك عبارة إسناد تُسند القيمة إلى الوسيط. على سبيل المثال، تمرير قيمة int إلى دالة ينسخ الـ int، وتمرير قيمة مؤشّر ينسخ المؤشّر، لكن ليس البيانات التي يشير إليها. (راجع قسمًا لاحقًا لمناقشة كيف يؤثر هذا في مستقبِلات الدوال.)

تتصرف قيم الخرائط والشرائح مثل المؤشّرات: فهي واصفات (descriptors) تحتوي مؤشّرات إلى بيانات الخريطة أو الشريحة الأساسية. ونسخ قيمة خريطة أو شريحة لا ينسخ البيانات التي تشير إليها. ونسخ قيمة واجهة ينسخ الشيء المخزّن في قيمة الواجهة. فإذا كانت قيمة الواجهة تحمل بنية، فنسخ قيمة الواجهة ينسخ البنية. وإذا كانت قيمة الواجهة تحمل مؤشّرًا، فنسخ قيمة الواجهة ينسخ المؤشّر، لكن ليس البيانات التي يشير إليها مرة أخرى.

لاحظ أن هذه المناقشة تتعلق بدلالات العمليات. وقد تطبق التنفيذات الفعلية تحسينات لتجنب النسخ ما دامت التحسينات لا تغيّر الدلالات.

متى ينبغي أن أستخدم مؤشّرًا إلى واجهة؟

لا تقريبًا أبدًا. لا تنشأ المؤشّرات إلى قيم الواجهات إلا في حالات نادرة وماكرة تتضمن تمويه نوع قيمة واجهة لأجل تقييم مؤجل.

من الأخطاء الشائعة تمرير مؤشّر إلى قيمة واجهة إلى دالة تتوقع واجهة. سيشكو المُصرِّف من هذا الخطأ لكن الوضع قد يظل مربكًا، لأن المؤشّر ضروري أحيانًا لتحقيق واجهة. والخلاصة أنه، رغم أن مؤشّرًا إلى نوع ملموس يمكن أن يحقق واجهة، فإنه باستثناء واحد لا يمكن لمؤشّر إلى واجهة أن يحقق واجهة أبدًا.

تأمل تعريف المتغير،

var w io.Writer

تتخذ دالة الطباعة fmt.Fprintf وسيطها الأول قيمة تحقق io.Writer — أي شيئًا ينفذ دالة Write القانونية. ولذلك يمكننا كتابة

fmt.Fprintf(w, "hello, world\n")

لكن إذا مرّرنا عنوان w، فلن يُصرَّف البرنامج.

fmt.Fprintf(&w, "hello, world\n") // Compile-time error.

الاستثناء الوحيد هو أنه يمكن إسناد أي قيمة، حتى مؤشّر إلى واجهة، إلى متغير من نوع الواجهة الفارغة (interface{}). ومع ذلك، فمن المؤكد تقريبًا أنه خطأ إذا كانت القيمة مؤشّرًا إلى واجهة؛ فالناتج قد يكون مربكًا.

هل ينبغي أن أعرّف الدوال على القيم أم على المؤشّرات؟

func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct)  valueMethod()   { } // method on value

بالنسبة للمبرمجين غير المعتادين على المؤشّرات، قد يكون التمييز بين هذين المثالين مربكًا، لكن الوضع في الحقيقة بسيط جدًا. فعند تعريف دالة على نوع، يتصرف المستقبِل (s في المثالين أعلاه) تمامًا كما لو كان وسيطًا للدالة. ومن ثم، فإن السؤال عما إذا كان ينبغي تعريف المستقبِل كقيمة أم كمؤشّر هو السؤال نفسه عما إذا كان ينبغي أن يكون وسيط الدالة قيمة أم مؤشّرًا. وهناك عدة اعتبارات.

أولًا، والأهم، هل تحتاج الدالة إلى تعديل المستقبِل؟ إذا كان الأمر كذلك، يجب أن يكون المستقبِل مؤشّرًا. (تتصرف الشرائح والخرائط كمراجع، ولذلك قصتها أكثر دقة، لكن مثلًا لتغيير طول شريحة داخل دالة يظل المستقبِل بحاجة إلى أن يكون مؤشّرًا.) وفي المثالين أعلاه، إذا عدّلت pointerMethod حقول s، فسيرى المستدعي تلك التغييرات، أما valueMethod فتُنادى بنسخة من وسيط المستدعي (وهذا هو تعريف تمرير القيمة)، ولذلك ستكون التغييرات التي تُجريها غير مرئية للمستدعي.

بالمناسبة، في Java كانت مستقبِلات الدوال دائمًا مؤشّرات، رغم أن طبيعتها المؤشّرية مموّهة نوعًا ما (والتطورات الحديثة تجلب مستقبِلات القيم إلى Java). إن مستقبِلات القيم في Go هي غير المعتادة.

ثانيًا، اعتبار الكفاءة. إذا كان المستقبِل كبيرًا، مثل بنية ضخمة، فقد يكون استخدام مستقبِل مؤشّر أرخص.

التالي هو الاتساق. إذا كان يجب أن تمتلك بعض دوال النوع مستقبِلات مؤشّرة، فينبغي أن تمتلكها البقية أيضًا، حتى تكون مجموعة الدوال متسقة بصرف النظر عن كيفية استخدام النوع. راجع قسم مجموعات الدوال للتفاصيل.

أما بالنسبة للأنواع مثل الأنواع الأساسية والشرائح والبنى الصغيرة، فمستقبِل القيمة رخيص جدًا، ولذلك ما لم تتطلب دلالات الدالة مؤشّرًا، فمستقبِل القيمة فعّال وواضح.

ما الفرق بين new و make؟

باختصار: new تخصّص الذاكرة، بينما make تهيّئ أنواع الشرائح والخريطة والقناة.

راجع القسم ذا الصلة من Effective Go لمزيد من التفاصيل.

ما حجم int على آلة 64 بت؟

أحجام int وuint خاصة بالتنفيذ لكنها متساوية مع بعضها على منصة معينة. ولأجل قابلية النقل، ينبغي للشيفرة التي تعتمد على حجم معين من القيم أن تستخدم نوعًا محدد الحجم صراحة، مثل int64. وعلى آلات 32 بت تستخدم المُصرِّفات أعدادًا صحيحة 32 بت افتراضيًا، بينما على آلات 64 بت تكون الأعداد الصحيحة بعرض 64 بت. (تاريخيًا، لم يكن هذا صحيحًا دائمًا.)

من ناحية أخرى، تكون القيم العشرية العائمة والأنواع المركبة محددة الحجم دائمًا (لا توجد أنواع أساسية float أو complex)، لأن المبرمجين ينبغي أن يدركوا الدقة عند استخدام أعداد الفاصلة العائمة. والنوع الافتراضي المستخدم للثابت العشري (غير المصنّف) هو float64. وهكذا يعرّف foo := 3.0 متغيرًا foo من النوع float64. وبالنسبة لمتغير float32 مهيأ بثابت (غير مصنّف)، يجب تحديد نوع المتغير صراحة في تعريفه:

var foo float32 = 3.0

بدلًا من ذلك، يجب إعطاء الثابت نوعًا عبر تحويل كما في foo := float32(3.0).

كيف أعرف ما إذا كان المتغير مخصّصًا على الكومة أم على المكدّس؟

من وجهة نظر الصحة، لا تحتاج إلى معرفة ذلك. فكل متغير في Go يوجد ما دامت هناك مراجع إليه. والموقع التخزيني الذي يختاره التنفيذ لا صلة له بدلالات اللغة.

ومع ذلك، لموقع التخزين أثر في كتابة برامج فعّالة. فعند الإمكان، تخصّص مُصرِّفات Go المتغيرات المحلية لدالة في إطار مكدّس تلك الدالة. لكن إذا لم يستطع المُصرِّف إثبات أن المتغير غير مُشار إليه بعد رجوع الدالة، فيجب أن يخصّص المتغير على الكومة (heap) المُدارة بجمع النفايات لتجنب أخطاء المؤشّرات المعلّقة. كذلك، إذا كان المتغير المحلي كبيرًا جدًا، فقد يكون من المنطقي أكثر تخزينه على الكومة بدلًا من المكدّس (stack).

في المُصرِّفات الحالية، إذا أُخذ عنوان متغير، فإن ذلك المتغير مرشح للتخصيص على الكومة. غير أن تحليل الهروب (escape analysis) الأساسي يتعرف على بعض الحالات التي لن تعيش فيها هذه المتغيرات بعد رجوع الدالة ويمكن أن تبقى على المكدّس.

لماذا تستخدم عملية Go لديّ قدرًا كبيرًا من الذاكرة الافتراضية؟

يحجز مخصّص ذاكرة Go منطقة كبيرة من الذاكرة الافتراضية كساحة للتخصيصات. وهذه الذاكرة الافتراضية محلية لعملية Go المعنية؛ فالحجز لا يحرم العمليات الأخرى من الذاكرة.

لمعرفة مقدار الذاكرة الفعلية المخصّصة لعملية Go، استخدم أمر Unix top واطلع على عمودي RES (في Linux) أو RSIZE (في macOS).

التزامن

ما العمليات الذرّية؟ وماذا عن mutexes؟

يمكن العثور على وصف لذرّية العمليات في Go في وثيقة نموذج ذاكرة Go.

تتوفر بدائيات المزامنة والذرّية منخفضة المستوى في حزمتي sync وsync/atomic. وهذه الحزم جيدة لمهام بسيطة مثل زيادة أعداد المراجع أو ضمان استبعاد متبادل صغير النطاق.

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

لا تتواصلوا عبر مشاركة الذاكرة. بل شاركوا الذاكرة عبر التواصل.

راجع العرض العملي «شارك الذاكرة بالتواصل» ومقالته المرتبطة لمناقشة تفصيلية لهذا المفهوم.

من المرجح أن تستعير البرامج المتزامنة الكبيرة من كلتا المجموعتين.

لماذا لا يعمل برنامجي أسرع مع المزيد من وحدات المعالجة؟

يعتمد كون البرنامج يعمل أسرع مع المزيد من وحدات المعالجة على المشكلة التي يحلها. توفر لغة Go بدائيات التزامن، مثل كوروتينات Go والقنوات، لكن التزامن لا يتيح التوازي إلا عندما تكون المشكلة الأساسية متوازية بطبيعتها. فالمشكلات المتسلسلة بطبيعتها لا يمكن تسريعها بإضافة مزيد من وحدات المعالجة، أما تلك التي يمكن تجزئتها إلى قطع تُنفَّذ بالتوازي فيمكن تسريعها، وأحيانًا بشكل هائل.

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

لمزيد من التفاصيل حول هذا الموضوع، راجع المحاضرة المعنونة التزامن ليس توازيًا.

كيف يمكنني التحكم في عدد وحدات المعالجة؟

يتحكم متغير بيئة الصدفة GOMAXPROCS في عدد وحدات المعالجة المتاحة في الوقت نفسه لتنفيذ الكوروتينات، وقيمته الافتراضية هي عدد أنوية المعالج المتاحة. ولذلك ينبغي للبرامج القابلة للتنفيذ المتوازي أن تحقق ذلك افتراضيًا على آلة متعددة المعالجات. ولتغيير عدد وحدات المعالجة المتوازية المستخدمة، اضبط متغير البيئة أو استخدم الدالة التي تحمل الاسم نفسه في حزمة runtime لضبط دعم وقت التشغيل كي يستخدم عددًا مختلفًا من الخيوط. وضبطه على 1 يلغي إمكانية التوازي الحقيقي، ويجبر الكوروتينات المستقلة على التناوب في التنفيذ.

يمكن لبيئة التشغيل تخصيص خيوط أكثر من قيمة GOMAXPROCS لخدمة طلبات إدخال/إخراج معلّقة متعددة. ولا يؤثر GOMAXPROCS إلا في عدد الكوروتينات التي يمكنها التنفيذ فعلًا في الوقت نفسه؛ وقد يُحجب عدد كبير اعتباطًا في نداءات النظام.

تؤدي جدولة الكوروتينات (goroutine scheduler) في Go أداءً جيدًا في موازنة الكوروتينات والخيوط، ويمكنها حتى قطع تنفيذ كوروتين لضمان عدم تجويع الآخرين على الخيط نفسه. لكنها ليست مثالية. وإذا رأيت مشكلات في الأداء، فقد يساعد ضبط GOMAXPROCS على أساس كل تطبيق.

لماذا لا يوجد معرّف للكوروتين؟

لا تمتلك الكوروتينات أسماء؛ فهي مجرد عمال مجهولين. ولا تكشف للمبرمج عن أي معرّف فريد أو اسم أو بنية بيانات. ويتفاجأ بعض الناس بهذا، متوقعين أن ترجع عبارة go عنصرًا يمكن استخدامه للوصول إلى الكوروتين والتحكم فيه لاحقًا.

السبب الجوهري في كون الكوروتينات مجهولة هو أن تكون لغة Go كاملة متاحة عند برمجة شيفرة متزامنة. في المقابل، يمكن لأنماط الاستخدام التي تنشأ عند تسمية الخيوط والكوروتينات أن تقيّد ما يمكن لمكتبة تستخدمها أن تفعله.

وإليك توضيح للصعوبات. فبمجرد أن يسمّي المرء كوروتينًا ويبني حوله نموذجًا، يصبح مميزًا، ويُغرى بربط كل الحساب بذلك الكوروتين، متجاهلًا إمكانية استخدام كوروتينات متعددة وربما مشتركة للمعالجة. ولو ربطت حزمة net/http حالة خاصة بكل طلب بكوروتين، لما استطاع العملاء استخدام كوروتينات أكثر عند خدمة طلب.

علاوة على ذلك، أظهرت التجربة مع مكتبات مثل تلك الخاصة بأنظمة الرسوميات التي تشترط حدوث كل المعالجة على «الخيط الرئيسي» كم يمكن أن يكون النهج محرجًا ومقيّدًا عند نشره في لغة متزامنة. فمجرد وجود خيط أو كوروتين خاص يدفع المبرمج إلى تشويه البرنامج لتجنب الانهيارات ومشكلات أخرى ناتجة عن العمل عن غير قصد على الخيط الخاطئ.

وفي الحالات التي يكون فيها كوروتين معين مميزًا حقًا، توفر اللغة ميزات مثل القنوات يمكن استخدامها بطرق مرنة للتفاعل معه.

الدوال والدوال المرتبطة بالأنواع

لماذا يمتلك T و*T مجموعتي دوال مختلفتين؟

كما تقول مواصفات Go، تتكون مجموعة دوال النوع T من كل الدوال ذات نوع المستقبِل T، بينما تتكون مجموعة دوال نوع المؤشّر المقابل *T من كل الدوال ذات المستقبِل *T أو T. وهذا يعني أن مجموعة دوال *T تتضمن مجموعة دوال T، وليس العكس.

ينشأ هذا التمييز لأنه إذا كانت قيمة واجهة تحتوي مؤشّرًا *T، فيمكن لنداء دالة الحصول على قيمة بإلغاء مرجعية المؤشّر، لكن إذا كانت قيمة واجهة تحتوي قيمة T، فلا سبيل آمن لنداء دالة للحصول على مؤشّر. (ففعل ذلك سيتيح لدالة تعديل محتويات القيمة داخل الواجهة، وهو ما لا تسمح به مواصفات اللغة.)

وحتى في الحالات التي يستطيع فيها المُصرِّف أخذ عنوان قيمة لتمريرها إلى الدالة، إذا عدّلت الدالة القيمة فستضيع التغييرات في المستدعي.

وعلى سبيل المثال، لو كانت الشيفرة أدناه صحيحة:

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

لنسخت المدخل القياسي إلى نسخة من buf، وليس إلى buf نفسه. وهذا ليس السلوك المرغوب فيه تقريبًا أبدًا، ولذلك منعته اللغة.

ماذا يحدث مع الإغلاقات (closures) التي تعمل ككوروتينات؟

بسبب طريقة عمل متغيرات الحلقات، قبل الإصدار 1.22 من Go (راجع نهاية هذا القسم للتحديث)، كان يمكن أن ينشأ بعض الالتباس عند استخدام الإغلاقات مع التزامن. تأمل البرنامج التالي:

func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

    // wait for all goroutines to complete before exiting
    for _ = range values {
        <-done
    }
}

قد يتوقع المرء خطأً أن يرى a, b, c كمخرجات. لكن ما ستراه غالبًا بدلًا من ذلك هو c, c, c. وذلك لأن كل تكرار من الحلقة يستخدم المثيل نفسه من المتغير v، فيشترك كل إغلاق في ذلك المتغير الواحد. وعند تشغيل الإغلاق، يطبع قيمة v في وقت تنفيذ fmt.Println، لكن v ربما تغيّرت منذ إطلاق الكوروتين. وللمساعدة في كشف هذه المشكلة ومشكلات أخرى قبل حدوثها، شغّل go vet.

لربط قيمة v الحالية بكل إغلاق عند إطلاقه، يجب تعديل الحلقة الداخلية لإنشاء متغير جديد في كل تكرار. وإحدى الطرق تمرير المتغير كوسيط إلى الإغلاق:

    for _, v := range values {
        go func(u string) {
            fmt.Println(u)
            done <- true
        }(v)
    }

في هذا المثال، تُمرَّر قيمة v كوسيط إلى الدالة المجهولة. ثم تصبح هذه القيمة متاحة داخل الدالة كمتغير u.

والأسهل من ذلك إنشاء متغير جديد، بأسلوب تعريف قد يبدو غريبًا لكنه يعمل جيدًا في Go:

    for _, v := range values {
        v := v // create a new 'v'.
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

اعتُبر هذا السلوك في اللغة، أي عدم تعريف متغير جديد لكل تكرار، خطأً عند النظر إليه بأثر رجعي، وعولج في Go 1.22 الذي ينشئ فعلًا متغيرًا جديدًا لكل تكرار، ما يلغي هذه المشكلة.

التحكم في التدفق

لماذا لا تمتلك Go المعامل ?:؟

لا توجد عملية اختبار ثلاثية في Go. يمكنك استخدام ما يلي لتحقيق النتيجة نفسها:

if expr {
    n = trueVal
} else {
    n = falseVal
}

سبب غياب ?: من Go هو أن مصممي اللغة رأوا العملية تُستخدم أكثر من اللازم لإنشاء تعبيرات معقدة يتعذر فهمها. وصيغة if-else، وإن كانت أطول، أوضح بلا جدال. ولا تحتاج اللغة إلا إلى بنية واحدة للتحكم الشرطي.

وسائط الأنواع

لماذا تمتلك Go وسائط الأنواع؟

تتيح وسائط الأنواع ما يُعرف بالبرمجة العامة، حيث تُعرَّف الدوال وبنى البيانات من حيث أنواع تُحدد لاحقًا عند استخدام تلك الدوال وبنى البيانات. على سبيل المثال، تجعل من الممكن كتابة دالة ترجع الحد الأدنى من قيمتين من أي نوع مرتب، دون كتابة نسخة منفصلة لكل نوع ممكن. ولمزيد من الشرح المتعمق مع أمثلة، راجع التدوينة لماذا الأنواع العامة؟.

كيف تُنفَّذ الأنواع العامة في Go؟

يمكن للمُصرِّف أن يختار بين تصريف كل تجسيد على حدة أو تصريف التجسيدات المتشابهة كتنفيذ واحد. ويشبه نهج التنفيذ الواحد دالة بوسيط واجهة. وستتخذ المُصرِّفات المختلفة خيارات مختلفة لحالات مختلفة. ويصدر مُصرِّف Go القياسي عادة تجسيدًا واحدًا لكل وسيط نوع بالشكل نفسه، حيث يُحدد الشكل بخصائص النوع مثل الحجم وموضع المؤشّرات التي يحتويها. وقد تجرب الإصدارات المستقبلية المقايضة بين زمن التصريف وكفاءة وقت التشغيل وحجم الشيفرة.

كيف تقارن الأنواع العامة في Go نظيرتها في لغات أخرى؟

الوظائف الأساسية في كل اللغات متشابهة: من الممكن كتابة أنواع ودوال باستخدام أنواع تُحدد لاحقًا. ومع ذلك، هناك بعض الاختلافات.

  • Java في Java، يتحقق المُصرِّف من الأنواع العامة في وقت التصريف لكنه يزيل الأنواع في وقت التشغيل. ويُعرف هذا بـ محو الأنواع. على سبيل المثال، نوع Java معروف باسم List في وقت التصريف سيصبح النوع غير العام List في وقت التشغيل. وهذا يعني، على سبيل المثال، أنه عند استخدام الشكل الـ Java من انعكاس الأنواع يستحيل التمييز بين قيمة من النوع List وقيمة من النوع List. في Go تتضمن معلومات الانعكاس لنوع عام كامل معلومات النوع في وقت التصريف. وتستخدم Java رموزًا بديلة مثل List أو List لتنفيذ التغاير والتباين العام. ولا تمتلك Go هذه المفاهيم، ما يجعل الأنواع العامة في Go أبسط بكثير.
  • C++ تقليديًا لا تفرض قوالب C++ أي قيود على وسائط الأنواع، وإن كان C++20 يدعم قيودًا اختيارية عبر المفاهيم (concepts). وفي Go تكون القيود إلزامية لكل وسائط الأنواع. وتُعبَّر مفاهيم C++20 عن شيفرة صغيرة يجب أن تُصرَّف مع وسائط الأنواع. أما قيود Go فهي أنواع واجهات تحدد مجموعة كل وسائط الأنواع المسموح بها. وتدعم C++ البرمجة الوصفية للقوالب؛ أما Go فلا. وعمليًا، تصرّف كل مُصرِّفات C++ كل قالب عند نقطة تجسيده؛ وكما ذُكر أعلاه، يمكن لـ Go أن تستخدم — وهي تفعل — مناهج مختلفة لتجسيدات مختلفة.
  • Rust تُعرف نسخة القيود في Rust باسم حدود السمات (trait bounds). وفي Rust يجب تعريف الارتباط بين حد السمة ونوع ما صراحة، إما في الحزمة التي تعرّف حد السمة أو الحزمة التي تعرّف النوع. وفي Go تفي وسائط الأنواع بالقيود ضمنيًا، تمامًا كما تنفذ أنواع Go أنواع الواجهات ضمنيًا. وتعرّف مكتبة Rust القياسية سمات قياسية لعمليات مثل المقارنة أو الجمع؛ أما مكتبة Go القياسية فلا، لأن هذه يمكن التعبير عنها في شيفرة المستخدم عبر أنواع الواجهات. والاستثناء الوحيد هو واجهة Go المعرفة مسبقًا comparable، التي تلتقط خاصية لا يمكن التعبير عنها في نظام الأنواع.
  • Python ليست لغة ساكنة الأنواع، ولذلك يمكن القول بمعقولية إن كل دوال Python عامة دائمًا افتراضيًا: فيمكن دائمًا نداؤها بقيم من أي نوع، وتُكشف أي أخطاء أنواع في وقت التشغيل.

لماذا تستخدم Go الأقواس المربعة لقوائم وسائط الأنواع؟

تستخدم Java وC++ الأقواس الزاوية لقوائم وسائط الأنواع، كما في Java List وC++ std::vector. لكن ذلك الخيار لم يكن متاحًا لـ Go، لأنه يؤدي إلى مشكلة صياغية: عند تحليل الشيفرة داخل دالة، مثل v := F، وعند رؤية < يكون الأمر غامضًا: هل نرى تجسيدًا أم تعبيرًا يستخدم المعامل <. ويصعب حل هذا جدًا دون معلومات الأنواع.

على سبيل المثال، تأمل عبارة مثل

    a, b = w < x, y > (z)

دون معلومات الأنواع، يستحيل تحديد ما إذا كان الطرف الأيمن من الإسناد زوجًا من التعبيرات (w z)، أم تجسيدًا ونداءً لدالة عامة يرجع قيمتي نتيجة ((w)(z)).

من قرارات التصميم الرئيسية في Go أن يكون التحليل ممكنًا دون معلومات الأنواع، وهو ما يبدو مستحيلًا عند استخدام الأقواس الزاوية للأنواع العامة.

ليست Go فريدة أو مبتكرة في استخدام الأقواس المربعة؛ فهناك لغات أخرى مثل Scala تستخدم أيضًا الأقواس المربعة للشيفرة العامة.

لماذا لا تدعم Go الدوال ذات وسائط الأنواع؟

تسمح Go للنوع العام بامتلاك دوال، لكن وسائط تلك الدوال، عدا المستقبِل، لا يمكنها استخدام أنواع ذات وسائط. ولا نتوقع أن تضيف Go يومًا دوالً عامة.

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

type Empty struct{}

func (Empty) Nop[T any](x T) T {
    return x
}

والآن افترض أن قيمة Empty مخزّنة في any وممرَّرة إلى شيفرة أخرى تتحقق مما يمكنها فعله:

func TryNops(x any) {
    if x, ok := x.(interface{ Nop(string) string }); ok {
        fmt.Printf("string %s\n", x.Nop("hello"))
    }
    if x, ok := x.(interface{ Nop(int) int }); ok {
        fmt.Printf("int %d\n", x.Nop(42))
    }
    if x, ok := x.(interface{ Nop(io.Reader) io.Reader }); ok {
        data, err := io.ReadAll(x.Nop(strings.NewReader("hello world")))
        fmt.Printf("reader %q %v\n", data, err)
    }
}

كيف تعمل تلك الشيفرة إذا كان x من النوع Empty؟ يبدو أن x يجب أن يحقق الاختبارات الثلاثة كلها، مع أي شكل آخر بأي نوع آخر.

أي شيفرة تُنفَّذ عند نداء تلك الدوال؟ في الدوال غير العامة، يولّد المُصرِّف شيفرة كل تنفيذات الدوال ويربطها في البرنامج النهائي. لكن في الدوال العامة، يمكن أن يوجد عدد لا نهائي من تنفيذات الدوال، ولذلك تلزم استراتيجية مختلفة.

وهناك أربعة خيارات:

  1. في وقت الربط، ضع قائمة بكل فحوص الواجهات الديناميكية الممكنة، ثم ابحث عن أنواع تحققها لكن دوالها المُصرَّفة ناقصة، ثم أعد استدعاء المُصرِّف لإضافة تلك الدوال. سيجعل هذا عمليات البناء أبطأ بكثير، لأنه يتطلب التوقف بعد الربط وتكرار بعض عمليات التصريف. وسيبطئ خصوصًا عمليات البناء التزايدية. والأسوأ أنه من الممكن أن تحتوي شيفرة الدوال المُصرَّفة حديثًا نفسها على فحوص واجهات ديناميكية جديدة، فتتكرر العملية. ويمكن بناء أمثلة لا تنتهي فيها العملية أبدًا.
  2. نفّذ نوعًا من التصريف في وقت التنفيذ (JIT)، بتصريف شيفرة الدوال المطلوبة في وقت التشغيل. تستفيد Go كثيرًا من البساطة والأداء القابل للتنبؤ الناتجين عن كونها مُصرَّفة مسبقًا بالكامل. ونتردد في تحمل تعقيد JIT فقط لتنفيذ ميزة لغوية واحدة.
  3. رتّب إصدار بديل بطيء لكل دالة عامة يستخدم جدول دوال لكل عملية لغوية ممكنة على وسيط النوع، ثم استخدم ذلك التنفيذ البديل للفحوص الديناميكية. سيجعل هذا النهج دالة عامة بوسيط نوع غير متوقع أبطأ بكثير من الدالة نفسها بوسيط نوع لوحظ في وقت التصريف. وسيجعل الأداء أقل قابلية للتنبؤ بكثير.
  4. اشرط ألا تُستخدم الدوال العامة لتحقيق الواجهات على الإطلاق. الواجهات جزء أساسي من البرمجة في Go. ومن غير المقبول من وجهة نظر التصميم أن نمنع الدوال العامة من تحقيق الواجهات.

لا يعد أي من هذه الخيارات جيدًا، ولذلك اخترنا «لا شيء مما سبق».

بدلًا من الدوال ذات وسائط الأنواع، استخدم دوالًا على المستوى الأعلى بوسائط أنواع، أو أضف وسائط الأنواع إلى نوع المستقبِل.

لمزيد من التفاصيل، بما في ذلك مزيد من الأمثلة، راجع المقترح.

لماذا لا يمكنني استخدام نوع أكثر تحديدًا لمستقبِل نوع ذي وسائط؟

تُكتب تعريفات دوال النوع العام بمستقبِل يتضمن أسماء وسائط الأنواع. ولعل السبب في ذلك تشابه الصيغة المستخدمة لتحديد الأنواع في موقع النداء، فقد ظن بعضهم أن هذا يوفر آلية لإنتاج دالة مخصصة لوسائط أنواع معينة بتسمية نوع محدد في المستقبِل، مثل string:

type S[T any] struct { f T }

func (s S[string]) Add(t string) string {
    return s.f + t
}

هذا يفشل لأن الكلمة string يعتبرها المُصرِّف اسم وسيط النوع في الدالة. وستكون رسالة خطأ المُصرِّف شيئًا مثل «operator + not defined on s.f (variable of type string)». وقد يكون هذا مربكًا لأن المعامل + يعمل جيدًا على النوع المعرّف مسبقًا string، لكن التعريف استبدل تعريف string لهذه الدالة، ولا يعمل المعامل على تلك النسخة غير المرتبطة من string. ومن الجائز تجاوز اسم معرّف مسبقًا بهذه الطريقة، لكنه أمر غريب وغالبًا خطأ.

لماذا لا يستطيع المُصرِّف استنتاج وسيط النوع في برنامجي؟

هناك حالات كثيرة يستطيع فيها المبرمج أن يرى بسهولة ما يجب أن يكون عليه وسيط النوع لنوع أو دالة عامة، لكن اللغة لا تسمح للمُصرِّف باستنتاجه. واستنتاج الأنواع محدود عن قصد لضمان ألا يكون هناك أي التباس بشأن النوع المستنتج. وتشير التجربة مع لغات أخرى إلى أن استنتاج الأنواع غير المتوقع قد يؤدي إلى التباس كبير عند قراءة البرنامج وتنقيحه. ومن الممكن دائمًا تحديد وسيط النوع الصريح الذي سيُستخدم في النداء. وفي المستقبل قد تُدعم أشكال جديدة من الاستنتاج، ما دامت القواعد تبقى بسيطة وواضحة.

الحزم والاختبار

كيف أنشئ حزمة متعددة الملفات؟

ضع كل الملفات المصدرية للحزمة في دليل بمفردها. ويمكن للملفات المصدرية الإشارة إلى عناصر من ملفات مختلفة كما تشاء؛ فلا حاجة إلى تعريفات مسبقة أو ملف رأس.

وعدا عن تقسيمها إلى ملفات متعددة، ستُصرَّف الحزمة وتُختبر مثل حزمة من ملف واحد.

كيف أكتب اختبار وحدة؟

أنشئ ملفًا جديدًا ينتهي بـ _test.go في الدليل نفسه الذي توجد فيه مصادر حزمتك. وداخل ذلك الملف، اكتب import "testing" ودوالًا بالشكل التالي

func TestFoo(t *testing.T) {
    ...
}

شغّل go test في ذلك الدليل. يجد ذلك النص دوال Test، ويبني ملفًا تنفيذيًا للاختبار، ويشغّله.

راجع وثيقة How to Write Go Code، وحزمة testing والأمر الفرعي go test لمزيد من التفاصيل.

أين الدالة المساعدة المفضلة لديّ للاختبار؟

تسهّل حزمة testing القياسية في Go كتابة اختبارات الوحدات، لكنها تفتقر إلى ميزات توفرها أطر الاختبار في لغات أخرى مثل دوال التأكيد. وقد شرح قسم سابق من هذه الوثيقة لماذا لا تمتلك Go تأكيدات، وتنطبق الحجج نفسها على استخدام assert في الاختبارات. فمعالجة الأخطاء الصحيحة تعني ترك الاختبارات الأخرى تعمل بعد فشل أحدها، حتى يحصل من ينقّح الفشل على صورة كاملة عما هو خطأ. ومن المفيد أكثر أن يبلغ اختبار بأن isPrime تعطي إجابة خاطئة لـ 2 و3 و5 و7 (أو لـ 2 و4 و8 و16) من أن يبلغ بأن isPrime تعطي إجابة خاطئة لـ 2 ولذلك لم تُشغَّل اختبارات أخرى. وقد لا يكون المبرمج الذي يفعّل فشل الاختبار على دراية بالشيفرة التي تفشل. والوقت المستثمر في كتابة رسالة خطأ جيدة الآن يعود بالنفع لاحقًا عندما ينكسر الاختبار.

وهناك نقطة ذات صلة هي أن أطر الاختبار تميل إلى التطور إلى لغات صغيرة خاصة بها، بشرطيات وتحكم وآليات طباعة، لكن Go تمتلك كل هذه القدرات أصلًا؛ فلماذا نعيد إنشاءها؟ نفضل كتابة الاختبارات في Go؛ فهي لغة واحدة أقل للتعلم، ويبقي النهج الاختبارات مباشرة وسهلة الفهم.

وإذا بدا مقدار الشيفرة الإضافية المطلوب لكتابة أخطاء جيدة متكررًا وطاغيًا، فقد يعمل الاختبار بشكل أفضل إن كان مدفوعًا بجدول، بتكرار على قائمة مدخلات ومخرجات معرفة في بنية بيانات (فلدى Go دعم ممتاز لتمثيلات بنى البيانات الحرفية). عندئذٍ سيُوزَّع الجهد المبذول في كتابة اختبار جيد ورسائل خطأ جيدة على حالات اختبار كثيرة. ومكتبة Go القياسية مليئة بالأمثلة التوضيحية، كما في اختبارات التنسيق في حزمة fmt.

لماذا لا توجد X في المكتبة القياسية؟

غرض المكتبة القياسية (standard library) هو دعم مكتبة وقت التشغيل، والاتصال بنظام التشغيل، وتوفير وظائف رئيسية تحتاجها برامج Go كثيرة، مثل الإدخال/الإخراج المنسق والشبكات. كما تحتوي عناصر مهمة للبرمجة على الويب، منها التشفير ودعم معايير مثل HTTP وJSON وXML.

لا يوجد معيار واضح يحدد ما يُضمَّن، لأنه كان لفترة طويلة المكتبة الوحيدة لـ Go. لكن هناك معايير تحدد ما يُضاف اليوم.

الإضافات الجديدة إلى المكتبة القياسية نادرة، والعتبة للضم مرتفعة. فالشيفرة المضمَّنة في المكتبة القياسية تتحمل تكلفة صيانة مستمرة كبيرة (غالبًا يتحملها غير المؤلف الأصلي)، وتخضع لوعد توافق Go 1 (ما يمنع إصلاح أي عيوب في الواجهة البرمجية)، وتخضع لـ جدول إصدارات Go، ما يمنع إتاحة إصلاحات الأخطاء للمستخدمين بسرعة.

ينبغي أن تعيش معظم الشيفرة الجديدة خارج المكتبة القياسية وأن تكون متاحة عبر أمر go get في أداة go. ويمكن لهذه الشيفرة أن يكون لها صائنوها ودورة إصدارها وضمانات توافقها. ويمكن للمستخدمين العثور على الحزم وقراءة توثيقها على pkg.go.dev.

رغم وجود قطع في المكتبة القياسية لا تنتمي إليها فعلًا، مثل log/syslog، فإننا نواصل صيانة كل شيء في المكتبة بسبب وعد توافق Go 1. لكننا نشجع معظم الشيفرة الجديدة على العيش في مكان آخر.

التنفيذ

ما تقنية التصريف المستخدمة لبناء المُصرِّفات؟

هناك عدة مُصرِّفات إنتاجية لـ Go، وعدد آخر قيد التطوير لمنصات متنوعة.

المُصرِّف الافتراضي، gc، مضمّن مع توزيعة Go كجزء من دعم أمر go. وقد كُتب Gc في الأصل بلغة C بسبب صعوبات التمهيد الذاتي — إذ تحتاج إلى مُصرِّف Go لإعداد بيئة Go. لكن الأمور تقدمت، ومنذ إصدار Go 1.5 أصبح المُصرِّف برنامجًا بلغة Go. وقد حُوّل المُصرِّف من C إلى Go باستخدام أدوات ترجمة تلقائية، كما ورد في وثيقة التصميم هذه والمحاضرة. وهكذا أصبح المُصرِّف الآن «ذاتي الاستضافة» (self-hosting)، ما يعني أننا احتجنا إلى مواجهة مشكلة التمهيد الذاتي. والحل هو أن يكون لديك تثبيت Go عامل أصلًا، تمامًا كما يكون لدى المرء عادة تثبيت C عامل. وقصة كيفية إنشاء بيئة Go جديدة من المصدر موصوفة هنا وهنا.

كُتب Gc بلغة Go بمحلل نزولي تعاودي (recursive descent)، ويستخدم محمّلًا مخصصًا، مكتوبًا هو أيضًا بلغة Go لكنه مبني على محمّل Plan 9، لتوليد ملفات ELF/Mach-O/PE الثنائية.

أما مُصرِّف Gccgo فهو واجهة أمامية مكتوبة بلغة C++ بمحلل نزولي تعاودي مقترن بالخلفية القياسية لـ GCC. وتستخدم خلفية LLVM تجريبية الواجهة الأمامية نفسها.

في بداية المشروع فكرنا في استخدام LLVM لـ gc لكن قررنا أنه كبير وبطيء جدًا لتحقيق أهداف أدائنا. والأهم عند النظر بأثر رجعي أن البدء بـ LLVM كان سيجعل إدخال بعض تغييرات ABI والتغييرات المرتبطة بها، مثل إدارة المكدّس، أصعب، وهي تغييرات تحتاجها Go لكنها ليست جزءًا من إعداد C القياسي.

تبيّن أن Go لغة جيدة لتنفيذ مُصرِّف Go بها، رغم أن ذلك لم يكن هدفها الأصلي. وقد أتاح كونها غير ذاتية الاستضافة منذ البداية لتصميم Go التركيز على حالة الاستخدام الأصلية، أي الخوادم الشبكية. ولو كنا قررنا في وقت مبكر أن تصرّف Go نفسها، لربما انتهينا إلى لغة موجهة أكثر لبناء المُصرِّفات، وهو هدف جدير لكنه ليس ما بدأنا به.

مع أن gc له تنفيذه الخاص، فإن محللًا وراسمًا أصليين متاحان في حزمة go/parser، وهناك أيضًا مدقق أنواع أصلي. ويستخدم مُصرِّف gc نسخًا مختلفة من هذه المكتبات.

كيف يُنفَّذ دعم وقت التشغيل؟

مرة أخرى بسبب مسائل التمهيد الذاتي، كُتبت شيفرة وقت التشغيل في الأصل معظمها بلغة C (مع قليل جدًا من لغة التجميع) لكنها حُوّلت منذ ذلك الحين إلى Go (فيما عدا بعض أجزاء لغة التجميع). ويستخدم دعم وقت التشغيل في Gccgo مكتبة glibc. وينفذ مُصرِّف gccgo الكوروتينات باستخدام تقنية تُسمى المكدّسات المُجزَّأة (segmented stacks)، مدعومة بتعديلات حديثة على الرابط gold. وبالمثل، بُني Gollvm على البنية التحتية المقابلة في LLVM.

لماذا يكون برنامجي التافه ملفًا تنفيذيًا كبيرًا إلى هذا الحد؟

ينشئ الرابط (linker) في سلسلة أدوات gc ملفات تنفيذية مرتبطة ساكنًا افتراضيًا. ولذلك تتضمن كل ملفات Go التنفيذية بيئة تشغيل Go، إلى جانب معلومات الأنواع في وقت التشغيل اللازمة لدعم الفحوص الديناميكية للأنواع والانعكاس وحتى آثار المكدّس وقت panic.

برنامج C بسيط لـ «hello, world» مُصرَّف ومرتبط ساكنًا باستخدام gcc على Linux يبلغ حجمه نحو 750 كيلوبايت، بما في ذلك تنفيذ لـ printf. أما برنامج Go مكافئ يستخدم fmt.Printf فيبلغ وزنه بضعة ميغابايت، لكن ذلك يتضمن دعمًا أقوى لوقت التشغيل ومعلومات أنواع وتنقيح.

يمكن ربط برنامج Go مُصرَّف بـ gc بعلامة -ldflags=-w لتعطيل توليد DWARF، ما يزيل معلومات التنقيح من الملف التنفيذي دون أي خسارة أخرى في الوظائف. وقد يقلل هذا حجم الملف التنفيذي بشكل كبير.

هل يمكنني إيقاف هذه الشكاوى بشأن المتغير/الاستيراد غير المستخدم؟

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

ومع ذلك، عند تطوير الشيفرة، من الشائع إنشاء هذه الأوضاع مؤقتًا، وقد يكون من المزعج تعديلها لإزالتها قبل أن يُصرَّف البرنامج.

طلب بعضهم خيارًا في المُصرِّف لإيقاف هذه الفحوص أو تحويلها إلى تحذيرات على الأقل. لكن لم يُضف مثل هذا الخيار، لأن خيارات المُصرِّف ينبغي ألا تؤثر في دلالات اللغة، ولأن مُصرِّف Go لا يصدر تحذيرات، بل أخطاءً تمنع التصريف فقط.

وهناك سببان لعدم وجود تحذيرات. أولًا، إذا كان الأمر يستحق الشكوى منه، فهو يستحق الإصلاح في الشيفرة. (وبالمقابل، إذا لم يكن يستحق الإصلاح، فهو لا يستحق الذكر.) ثانيًا، توليد المُصرِّف تحذيرات يشجع التنفيذ على التحذير من حالات ضعيفة قد تجعل التصريف صاخبًا، فتحجب الأخطاء الحقيقية التي ينبغي إصلاحها.

لكن معالجة الوضع سهلة. استخدم المعرّف الفارغ للسماح للأشياء غير المستخدمة بالبقاء أثناء التطوير.

import "unused"

// This declaration marks the import as used by referencing an
// item from the package.
var _ = unused.Item  // TODO: Delete before committing!

func main() {
    debugData := debug.Profile()
    _ = debugData // Used only during debugging.
    ....
}

في الوقت الحاضر، يستخدم معظم مبرمجي Go أداة، goimports، تعيد كتابة ملف مصدري Go تلقائيًا ليمتلك الاستيرادات الصحيحة، ما يلغي مشكلة الاستيرادات غير المستخدمة عمليًا. ويمكن ربط هذا البرنامج بسهولة بمعظم المحررات وبيئات التطوير المتكاملة ليعمل تلقائيًا عند كتابة ملف مصدري Go. وهذه الوظيفة مدمجة أيضًا في gopls، كما نوقش أعلاه.

لماذا يظن برنامج مكافحة الفيروسات لديّ أن توزيعة Go أو الملف التنفيذي المُصرَّف مصاب؟

هذا أمر شائع، خصوصًا على آلات Windows، وهو في كل الأحوال تقريبًا إنذار كاذب. فبرامج مكافحة الفيروسات التجارية كثيرًا ما ترتبك بفعل بنية ملفات Go التنفيذية، التي لا تراها بالقدر نفسه الذي ترى فيه الملفات المُصرَّفة من لغات أخرى.

إذا كنت قد ثبّت توزيعة Go للتو وأبلغ النظام أنها مصابة، فهذا بالتأكيد خطأ. وللتحقق بشكل شامل، يمكنك التحقق من التنزيل بمقارنة المجموع الاختباري مع المجاميع الموجودة في صفحة التنزيلات.

وفي كل الأحوال، إذا كنت تعتقد أن التقرير خاطئ، فيرجى الإبلاغ عن خطأ لمورّد برنامج مكافحة الفيروسات لديك. وربما تتعلم برامج مكافحة الفيروسات مع الوقت فهم برامج Go.

الأداء

لماذا يؤدي Go أداءً سيئًا في معيار الأداء X؟

أحد أهداف تصميم Go هو الاقتراب من أداء C في البرامج المماثلة، ومع ذلك فإن أداءها ضعيف جدًا في بعض معايير الأداء، بما فيها عدة معايير في golang.org/x/exp/shootout. تعتمد أبطؤها على مكتبات لا تتوفر لها في Go نسخ ذات أداء مماثل. على سبيل المثال، يعتمد pidigits.go على حزمة رياضيات متعددة الدقة، وتستخدم نسخ C، خلافًا لنسخ Go، مكتبة GMP (المكتوبة بلغة تجميع محسّنة). أما معايير الأداء التي تعتمد على التعبيرات النمطية (مثل regex-dna.go) فهي تقارن أساسًا بين حزمة regexp الأصلية في Go ومكتبات تعبيرات نمطية ناضجة وعالية التحسين مثل PCRE.

تُكسب مسابقات معايير الأداء بالضبط المكثف، وتحتاج معظم نسخ Go من معايير الأداء إلى اهتمام. وإذا قست برامج C وGo قابلة للمقارنة فعلًا (reverse-complement.go مثال على ذلك)، فسترى أن اللغتين أقرب بكثير في الأداء الخام مما تشير إليه هذه المجموعة.

ومع ذلك، هناك مجال للتحسين. المُصرِّفات جيدة لكنها قد تكون أفضل، وكثير من المكتبات يحتاج إلى عمل كبير في الأداء، وجامع النفايات ليس سريعًا بما يكفي بعد. (وحتى لو كان كذلك، فإن الحرص على عدم توليد نفايات غير ضرورية يمكن أن يكون له أثر هائل.)

وفي كل الأحوال، غالبًا ما يمكن لـ Go أن تكون تنافسية جدًا. وقد حدث تحسن كبير في أداء برامج كثيرة مع تطور اللغة والأدوات. راجع التدوينة عن تحليل أداء برامج Go لمثال مفيد. وهي قديمة إلى حد ما لكنها لا تزال تحتوي معلومات مفيدة.

التغييرات عن C

لماذا تختلف الصيغة عن C إلى هذا الحد؟

عدا صيغة التعريف، لا تكون الاختلافات كبيرة وتنبع من رغبتين. أولًا، ينبغي أن تكون الصيغة خفيفة، دون كلمات مفتاحية إلزامية كثيرة أو تكرار أو غموض. ثانيًا، صُممت اللغة لتكون سهلة التحليل ويمكن تحليلها دون جدول رموز. وهذا يجعل بناء أدوات مثل المنقحات ومحللي الاعتماديات ومستخرجات التوثيق الآلية وإضافات بيئات التطوير المتكاملة أسهل بكثير. أما C وسلالتها فمعروفة بصعوبة ذلك.

لماذا التعريفات معكوسة؟

إنها معكوسة فقط إذا كنت معتادًا على C. في C، الفكرة أن المتغير يُعرَّف كتعبير يدل على نوعه، وهي فكرة جميلة، لكن قواعد النوع والتعبير لا تمتزج جيدًا وقد تكون النتائج مربكة؛ تأمل مؤشّرات الدوال. وتفصل Go في الغالب بين صيغتي التعبير والنوع، وهذا يبسّط الأمور (استخدام السابقة * للمؤشّرات استثناء يثبت القاعدة). في C، التعريف

    int* a, b;

يعرّف a كمؤشّر لكن ليس b؛ أما في Go

    var a, b *int

فيعرّف كليهما كمؤشّرين. وهذا أوضح وأكثر انتظامًا. كذلك فإن صيغة التعريف المختصرة := تقتضي أن يعرض تعريف المتغير الكامل الترتيب نفسه الذي تعرضه :=، لذا

    var a uint64 = 1

له الأثر نفسه الذي لـ

    a := uint64(1)

ويُبسَّط التحليل أيضًا بوجود قواعد مميزة للأنواع ليست مجرد قواعد التعبيرات؛ فكلمات مفتاحية مثل func وchan تحفظ الوضوح.

راجع المقالة عن صيغة التعريف في Go لمزيد من التفاصيل.

لماذا لا توجد حسابات على المؤشّرات؟

الأمان. فمن دون حسابات على المؤشّرات يمكن إنشاء لغة لا يمكنها أبدًا اشتقاق عنوان غير قانوني ينجح بشكل خاطئ. وقد تقدمت تقنيات المُصرِّفات والعتاد إلى حد أن حلقة تستخدم فهارس مصفوفة يمكن أن تكون بفعالية حلقة تستخدم حسابات على المؤشّرات. كذلك، يمكن لغياب الحسابات على المؤشّرات أن يبسّط تنفيذ جامع النفايات.

لماذا ++ و-- عبارتان وليستا تعبيرين؟ ولماذا لاحقة لا سابقة؟

من دون حسابات على المؤشّرات، تنخفض قيمة الراحة في معاملي الزيادة السابقة واللاحقة. وبإزالتهما من التدرج الهرمي للتعبيرات كليةً، تُبسَّط صيغة التعبيرات، وتُلغى أيضًا المسائل الفوضوية حول ترتيب تقييم ++ و-- (تأمل f(i++) وp[i] = q[++i]). والتبسيط كبير. أما اللاحقة مقابل السابقة، فكلتاهما ستعمل جيدًا لكن الصيغة اللاحقة أكثر تقليدية؛ وقد نشأ الإصرار على السابقة مع STL، المكتبة الخاصة بلغة يحتوي اسمها، من باب المفارقة، على زيادة لاحقة.

لماذا توجد أقواس لكن لا فواصل منقوطة؟ ولماذا لا يمكنني وضع قوس الفتح في السطر التالي؟

تستخدم Go الأقواس المعقوفة لتجميع العبارات، وهي صيغة مألوفة للمبرمجين الذين عملوا بأي لغة من عائلة C. أما الفواصل المنقوطة فهي للمحللات لا للناس، وأردنا إزالتها قدر الإمكان. ولتحقيق هذا الهدف، تستعير Go حيلة من BCPL: الفواصل المنقوطة التي تفصل العبارات موجودة في القواعد الرسمية لكن المحلل المعجمي يحقنها تلقائيًا، دون استشراف، في نهاية أي سطر يمكن أن يكون نهاية عبارة. وهذا يعمل جيدًا جدًا في الممارسة لكن أثره أنه يفرض أسلوبًا للأقواس. على سبيل المثال، لا يمكن لقوس فتح دالة أن يظهر في سطر بمفرده.

قال بعضهم إنه ينبغي للمحلل المعجمي أن يستشرف للسماح للقوس بالعيش في السطر التالي. ولا نوافق. فبما أن الشيفرة في Go يفترض أن تُنسَّق تلقائيًا بواسطة gofmt، فيجب اختيار أسلوب ما. وقد يختلف ذلك الأسلوب عما استخدمته في C أو Java، لكن Go لغة مختلفة وأسلوب gofmt جيد كأي أسلوب آخر. والأهم — بل الأهم كثيرًا — أن مزايا صيغة واحدة مفروضة برمجيًا على كل برامج Go تفوق بكثير أي عيوب متصورة للأسلوب المعين. ولاحظ أيضًا أن أسلوب Go يعني أن التنفيذ التفاعلي لـ Go يمكنه استخدام الصيغة القياسية سطرًا واحدًا في كل مرة دون قواعد خاصة.

لماذا نستخدم جمع النفايات؟ ألن يكون مكلفًا أكثر من اللازم؟

أحد أكبر مصادر الأعمال المكتبية في برامج الأنظمة هو إدارة أعمار الكائنات المخصَّصة. وفي لغات مثل C التي يُدار فيها ذلك يدويًا، يمكن أن يستهلك قدرًا كبيرًا من وقت المبرمج ويكون غالبًا سببًا لأخطاء خبيثة. وحتى في لغات مثل C++ أو Rust التي توفر آليات للمساعدة، يمكن أن يكون لهذه الآليات أثر كبير على تصميم البرمجيات، وغالبًا تضيف عبئًا برمجيًا خاصًا بها. ورأينا أنه من الحيوي إزالة مثل هذه الأعباء على المبرمج، وقد أعطتنا التطورات في تقنية جمع النفايات في السنوات الأخيرة ثقة في أنه يمكن تنفيذها بثمن رخيص بما يكفي، وبزمن انتقال منخفض بما يكفي، ليكون نهجًا قابلًا للاستمرار للأنظمة الشبكية.

كثير من صعوبة البرمجة المتزامنة له جذور في مشكلة عمر الكائنات: فعند تمرير الكائنات بين الخيوط يصبح ضمان تحريرها بأمان أمرًا مرهقًا. ويجعل جمع النفايات التلقائي الشيفرة المتزامنة أسهل بكثير في الكتابة. وبالطبع، فإن تنفيذ جمع النفايات في بيئة متزامنة هو نفسه تحدٍّ، لكن مواجهته مرة واحدة بدلًا من مواجهتها في كل برنامج تساعد الجميع.

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

وهذا لا يعني أن العمل الحديث في لغات مثل Rust، الذي يجلب أفكارًا جديدة لمشكلة إدارة الموارد، عمل مضلَّل؛ بل نشجع هذا العمل ونتشوق لرؤية كيف يتطور. لكن Go تتبع نهجًا أكثر تقليدية بمعالجة أعمار الكائنات عبر جمع النفايات، وجمع النفايات وحده.

التنفيذ الحالي جامع علام-وكنس (mark-and-sweep). وإذا كانت الآلة متعددة المعالجات، يعمل الجامع على نواة معالج منفصلة بالتوازي مع البرنامج الرئيسي. وقد قلل العمل الكبير على الجامع في السنوات الأخيرة أزمنة التوقف غالبًا إلى نطاق أقل من ميلي ثانية، حتى بالنسبة للأكوام الكبيرة، فكاد يلغي أحد الاعتراضات الرئيسية على جمع النفايات في الخوادم الشبكية. ويستمر العمل على تحسين الخوارزمية وتقليل العبء وزمن الانتقال أكثر، واستكشاف مناهج جديدة. وتصف الكلمة الرئيسية في ISMM عام 2018 التي ألقاها Rick Hudson من فريق Go التقدم حتى الآن وتقترح بعض المناهج المستقبلية.

وفي موضوع الأداء، تذكّر أن Go تمنح المبرمج تحكمًا كبيرًا في تخطيط الذاكرة وتخصيصها، أكثر بكثير مما هو معتاد في اللغات المُدارة بجمع النفايات. ويمكن للمبرمج الحريص أن يقلل عبء جمع النفايات بشكل هائل عبر الاستخدام الجيد للغة؛ راجع المقالة عن تحليل أداء برامج Go لمثال عملي، بما في ذلك عرض لأدوات التحليل في Go.

ملاحظات الإصدار

تعرّف على الجديد في كل إصدار من Go.

عرض ملاحظات الإصدار

مدونة قواعد السلوك

إرشادات المشاركة في مساحات مجتمع Go وعملية الإبلاغ عن المشكلات.

عرض المزيد

إرشادات العلامة التجارية

إرشادات حول إعادة استخدام شعار Go وتميمة gopher وما إلى ذلك.

عرض الإرشادات

دليل المساهمة

تعرّف على كيفية الإبلاغ عن الأخطاء أو إرسال طلبات السحب أو المساهمة بأي شكل آخر في منظومة Go.

عرض الدليل

تواصل معنا