ستاس أفاناسييف. جونو. خطوط الأنابيب على أساس io.Reader / io.Writer. الجزء الأول

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



ستانيسلاف أفاناسييف (فيما يلي - SA): - مساء الخير! اسمي ستاس. لقد جئت من مينسك من شركة جونو. شكرا لحضوركم في هذا اليوم الممطر ، بعد أن وجدت القوة لمغادرة المنزل.

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

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

قبل أن نبدأ ، يجب أن نجيب على السؤال ، لماذا هذه الواجهات مطلوبة على الإطلاق؟ ارفع يديك ، الذي يعمل مع Go بإحكام (كل يوم ، كل يوم) ...



عظيم! لا يزال لدينا مجتمع Go. أعتقد أن الكثير منكم قد عمل مع هذه الواجهات ، وقد سمع بها ، على الأقل. قد لا تعرف شيئًا عنهم ، ولكن من المؤكد أنه كان يجب أن تسمع شيئًا عنها.

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

io.Reader


هذه واجهة بسيطة للغاية. وتتكون من طريقة واحدة فقط - طريقة القراءة. من الناحية المفاهيمية ، يمكن أن يكون تنفيذ واجهة io.Reader اتصالاً بالشبكة - على سبيل المثال ، حيث لا توجد بيانات حتى الآن ، ولكن يمكن أن تظهر هناك:



يمكن أن يكون مخزنًا مؤقتًا في الذاكرة حيث توجد البيانات بالفعل ويمكن قراءتها بالكامل. يمكن أن يكون أيضًا واصفًا للملف - يمكننا قراءة هذا الملف في أجزاء إذا كان كبيرًا جدًا.

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

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

إرجاع أسلوب القراءة قيمتين:

  • عدد البايتات المطروحة ؛
  • خطأ إذا حدث.

تؤثر هذه القيم على السلوك الإضافي للعميل. هناك صورة gif على الشريحة تعرض وتعرض هذه العملية التي وصفتها للتو:





Io.Reader - كيف؟


هناك طريقتان تمامًا لبياناتك لتلبية واجهة Reader.



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

هناك طريقة أكثر تعقيدًا - لتنفيذ واجهة القارئ بنفسك. هناك ما يقرب من 30 سطرًا في الوثائق بقواعد صعبة ، وقيود يجب اتباعها. قبل أن نتحدث عنها جميعًا ، أصبح الأمر مثيرًا للاهتمام بالنسبة لي: "وفي أي الحالات لا تكون عمليات التنفيذ القياسية كافية (مكتبة قياسية)؟ ما هي اللحظة التي نحتاج فيها إلى تطبيق واجهة Reader بأنفسنا؟ "

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

  • الفئة الأكثر شيوعًا هي الاتصالات. هذا تنفيذ لكل من البروتوكولات والملفات الخاصة بالملكية للأنواع الموجودة. لذلك ، لدى براد فيتزباتريك مشروع Camlistore - هناك مثال في شكل statTrackingConn ، وهو ، بشكل عام ، غلاف عادي على نوع الاشتراكات من الحزمة الصافية (يضيف مقاييس لهذا النوع).
  • الفئة الثانية الأكثر شيوعًا هي المخازن المؤقتة المخصصة. هنا أعجبني المثال الوحيد: dataBuffer من حزمة x / net. تكمن خصوصيته في أنه يخزن البيانات المقطوعة إلى قطع ، وعند طرحها يمر عبر هذه القطع. إذا انتهت البيانات في القطعة ، تنتقل إلى القطعة التالية. في الوقت نفسه ، يأخذ في الاعتبار الطول والمكان الذي يمكنه ملؤه في الشريحة المرسلة.
  • فئة أخرى هي جميع أنواع أشرطة التقدم ، مع حساب عدد وحدات البايت التي يتم طرحها باستخدام المقاييس ...

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

قواعد التوثيق


كما قلت ، فإن قائمة القواعد ، والوثائق بشكل عام كبيرة جدًا وضخمة. يكفي 30 سطرًا لواجهة تتكون من ثلاثة خطوط فقط.

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



نظرًا لأن هذا عقد صارم إلى حد ما ، يمكن للعميل الوثوق بالمبلغ الذي يأتي من التنفيذ. توجد أغلفة في المكتبة القياسية (على سبيل المثال ، bytes.Buffer و bufio). هناك مثل هذه اللحظة في المكتبة القياسية: بعض عمليات التنفيذ تثق بالقراء المغلفين ، وبعضها لا يثق (سنتحدث عن هذا لاحقًا).

