Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

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)، أو أي تنفيذ مخصص.

challenge icon

التحدي

سهل

لنقم ببناء معالج تدفق نصوص مخصص ينفذ واجهات 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)
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming