ملخص المشروع
لدي مشروع منزلي أسميته Hashtrack. هذا موقع صغير ، تطبيق مكدس كامل كتبته لإجراء مقابلة فنية. من السهل جدًا التعامل معها:
- مصادقة المستخدم (بالنظر إلى أنه قام بالفعل بإنشاء حساب لنفسه).
- يقدم علامات التجزئة التي يريد مشاهدتها تظهر على Twitter.
- ينتظر ظهور التغريدات التي تم العثور عليها مع الهاشتاج المحدد على الشاشة.
يمكنك تجربة Hashtrack هنا .
بعد الانتهاء من المقابلة ، واصلت العمل في المشروع بدافع الاهتمام الرياضي ولاحظت أنه يمكن أن يكون منصة رائعة حيث يمكنني اختبار معرفتي ومهاراتي في مجال تطوير أدوات سطر الأوامر. كان لدي خادم بالفعل ، لذلك كان علي فقط اختيار لغة يمكنني من خلالها تنفيذ مجموعة صغيرة من الإمكانات داخل واجهة برمجة التطبيقات الخاصة بمشروعي.
قدرات أداة سطر الأوامر
فيما يلي وصف للميزات الرئيسية ، على وجه الخصوص - الأوامر التي أردت تنفيذها في أداة سطر الأوامر.
hashtrack login- تسجيل الدخول إلى النظام ، أي إنشاء رمز جلسة وحفظه في نظام الملفات المحلي ، في ملف التكوين.hashtrack logout— , — , .hashtrack track <hashtag> [...]— .hashtrack untrack <hashtag> [...]— .hashtrack tracks— , .hashtrack list— 50 .hashtrack watch— .hashtrack status— , .-
--endpoint, . -
--config, . -
endpoint.
فيما يلي بعض الأشياء المهمة التي يجب مراعاتها حول الأداة الخاصة بي قبل بدء العمل عليها:
- يجب أن تستخدم واجهة برمجة تطبيقات المشروع التي تستخدم GraphQL و HTTP و WebSocket.
- يجب أن يستخدم نظام الملفات لتخزين ملف التكوين.
- يجب أن تكون قادرة على تحليل الحجج الموضعية وأعلام سطر الأوامر.
لماذا قررت استخدام Go and Rust؟
هناك العديد من اللغات التي يمكنك كتابة أدوات سطر الأوامر بها.
في هذه الحالة ، أردت اختيار لغة ليس لدي خبرة بها ، أو لغة لم يكن لدي سوى القليل من الخبرة بها. بالإضافة إلى ذلك ، كنت أرغب في العثور على شيء يمكن ترجمته بسهولة إلى رمز الجهاز ، لأن هذه إضافة إضافية لأداة سطر الأوامر.
اللغة الأولى ، وهو أمر واضح بالنسبة لي ، خطرت في بالي Go. ربما يكون هذا بسبب كتابة الكثير من أدوات سطر الأوامر التي أستخدمها في Go. لكن لدي أيضًا خبرة قليلة في برمجة Rust ، وبدا لي أن هذه اللغة ستكون مناسبة تمامًا لمشروعي.
بالتفكير في Go and Rust ، اعتقدت أنه يمكنك اختيار اللغتين. نظرًا لأن هدفي الرئيسي كان الدراسة الذاتية ، فإن مثل هذه الخطوة ستمنحني فرصة ممتازة لتنفيذ المشروع مرتين واكتشاف مزايا وعيوب كل لغة بشكل مستقل.
هنا أود أن أذكر اللغات Crystal و Nim . تبدو واعدة. إنني أتطلع إلى فرصة اختبارها في مشروعي التالي.
البيئة المحلية
قبل استخدام مجموعة جديدة من الأدوات ، أنا مهتم دائمًا بقابليتها للاستخدام. أي ما إذا كان يتعين علي استخدام نوع من مدير الحزم لتثبيت البرامج عالميًا على النظام. أو ، الذي يبدو لي حلاً أكثر ملاءمة ، ما إذا كان من الممكن تثبيت كل شيء بناءً على حساب المستخدم. نحن نتحدث عن مديري الإصدارات ، فهم يبسطون حياتنا ، ويركزون على تثبيت البرامج على المستخدمين ، وليس على النظام ككل. في بيئة Node.js ، تقوم NVM بهذا الأمر جيدًا .
عند العمل مع Go ، يمكنك استخدام GVM للغرض نفسه . هذا المشروع مسؤول عن تثبيت البرامج المحلية والتحكم في الإصدار. التثبيت بسيط للغاية:
gvm install go1.14 -B
gvm use go1.14
عند إعداد بيئة تطوير في Go ، يجب أن تكون على دراية بوجود متغيرين للبيئة -
GOROOTو GOPATH. يمكنك قراءة المزيد عنها هنا .
كانت المشكلة الأولى التي واجهتها باستخدام Go هي التالية. عندما حاولت فهم كيفية عمل نظام دقة الوحدة النمطية وكيفية تطبيقه
GOPATH، كان من الصعب جدًا بالنسبة لي إعداد هيكل مشروع مع بيئة تطوير محلية وظيفية.
انتهى بي الأمر باستخدام دليل المشروع فقط
GOPATH=$(pwd). كانت الإضافة الرئيسية لهذا هو أنه كان لدي نظام للعمل مع التبعيات الموجودة تحت تصرفي ، مقيدًا بإطار عمل لمشروع منفصل ، شيء من هذا القبيل node_modules. لقد كان أداء هذا النظام جيدًا.
بعد أن انتهيت من العمل على أداتي ، وجدت أن هناك مشروع فيرتوالغو من شأنه أن يساعدني في حل مشاكلي
GOPATH.
يحتوي Rust على برنامج تثبيت rustup رسمي يقوم بتثبيت مجموعة الأدوات اللازمة لاستخدام Rust. يمكن تثبيت الصدأ بأمر واحد حرفيًا. بالإضافة إلى ذلك ، عند الاستخدام
rustup، يمكننا الوصول إلى مكونات إضافية مثل خادم rls ومنسق التعليمات البرمجية rustfmt . تتطلب العديد من المشاريع إنشاءات ليلية من صندوق أدوات الصدأ. بفضل التطبيق rustup، لم أواجه مشكلة في التبديل بين الإصدارات.
دعم المحرر
أنا أستخدم VS Code وتمكنت من العثور على امتدادات تستهدف Go and Rust. يتم دعم كلتا اللغتين بشكل مثالي في المحرر.
لتصحيح شفرة Rust ، بعد هذا البرنامج التعليمي ، كنت بحاجة إلى تثبيت ملحق CodeLLDB .
إدارة الحزم
لا يحتوي نظام Go على مدير حزم أو حتى سجل رسمي. هنا يعتمد نظام دقة الوحدة على استيراد الوحدات من عناوين URL الخارجية.
يستخدم Rust مدير حزم الشحن لإدارة التبعيات ، والذي يقوم بتنزيل الحزم من crates.io من سجل حزم Rust الرسمي. في عبوات النظام البيئي الصناديق يمكن أن تكون وثائق نشرت على docs.rs .
مكتبات
كان هدفي الأول في استكشاف لغات جديدة هو معرفة مدى صعوبة تنفيذ اتصال HTTP بسيط مع خادم GraphQL باستخدام الطلبات والطفرات.
بالحديث عن Go ، تمكنت من العثور على العديد من المكتبات مثل machinebox / graphql و shurcooL / graphql . الثاني يستخدم هياكل لتنظيم البيانات وإلغاء تنظيمها. لذلك اخترتها.
لقد قمت بتقسيم shurcooL / graphql لأنني كنت بحاجة لتخصيص الرأس على العميل
Authorization. يتم تقديم التغييرات بواسطة هذا PR.
فيما يلي مثال لاستدعاء طفرة GraphQL مكتوبة في Go:
type creationMutation struct {
CreateSession struct {
Token graphql.String
} `graphql:"createSession(email: $email, password: $password)"`
}
type CreationPayload struct {
Email string
Password string
}
func Create(client *graphql.Client, payload CreationPayload) (string, error) {
var mutation creationMutation
variables := map[string]interface{}{
"email": graphql.String(payload.Email),
"password": graphql.String(payload.Password),
}
err := client.Mutate(context.Background(), &mutation, variables)
return string(mutation.CreateSession.Token), err
}
عند استخدام Rust ، كنت بحاجة إلى استخدام مكتبتين لتنفيذ استعلامات GraphQL. النقطة هنا هي أن المكتبة
graphql_clientمستقلة عن البروتوكول ، وتهدف إلى إنشاء رمز لتسلسل البيانات وإلغاء تسلسلها. لذلك ، كنت بحاجة إلى مكتبة ثانية ( reqwest) ، حيث قمت بتنظيم العمل مع طلبات HTTP.
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/createSession.graphql"
)]
struct CreateSession;
pub struct Session {
pub token: String,
}
pub type Creation = create_session::Variables;
pub async fn create(context: &Context, creation: Creation) -> Result<Session, api::Error> {
let res = api::build_base_request(context)
.json(&CreateSession::build_query(creation))
.send()
.await?
.json::<Response<create_session::ResponseData>>()
.await?;
match res.data {
Some(data) => Ok(Session {
token: data.create_session.token,
}),
_ => Err(api::Error(api::get_error_message(res).to_string())),
}
}
لم تدعم أي من مكتبات Go و Rust GraphQL عبر بروتوكول WebSocket.
في الواقع ،
graphql_clientتدعم المكتبة الاشتراكات ، ولكن نظرًا لأنها مستقلة عن البروتوكول ، فقد اضطررت إلى تنفيذ آليات تفاعل WebSocket مع GraphQL بنفسي.
لاستخدام WebSocket في إصدار Go من التطبيق ، كان لابد من تعديل المكتبة. نظرًا لأنني استخدمت بالفعل شوكة المكتبة ، لم أرغب في القيام بذلك. بدلاً من ذلك ، استخدمت طريقة مبسطة "لمشاهدة" التغريدات الجديدة. أي لتلقي تغريدات ، أرسلت طلبات API كل 5 ثوانٍ. أنا لست فخورة لأنني فعلت ذلك بالضبط .
عند كتابة برامج في Go ، يمكنك استخدام الكلمة الأساسية
goلتشغيل تيارات خفيفة تسمى goroutines. يستخدم Rust مؤشرات ترابط نظام التشغيل ، ويتم ذلك عن طريق الاتصال Thread::spawn. يتم استخدام القنوات لنقل البيانات بين التدفقات وهناك وهناك.
معالجة الخطأ
يتعامل Go مع الأخطاء بنفس الطريقة التي يعامل بها أي قيمة أخرى. الطريقة المعتادة للتعامل مع الأخطاء في Go هي التحقق من الأخطاء:
func (config *Config) Save() error {
contents, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(config.path, contents, 0o644)
if err != nil {
return err
}
return nil
}
يحتوي Rust على تعداد
Result<T, E>يتضمن القيم التي تشير إلى النجاح أو الفشل. هذا ، على التوالي ، Ok(T)و Err(E). يوجد هنا تعداد آخر Option<T>يتضمن القيم Some(T)و None. إذا كنت معتادًا على هاسكل ، فيمكنك التعرف على الأحاديات Eitherوفي هذه المعاني Maybe.
وهناك أيضا "نحوي السكر" تتعلق خطأ نشر (المشغل
?)، الذي يحل قيمة هيكل Resultإما Optionتلقائيا وعوائد Err(...)أو Noneإذا كان هناك شيء يذهب على نحو خاطئ.
pub fn save(&mut self) -> io::Result<()> {
let json = serde_json::to_string(&self.contents)?;
let mut file = File::create(&self.path)?;
file.write_all(json.as_bytes())
}
هذا الرمز يعادل الكود التالي:
pub fn save(&mut self) -> io::Result<()> {
let json = match serde_json::to_string(&self.contents) {
Ok(json) => json,
Err(e) => return Err(e.into())
};
let mut file = match File::create(&self.path) {
Ok(file) => file,
Err(e) => return Err(e.into())
};
file.write_all(json.as_bytes())
}
إذن ، لدى Rust ما يلي:
- هيكل أحادي (
OptionوResult). - دعم المشغل
?. - سمة
Fromتُستخدم لتحويل الأخطاء تلقائيًا عند انتشارها.
يمنحنا الجمع بين الميزات الثلاث المذكورة أعلاه نظام معالجة الأخطاء الذي يمكنني تسميته أفضل ما رأيته. إنه بسيط ومبسط ، ومن السهل الحفاظ على الكود المكتوب باستخدامه.
وقت الترجمة
Go هي لغة تم إنشاؤها بفكرة أن الكود المكتوب بها سيتم تجميعه في أسرع وقت ممكن. لنفحص هذا السؤال:
> time go get hashtrack #
go get hashtrack 1,39s user 0,41s system 43% cpu 4,122 total
> time go build -o hashtrack hashtrack #
go build -o hashtrack hashtrack 0,80s user 0,12s system 152% cpu 0,603 total
> time go build -o hashtrack hashtrack #
go build -o hashtrack hashtrack 0,19s user 0,07s system 400% cpu 0,065 total
> time go build -o hashtrack hashtrack #
go build -o hashtrack hashtrack 0,94s user 0,13s system 169% cpu 0,629 total
محرج. الآن دعنا نرى ما سيظهر لنا Rust:
> time cargo build
Compiling libc v0.2.67
Compiling cfg-if v0.1.10
Compiling autocfg v1.0.0
...
...
...
Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
Finished dev [unoptimized + debuginfo] target(s) in 1m 44s
cargo build 363,80s user 17,05s system 365% cpu 1:44,09 total
يتم هنا تجميع جميع التبعيات ، وهي 214 وحدة نمطية. عند إعادة تشغيل التجميع ، يكون كل شيء جاهزًا بالفعل ، لذلك يتم تنفيذ هذه المهمة على الفور تقريبًا:
> time cargo build #
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
cargo build 0,07s user 0,03s system 104% cpu 0,094 total
> time cargo build #
Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
Finished dev [unoptimized + debuginfo] target(s) in 3.15s
cargo build 3,01s user 0,52s system 111% cpu 3,162 total
كما ترى ، يستخدم Rust نموذج تجميع تزايدي. يتم إجراء إعادة ترجمة جزئية لشجرة التبعية ، بدءًا من الوحدة النمطية المعدلة وانتهاءً بالوحدات النمطية التي تعتمد عليها.
يستغرق بناء إصدار المشروع وقتًا أطول ، وهو أمر متوقع تمامًا ، نظرًا لأن المترجم يحسن الكود في هذه الحالة:
> time cargo build --release
Compiling libc v0.2.67
Compiling cfg-if v0.1.10
Compiling autocfg v1.0.0
...
...
...
Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
Finished release [optimized] target(s) in 2m 42s
cargo build --release 1067,72s user 16,95s system 667% cpu 2:42,45 total
التكامل المستمر
تظهر ميزات تجميع المشاريع المكتوبة بلغة Go and Rust ، والتي حددناها أعلاه ، وهو أمر متوقع تمامًا ، في نظام التكامل المستمر.