لا يثق Bufio بأي شيء على الإطلاق - إنه يفحص كل شيء على الإطلاق. يثق Buffer بكل ما يصل إليه. الآن سأوضح ما يحدث فيما يتعلق بهذا ...

سننظر الآن في ثلاث حالات محتملة - هذه هي ثلاثة قراء منفذة. فهي اصطناعية تمامًا ، ومفيدة للفهم. سنقرأ كل هؤلاء القراء باستخدام مساعد ReadAll. يتم تقديم توقيعه في أعلى الشريحة:



io.Reader # 1. مثال 1


ReadAll هو مساعد يأخذ نوعًا من التنفيذ لواجهة القارئ ، ويقرأه بالكامل ويعيد البيانات التي يقرأها ، بالإضافة إلى خطأ.

المثال الأول هو Reader ، الذي سيعرض دائمًا -1 ولا شيء كخطأ ، أي مثل NegativeReader. دعونا نديرها ونرى ما سيحدث:



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

ماذا حدث هنا؟ قبلت Buffer عددًا سالبًا من وحدات البايت ، ولم تتحقق من أنها سلبية ، وحاولت قطع المخزن المؤقت الداخلي على طول الحد العلوي ، الذي حصل عليه - وخرجنا من حدود الشريحة.

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

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

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

io.Reader # 1. مثال 2


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

في الواقع ، يمكن لهذا القارئ أن يسبب الكثير من المتاعب. لذا نعود إلى السؤال ، هل يجب أن نثق في القارئ؟ على سبيل المثال ، يتم تضمين الشيك في bufio: فهو يقرأ Reader بالتسلسل 100 مرة بالضبط - إذا تم إرجاع مثل هذا الزوج من القيم 100 مرة ، فإنه ببساطة يقوم بإرجاع NoProgress.

لا يوجد شيء مثل هذا بالبايت. إذا قمنا بتشغيل هذا المثال ، نحصل على حلقة لا نهائية (ReadAll يستخدم bytes.Buffer تحت الغطاء ، وليس القارئ نفسه):



io.Reader # 1. مثال 2


مثال آخر. كما أنها اصطناعية تمامًا ، ولكنها مفيدة في الفهم:



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

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

io.Reader # 2. عودة الخطأ


نأتي إلى القاعدة المهمة الثانية لتطبيق واجهة القارئ - وهذا خطأ إرجاع. توضح الوثائق ثلاثة أخطاء يجب أن يرجعها التطبيق. أهمها EOF.

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

هناك خطأ آخر يسمى UnexpectedEOF. إذا فجأة أثناء قراءة Reader لم يعد بإمكانه قراءة البيانات ، فقد كان يعتقد أنه سيعود Un يتوقع EOF. ولكن في الواقع ، يتم استخدام هذا الخطأ فقط في مكان واحد من المكتبة القياسية - في وظيفة ReadAtLeast.



خطأ آخر هو NoProgress ، الذي تحدثنا عنه بالفعل. تقول الوثائق ذلك: هذه علامة على أن الواجهة يتم تنفيذها.

Io.Reader # 3


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



يمكننا إرجاع خطأ مع عدد وحدات البايت المطروحة ، وبشكل منفصل. ولكن إذا نفدت بياناتك فجأة في القارئ الخاص بك ، ولا يمكنك إرجاع EOF [علامة النهاية] في الوقت الحالي (تعمل العديد من تطبيقات المكتبة القياسية بهذه الطريقة) ، فمن المفترض أنك ستعيد EOF إلى المكالمة التالية المتتالية (أي أنه يجب عليك تركها العميل).

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

io.Reader. أخطاء


بشكل عام ، وفقًا لقارئ Reader ، كانت هذه القواعد المهمة الرئيسية. لا تزال هناك مجموعة من المجموعات الصغيرة ، ولكنها ليست مهمة للغاية ولا تؤدي إلى مثل هذا الموقف:



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

حددت الخطأين الأكثر شيوعًا:
  • عدم إرجاع EOF مطلقًا ؛
  • الكثير من الثقة في القارئ الملفوف.

مرة أخرى ، هذه مشكلة من وجهة نظري. ومن أولئك الذين يشاهدون حزمة io ، هذه ليست مشكلة. سنتحدث عن هذا مرة أخرى.

