io.Reader و io.Writer
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 73 من 107.
تعرض مكتبة Go القياسية التصميم القائم على الواجهات من خلال واجهتين أساسيتين: io.Reader و io.Writer. تعمل هذه الواجهات البسيطة على تشغيل كل شيء بدءًا من عمليات الملفات وصولاً إلى الاتصال عبر الشبكة.
تتطلب واجهة io.Reader طريقة واحدة فقط:
type Reader interface {
Read(p []byte) (n int, err error)
}يقرأ ما يصل إلى len(p) بايتات في الـ slice ويعيد عدد البايتات التي تمت قراءتها. وبالمثل، يُعرّف io.Writer:
type Writer interface {
Write(p []byte) (n int, err error)
}أي نوع يقوم بتنفيذ هذه الأساليب يعمل مع منظومة الإدخال/الإخراج (I/O) بأكملها.
إليك نوع مخصص ينفذ io.Reader:
type RepeatReader struct {
Char byte
Count int
pos int
}
func (r *RepeatReader) Read(p []byte) (int, error) {
if r.pos >= r.Count {
return 0, io.EOF
}
n := 0
for n < len(p) && r.pos < r.Count {
p[n] = r.Char
n++
r.pos++
}
return n, nil
}الآن يعمل هذا القارئ المخصص مع أي دالة تتوقع io.Reader:
reader := &RepeatReader{Char: 'A', Count: 5}
data, _ := io.ReadAll(reader)
fmt.Println(string(data)) // AAAAAيتيح لك هذا التصميم كتابة دوال تقبل io.Reader أو io.Writer، مما يجعلها تعمل مع الملفات، أو اتصالات الشبكة، أو المخازن المؤقتة (buffers)، أو أي تنفيذ مخصص.
التحدي
سهللنقم ببناء معالج تدفق نصوص مخصص ينفذ واجهات io.Reader و io.Writer! ستقوم بإنشاء أنواع يمكنها تحويل النص أثناء تدفقه من خلالها، مما يوضح كيف تمكن هذه الواجهات الأساسية من تكوين عمليات إدخال/إخراج (I/O) قوية.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
streams.go: حدد أنواع القارئ (reader) والكاتب (writer) المخصصة لك.أنشئ هيكل
CountingReaderيغلف سلسلة نصية ويتتبع عدد البايتات التي تمت قراءتها. يجب أن يحتوي على حقول للسلسلة المصدر ومتتبع للموقع. قم بتنفيذ طريقةRead(p []byte) (int, error)التي تقرأ البايتات من المصدر إلى الشريحة (slice) المقدمة، وتحدث الموقع، وتعيدio.EOFعند استنفاد المصدر.أنشئ هيكل
UppercaseWriterيجمع البيانات المكتوبة ويحولها إلى أحرف كبيرة. يجب أن يخزن النتيجة المتراكمة داخلياً. قم بتنفيذ طريقةWrite(p []byte) (int, error)التي تحول البايتات الواردة إلى أحرف كبيرة وتخزنها. أضف طريقةResult() stringلاسترداد النص المحول للأحرف الكبيرة المتراكم.أنشئ دوال بناء
NewCountingReader(source string) *CountingReaderوNewUppercaseWriter() *UppercaseWriterلتهيئة أنواعك بشكل صحيح.main.go: استعرض أنواع الإدخال/الإخراج المخصصة لك وهي تعمل مع المكتبة القياسية.اقرأ مدخلاً نصياً، ثم استخدم
CountingReaderالخاص بك معio.ReadAllلقراءة جميع البيانات. مرر تلك البيانات عبرUppercaseWriterالخاص بك باستخدام طريقةWriteالخاصة به.اطبع النتائج بهذا التنسيق:
Original: [input] Uppercase: [result from writer] Bytes read: [total bytes]
سيتم توفير المدخلات التالية:
- سطر واحد من النص
على سبيل المثال، إذا كان المعطى:
Hello Worldيجب أن يكون مخرجك:
Original: Hello World
Uppercase: HELLO WORLD
Bytes read: 11وإذا كان المعطى:
Go interfaces are powerfulيجب أن يكون مخرجك:
Original: Go interfaces are powerful
Uppercase: GO INTERFACES ARE POWERFUL
Bytes read: 26الفكرة الأساسية هي أن CountingReader الخاص بك يعمل بسلاسة مع io.ReadAll لأنه ينفذ io.Reader. أي دالة في نظام Go البيئي تقبل io.Reader ستعمل مع نوعك المخصص — هذه هي قوة التصميم القائم على الواجهات.
ورقة مرجعية
تعد واجهات io.Reader و io.Writer في لغة Go أساسية لعمليات الإدخال والإخراج (I/O):
type Reader interface {
Read(p []byte) (n int, err error)
}type Writer interface {
Write(p []byte) (n int, err error)
}تقرأ طريقة Read ما يصل إلى len(p) بايت في الشريحة (slice) وتُرجع عدد البايتات المقروءة. أما طريقة Write فتكتب البايتات من الشريحة وتُرجع عدد البايتات المكتوبة.
أي نوع يقوم بتنفيذ هذه الطرق يعمل مع نظام Go البيئي الكامل للإدخال والإخراج. مثال على قارئ مخصص:
type RepeatReader struct {
Char byte
Count int
pos int
}
func (r *RepeatReader) Read(p []byte) (int, error) {
if r.pos >= r.Count {
return 0, io.EOF
}
n := 0
for n < len(p) && r.pos < r.Count {
p[n] = r.Char
n++
r.pos++
}
return n, nil
}استخدام القارئ المخصص مع وظائف المكتبة القياسية:
reader := &RepeatReader{Char: 'A', Count: 5}
data, _ := io.ReadAll(reader)
fmt.Println(string(data)) // AAAAAيسمح هذا التصميم للوظائف التي تقبل io.Reader أو io.Writer بالعمل مع الملفات، أو اتصالات الشبكة، أو المخازن المؤقتة (buffers)، أو أي تنفيذ مخصص آخر.
جرّب بنفسك
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// قراءة المدخلات
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
// إزالة سطر جديد في النهاية إذا وجد
if len(input) > 0 && input[len(input)-1] == '\n' {
input = input[:len(input)-1]
}
// TODO: إنشاء CountingReader باستخدام سلسلة المدخلات
// countingReader := NewCountingReader(input)
// TODO: استخدام io.ReadAll لقراءة جميع البيانات من CountingReader الخاص بك
// data, err := io.ReadAll(countingReader)
// TODO: إنشاء UppercaseWriter
// upperWriter := NewUppercaseWriter()
// TODO: كتابة البيانات إلى UppercaseWriter الخاص بك
// upperWriter.Write(data)
// TODO: طباعة النتائج بالتنسيق المطلوب:
// Original: [input]
// Uppercase: [result from writer]
// Bytes read: [total bytes]
// عنصر نائب لتجنب خطأ الاستيراد غير المستخدم - قم بإزالته عند التنفيذ
_ = io.EOF
fmt.Println("Original:", input)
// fmt.Println("Uppercase:", upperWriter.Result())
// fmt.Println("Bytes read:", countingReader.Position)
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP في Go
الملفات الخارجيةمساحة العمل والـ Modules في Goالـ Packages والـ Importsالأسماء المصدرة مقابل غير المصدرةمقدمة إلى الـ OOP في Goالـ Structs كـ Classesتعريف الـ Methods في الـ Structsالـ Pointer Receivers مقابل الـ Value Receiversتهيئة الـ Structدوال الـ Constructorمراجعة - آلة حاسبة بسيطة4الواجهات
مقدمة في الواجهاتالتنفيذ الضمنيالواجهة كعقدالواجهة الفارغة (any)تأكيد النوع (Type Assertion)تبديل النوع (Type Switch)تركيب الواجهاتواجهات Stringer و Errorمراجعة - حاسبة الأشكال7التغليف
الحقول المصدرة وغير المصدرةالتغليف على مستوى الحزمةدوال الـ Getter والـ Setterإخفاء المعلومات في Goملخص - سجلات الطلاب10الأنواع العامة (Go 1.18+)
مقدمة في الأنواع العامةمعاملات الأنواعقيود الأنواعالـ Structs العامةحل بديل للـ Methods العامةملخص - المجموعات العامة13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factoryنمط Abstract Factoryنمط Observerنمط Strategy2تعمق في الأنواع و Structs
الأنواع الأساسية والمركبةتعريفات الأنواع المخصصةStruct TagsStructs مجهولةStructs متداخلةالقيم الصفرية والافتراضيةمراجعة - دفتر العناوين5التركيب بدلاً من الوراثة
لماذا لا تدعم Go الوراثةأساسيات تضمين الـ Structترقية الـ Methodتضمين عدة Structsالتضمين مقابل التجميعحجب الـ Methods المضمنةملخص - الهيكل الهرمي للموظفين8معالجة الأخطاء و OOP
واجهة errorأنواع الأخطاء المخصصةتغليف الأخطاء (fmt.Errorf)أخطاء Sentinelerrors.Is() و errors.As()Panic و Defer و Recoverملخص - File Parser11المكتبة القياسية والبرمجة كائنية التوجه (OOP)
io.Reader و io.Writerواجهة sort.Interfaceواجهة fmt.Stringerencoding/json مع Structsواجهة http.Handlerمراجعة - نماذج REST API14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبMiddleware كنمط مزين3المؤشرات والذاكرة
أساسيات المؤشرات في Goالمؤشرات إلى الـ Structsالتمرير بالقيمة مقابل التمرير بالمرجعدالة ()newGarbage Collection في Goملخص - بناء Linked List6تعدد الأشكال في Go
تعدد الأشكال عبر InterfacesDuck Typing في Goقواعد استيفاء الـ Interfaceمجموعات متعددة الأشكالحقن التبعيةمراجعة - معالج الدفع9التزامن والبرمجة كائنية التوجه (OOP)
أساسيات الـ Goroutinesالقنوات (Channels) والاتصالالقنوات المخزنة (Buffered) مقابل غير المخزنةجملة Selectsync.Mutex و sync.RWMutexsync.WaitGroupتصميم الـ Structs الآمنة للخيوط (Thread-Safe)مراجعة - Worker Pool