اذهب لمعالجة المشروع

معالجة مشروع الصدأ
استهلاك الذاكرة
لتحليل استهلاك الذاكرة للإصدارات المختلفة من أداة سطر الأوامر ، استخدمت الأمر التالي:
/usr/bin/time -v ./hashtrack list
time -vيعرض
الأمر الكثير من المعلومات المثيرة للاهتمام ، لكنني كنت مهتمًا بمقياس العملية Maximum resident set size، وهو أقصى قدر من الذاكرة الفعلية المخصصة لبرنامج أثناء تنفيذه.
هذا هو الكود الذي استخدمته لجمع بيانات استهلاك الذاكرة لإصدارات مختلفة من البرنامج:
for n in {1..5}; do
/usr/bin/time -v ./hashtrack list > /dev/null 2>> time.log
done
grep 'Maximum resident set size' time.log
فيما يلي نتائج إصدار Go:
Maximum resident set size (kbytes): 13632
Maximum resident set size (kbytes): 14016
Maximum resident set size (kbytes): 14244
Maximum resident set size (kbytes): 13648
Maximum resident set size (kbytes): 14500
هنا هو استهلاك الذاكرة لإصدار Rust من البرنامج:
Maximum resident set size (kbytes): 9840
Maximum resident set size (kbytes): 10068
Maximum resident set size (kbytes): 9972
Maximum resident set size (kbytes): 10032
Maximum resident set size (kbytes): 10072
يتم تخصيص هذه الذاكرة خلال المهام التالية:
- تفسير حجج النظام.
- تحميل وتحليل ملف التكوين من نظام الملفات.
- الوصول إلى GraphQL عبر HTTP باستخدام TLS.
- تحليل استجابة JSON.
- كتابة البيانات المنسقة إلى
stdout.
لدى Go و Rust طرق مختلفة لإدارة الذاكرة.
يحتوي Go على جامع القمامة الذي يستخدم لاكتشاف الذاكرة غير المستخدمة واستعادتها. ونتيجة لذلك ، فإن هذه المهام لا تشتت انتباه المبرمج. نظرًا لأن جامع القمامة يعتمد على خوارزميات إرشادية ، فإن استخدامه يعني دائمًا تقديم تنازلات. عادةً بين الأداء ومقدار الذاكرة المستخدمة بواسطة التطبيق.
نموذج إدارة ذاكرة Rust لديه مفاهيم مثل الملكية ، والاقتراض ، ومدى الحياة. هذا لا يساهم فقط في المعالجة الآمنة للذاكرة ، ولكنه يضمن أيضًا التحكم الكامل في الذاكرة المخصصة على الكومة دون الحاجة إلى إدارة الذاكرة اليدوية أو جمع البيانات المهملة.
للمقارنة ، دعنا نلقي نظرة على البرامج الأخرى التي تحل مشكلة مماثلة لمشكلتي.
| أمر | الحجم الأقصى للمقيمين (كيلو بايت) |
heroku apps |
56436 |
gh pr list |
26456 |
git ls-remote (مع وصول SSH) |
6448 |
git ls-remote (مع وصول HTTP) |
23488 |
أسباب اختيار Go
سأختار Go لبعض المشاريع للأسباب التالية:
- إذا كنت بحاجة إلى لغة سيكون من السهل على أعضاء فريقي تعلمها.
- إذا كنت أرغب في كتابة رمز بسيط على حساب مرونة أقل للغة.
- إذا كنت أقوم بتطوير برنامج لنظام Linux فقط ، أو إذا كان نظام التشغيل Linux هو الأكثر أهمية بالنسبة لي.
- إذا كان وقت تجميع المشاريع مهمًا.
- إذا كنت بحاجة إلى آليات ناضجة لتنفيذ التعليمات البرمجية غير المتزامنة.
أسباب اختيار Rust
فيما يلي الأسباب التي قد تقودني إلى اختيار Rust لمشروع:
- إذا كنت بحاجة إلى نظام متقدم لمعالجة الأخطاء.
- إذا كنت أرغب في الكتابة بلغة متعددة النماذج تسمح لي بكتابة كود أكثر تعبيراً مما يمكنني إنشاؤه باللغات الأخرى.
- إذا كان مشروعي يحتوي على متطلبات أمان عالية جدًا.
- إذا كان الأداء العالي أمرًا حيويًا للمشروع.
- إذا كان المشروع يستهدف العديد من أنظمة التشغيل وأود أن يكون لدي قاعدة بيانات متعددة المنصات.
تصريحات او ملاحظات عامه
Go and Rust لديهما بعض المراوغات التي لا تزال تطاردني. هذه هي:
- يركز Go على البساطة بحيث يكون لهذا السعي أحيانًا تأثير معاكس (على سبيل المثال ، كما في الحالات مع
GOROOTوGOPATH). - ما زلت لا أفهم حقًا مفهوم "العمر" في Rust. حتى محاولات العمل مع الآليات المقابلة للغة تزعجني بالتوازن.
نعم ، أريد أن أشير إلى أنه في الإصدارات الأحدث من Go ،
GOPATHلم يعد العمل مع مشكلة ، لذلك يجب أن أنقل مشروعي إلى إصدار أحدث من Go.
أستطيع أن أقول إن كلا من Go و Rust هما لغات مثيرة جدًا للتعلم. أجدهم إضافات رائعة لقدرات عالم البرمجة C / C ++. إنها تسمح لك بإنشاء تطبيقات لمجموعة متنوعة من الأغراض. على سبيل المثال ، خدمات الويب وحتى ، بفضل WebAssembly ، تطبيقات الويب من جانب العميل .
النتيجة
تعد Go و Rust أدوات رائعة مناسبة تمامًا لتطوير أدوات سطر الأوامر. لكن ، بالطبع ، كان مبتكروها يسترشدون بأولويات مختلفة. تهدف إحدى اللغات إلى جعل تطوير البرامج أمرًا بسيطًا ويمكن الوصول إليه ، بحيث يمكن الحفاظ على الرمز المكتوب بهذه اللغة. أولويات اللغة الأخرى هي العقلانية والسلامة والأداء.
إذا كنت ترغب في قراءة المزيد عن مقارنة Go vs Rust ، فقم بإلقاء نظرة على هذه المقالة. من بين أمور أخرى ، فإنه يثير مشكلة تتعلق بالمشاكل الخطيرة المتعلقة بتوافق البرامج متعددة المنصات.
ما اللغة التي ستستخدمها لتطوير أداة سطر الأوامر؟