أود أن أعود إلى فارق بسيط واحد. انظر:



يجب على العميل عدم تفسير الزوج 0 ولا شيء على أنه EOF. هذا خطأ! بالنسبة إلى Reader ، تعد هذه القيمة مجرد فرصة لترك العميل. إذاً ، يبدو الخطأان اللذان قلتهما غير مهم ، ولكن يكفي أن نتخيل أن لديك خط أنابيب متعدد الطبقات في الوخز وأن "باغول" خبيث صغير قد تسلل في المنتصف ، ثم "الضربة تحت الأرض" لن تستغرق وقتًا طويلاً - مضمونة!

وفقا لقارئ ، كل شيء في الأساس. كانت هذه قواعد التنفيذ الأساسية.

الكاتب io.


في الطرف الآخر من خطوط الأنابيب ، لدينا io.Writer ، حيث نكتب البيانات عادة. واجهة متشابهة للغاية: تتكون أيضًا من طريقة واحدة (كتابة) ، توقيعها مشابه. من وجهة نظر الدلالات ، فإن واجهة الكاتب أكثر قابلية للفهم: أود أن أقول أنه كما هو مسموع ، فإنه مكتوب.



تأخذ طريقة الكتابة شريحة بايت وتكتبها بالكامل. لديه أيضًا مجموعة من القواعد التي يجب اتباعها.

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



بواسطة القارئ والكاتب ، هذا كل شيء.

مخطط الشريان


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



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

خطوط الأنابيب


تحدثنا عن ما هو Reader ، io.Writer. الآن دعونا نتحدث عن API الموجود في المكتبة القياسية لبناء خطوط الأنابيب. هيا لنبدأ مع الأساسيات. ربما لن تكون مثيرة للاهتمام حتى لأي شخص. ومع ذلك ، هذا مهم جدا.

سنقرأ البيانات من دفق الإدخال القياسي (من Stdin):



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

نحن الآن مهتمون بـ Reader. سنقرأ Stdin باستخدام نفس المساعد ReadAll الذي استخدمناه بالفعل.

هناك فارق بسيط بشأن هذا المساعد الجدير بالملاحظة: ReadAll يقرأ Reader حتى النهاية ، ولكنه يحدد النهاية بواسطة EOF ، وفقًا لعلامة النهاية التي تحدثنا عنها.
سنحد الآن من كمية البيانات التي نقرأها من Stdin. للقيام بذلك ، هناك تطبيق LimitedReader في المكتبة القياسية:



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

يحد LimitedReader الشريحة المعطاة لها كحجة على طول الحد العلوي. ويمرر هذه الشريحة المقطوعة إلى Reader ، الذي يلفها. هذا توضيح واضح لكيفية تنظيم طول البيانات المقروءة في تطبيقات واجهة io.Reader.

خطأ في إرجاع نهاية الملف


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

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

في التكرار التالي ، يجب أن يأتي العميل مرة أخرى - بشرط أول ، سيحصل على EOF. هذه هي الحالة التي ذكرتها.

يتبع قريبا جدا ...


القليل من الدعاية :)


أشكركم على البقاء معنا. هل تحب مقالاتنا؟ هل تريد رؤية مواد أكثر إثارة للاهتمام؟ ادعمنا عن طريق تقديم طلب أو التوصية لأصدقائك VPS القائم على السحابة للمطورين من $ 4.99 ، وهو نظير فريد من نوعه لخوادم مستوى الدخول التي اخترعناها لك: الحقيقة الكاملة عن VPS (KVM) E5-2697 v3 (6 نوى) 10GB DDR4 480GB SSD 1Gbps من $ 19 أو كيفية تقسيم الخادم؟ (تتوفر الخيارات مع RAID1 و RAID10 ، حتى 24 مركزًا و 40 جيجابايت DDR4).

Dell R730xd أرخص مرتين في مركز بيانات Equinix Tier IV في أمستردام؟ فقط لدينا 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TV من 199 دولارًا في هولندا!Dell R420 - 2x E5-2430 2.2 جيجا هرتز 6C 128 جيجا بايت DDR3 2x960GB SSD 1Gbps 100TB - من 99 دولار! اقرأ عن كيفية بناء مبنى البنية التحتية الفئة c باستخدام خوادم Dell R730xd E5-2650 v4 بتكلفة 9000 يورو مقابل سنت واحد؟

All Articles