4 انتقادات لغوية

نشرت: 2022-03-11

Go (المعروفة أيضًا باسم Golang) هي إحدى اللغات الأكثر اهتمامًا بها. اعتبارًا من أبريل 2018 ، احتلت المرتبة 19 في مؤشر TIOBE. يقوم المزيد والمزيد من الأشخاص بالتبديل من PHP و Node.js ولغات أخرى لاستخدامها في الإنتاج. تمت كتابة الكثير من البرامج الرائعة (مثل Kubernetes و Docker و Heroku CLI) باستخدام Go.

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

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

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

انتقادات اللغة 4 Go

1. الافتقار إلى التحميل الزائد على الوظيفة والقيم الافتراضية للوسائط

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

 func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }

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

سأحصل على مجموعة من "غير محدد"

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

كيف يمكن تحسينها؟

هذا هو نفس المثال (حسنًا ، تقريبًا نفس) في JavaScript:

 function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }

كما ترى ، تبدو أكثر وضوحا.

أنا أيضا أحب نهج الإكسير في ذلك. إليك كيف سيبدو في Elixir (أعلم أنه يمكنني استخدام القيم الافتراضية ، كما في المثال أعلاه - أنا فقط أعرضها كطريقة يمكن القيام بها):

 defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")

2. الافتقار إلى علم الوراثة

يمكن القول أن هذه هي الميزة التي يطلبها مستخدمو Go أكثر من غيرها.

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

لنفعل ذلك مع الأعداد الصحيحة:

 package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }

تبدو جيدة ، أليس كذلك؟

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

 func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }

هذا يتعارض بالتأكيد مع مبدأ DRY (لا تكرر نفسك) ، والذي ينص على أنك بحاجة إلى كتابة أقل قدر ممكن من كود النسخ / اللصق ، وبدلاً من ذلك نقله إلى الوظائف وإعادة استخدامها.

نقص الأدوية يعني مئات الوظائف المتنوعة

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

كيف يمكن تحسينها؟

هناك الكثير من اللغات الجيدة التي تتضمن دعم الأدوية الجنيسة. على سبيل المثال ، هذا هو نفس الكود في Rust (لقد استخدمت vec بدلاً من array لجعلها أبسط):

 fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }

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

3. إدارة التبعية

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

يمكنك القيام بعمل أفضل قليلاً باستخدام Dep لإدارة التبعيات (https://github.com/golang/dep) ، ولكن المشكلة هنا هي أنك إما تخزن جميع تبعياتك في المستودع الخاص بك (وهذا ليس جيدًا ، لأن المستودع الخاص بك سيفعل ذلك. لا تحتوي فقط على التعليمات البرمجية الخاصة بك ولكن تحتوي على آلاف وآلاف الأسطر من رمز التبعية) ، أو تقوم فقط بتخزين قائمة الحزم (ولكن مرة أخرى ، إذا قام مشرف التبعية بإجراء تغيير غير متوافق مع الإصدارات السابقة ، فسوف يتعطل كل شيء).

كيف يمكن تحسينها؟

أعتقد أن المثال المثالي هنا هو Node.js (وأعتقد أن JavaScript بشكل عام) و NPM. NPM هو مستودع الحزم. يخزن إصدارات مختلفة من الحزم ، لذلك إذا كنت بحاجة إلى إصدار معين من الحزمة ، فلا مشكلة - يمكنك الحصول عليه من هناك. أيضًا ، أحد الأشياء في أي تطبيق Node.js / JavaScript هو ملف package.json . هنا ، يتم سرد جميع التبعيات وإصداراتها ، بحيث يمكنك تثبيتها جميعًا (والحصول على الإصدارات التي تعمل بالتأكيد مع التعليمات البرمجية الخاصة بك) باستخدام npm install .

أيضًا ، من الأمثلة الرائعة لإدارة الحزم RubyGems / Bundler (لحزم Ruby) و Crates.io/Cargo (لمكتبات Rust).

4. معالجة الخطأ

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

 err, value := someFunction(); if err != nil { // handle it somehow }

تخيل الآن أنك بحاجة إلى كتابة دالة تقوم بثلاثة إجراءات تؤدي إلى حدوث خطأ. سيبدو شيئا من هذا القبيل:

 func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }

يوجد الكثير من الأكواد القابلة للتكرار هنا ، وهذا ليس جيدًا. ومع الوظائف الكبيرة ، يمكن أن يكون الأمر أسوأ! ستحتاج على الأرجح إلى مفتاح على لوحة المفاتيح من أجل هذا:

صورة روح الدعابة لخطأ التعامل مع رمز على لوحة المفاتيح

كيف يمكن تحسينها؟

أنا أحب نهج JavaScript في ذلك. يمكن أن تؤدي الوظيفة إلى حدوث خطأ ، ويمكنك التقاطها. تأمل المثال:

 function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }

إنها طريقة أكثر وضوحًا ولا تحتوي على رمز قابل للتكرار لمعالجة الأخطاء.

الأشياء الجيدة في Go

على الرغم من أن Go به العديد من العيوب حسب التصميم ، إلا أنه يحتوي على بعض الميزات الرائعة أيضًا.

1. Goroutines

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

 func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }

2. الأدوات المرفقة مع Go

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

  • gofmt - أداة لتحليل الكود الثابت. بالمقارنة مع JavaScript ، حيث تحتاج إلى تثبيت تبعية إضافية ، مثل eslint أو jshint ، يتم تضمينها هنا افتراضيًا. ولن يقوم البرنامج بالتجميع حتى إذا لم تكتب رمز Go-style (لا تستخدم المتغيرات المعلنة ، أو استيراد الحزم غير المستخدمة ، وما إلى ذلك).
  • go test - إطار اختبار. مرة أخرى ، بالمقارنة مع JavaScript ، تحتاج إلى تثبيت تبعيات إضافية للاختبار (Jest ، Mocha ، AVA ، إلخ). هنا ، يتم تضمينه افتراضيًا. ويتيح لك القيام بالكثير من الأشياء الرائعة افتراضيًا ، مثل قياس الأداء ، وتحويل التعليمات البرمجية في التوثيق إلى الاختبارات ، وما إلى ذلك.
  • godoc - أداة توثيق. من الجيد تضمينها في الأدوات الافتراضية.
  • المترجم نفسه. إنه سريع بشكل لا يصدق ، مقارنة باللغات الأخرى المترجمة!

3. تأجيل

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

 function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }

تبدو معقدة. هذا هو المكان الذي يأتي فيه defer Go في مكانه:

 package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }

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

خاتمة

لم أذكر كل الأشياء الجيدة والسيئة في Go ، فقط الأشياء التي أعتبرها أفضل وأسوأ الأشياء.

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

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

في غضون ذلك ، إذا كنت تحاول تحسين اختباراتك باستخدام Go ، فجرّب اختبار تطبيقك Go: ابدأ بالطريقة الصحيحة بواسطة زميلك Toptaler Gabriel Aszalos.

ذات صلة: منطق منظم جيدًا: برنامج تعليمي لـ Golang OOP