io.Reader ו-io.Writer
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 73 מתוך 107.
הספרייה הסטנדרטית של Go מציגה עיצוב מבוסס ממשקים באמצעות שני ממשקים בסיסיים: io.Reader ו־io.Writer. הממשקים הפשוטים האלה עומדים בבסיס כל דבר, מפעולות קבצים ועד תקשורת רשת.
הממשק io.Reader דורש רק מתודה אחת:
type Reader interface {
Read(p []byte) (n int, err error)
}הוא קורא עד len(p) בתים לתוך הפרוסה ומחזיר כמה בתים נקראו. באופן דומה, io.Writer מגדיר:
type Writer interface {
Write(p []byte) (n int, err error)
}כל טיפוס שמממש את המתודות האלה עובד עם כל מערכת הקלט/פלט.
הנה טיפוס מותאם אישית שמממש את 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, כך שהן יוכלו לעבוד עם קבצים, חיבורי רשת, מאגרי נתונים או כל מימוש מותאם אישית.
אתגר
קלבואו נבנה מעבד זרם טקסט מותאם אישית שמממש את הממשקים io.Reader ו־io.Writer! תיצרו טיפוסים שיוכלו לשנות טקסט בזמן שהוא עובר דרכם, וכך תדגימו כיצד הממשקים הבסיסיים האלה מאפשרים להרכיב פעולות קלט/פלט עוצמתיות.
תארגנו את הקוד בשני קבצים:
streams.go: הגדירו את טיפוסי הקורא והכותב המותאמים אישית שלכם.צרו struct בשם
CountingReaderשעוטף מחרוזת ועוקב אחר מספר הבתים שנקראו. צריכים להיות בו שדות עבור מחרוזת המקור ועבור מעקב אחר המיקום. ממשו את המתודהRead(p []byte) (int, error), שקוראת בתים מהמקור אל הפרוסה שסופקה, מעדכנת את המיקום ומחזירהio.EOFכשהמקור מסתיים.צרו struct בשם
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 תעבוד עם הטיפוס המותאם אישית שלכם: זה הכוח של תכנון המבוסס על ממשקים.
נסו בעצמכם
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: הדפס את התוצאות בפורמט הנדרש:
// מקור: [input]
// אותיות גדולות: [תוצאה מהכותב]
// בתים שנקראו: [סך הבתים]
// מציין מקום למניעת שגיאה על ייבוא שאינו בשימוש - יש להסיר בעת המימוש
_ = io.EOF
fmt.Println("Original:", input)
// fmt.Println("Uppercase:", upperWriter.Result())
// fmt.Println("Bytes read:", countingReader.Position)
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP ב-Go
קבצים חיצונייםסביבת העבודה והמודולים של Goחבילות וייבואשמות מיוצאים לעומת שמות לא מיוצאיםמבוא ל-OOP ב-GoStructs כמחלקותהגדרת מתודות עבור Structsמקבלי Pointer לעומת Valueאתחול Structפונקציות Constructorחזרה – מחשבון פשוט4ממשקים
מבוא לממשקיםמימוש משתמעממשק כחוזהממשק ריק (any)אימות טיפוסמתג טיפוסיםהרכבת ממשקיםממשקי Stringer ו-Errorסיכום – מחשבון צורות7כימוס
שדות מיוצאים לעומת שדות לא מיוצאיםכימוס ברמת החבילהמתודות Getter ו-Setterהסתרת מידע ב-Goסיכום – רשומות תלמידים10Generics (Go 1.18 ומעלה)
מבוא ל־Genericsפרמטרים של טיפוסיםאילוצי טיפוסיםמבנים גנרייםפתרון עוקף למתודות גנריותסיכום – אוסף גנרי2העמקה בסוגים וב-Structs
סוגים בסיסיים ומורכביםהגדרות של סוגים מותאמים אישיתתגיות StructStructs אנונימייםStructs מקונניםערכי אפס וברירות מחדלסיכום – ספר אנשי קשר5קומפוזיציה במקום ירושה
למה ב-Go אין ירושהיסודות הטמעת Structקידום מתודותהטמעת כמה Structsהטמעה לעומת אגרגציההסתרת מתודות מוטמעותסיכום – היררכיית עובדים8טיפול בשגיאות ו-OOP
ממשק errorסוגי שגיאות מותאמים אישיתעטיפת שגיאות (fmt.Errorf)שגיאות Sentinelerrors.Is() ו-errors.As()Panic, Defer ו-Recoverסיכום – מנתח קבצים11ספרייה סטנדרטית ותכנות מונחה עצמים
io.Reader ו-io.Writersort.Interfaceהממשק fmt.Stringerencoding/json עם מבניםהממשק http.Handlerסיכום – מודלים של REST API3מצביעים וזיכרון
יסודות המצביעים ב-Goמצביעים למבניםהעברה לפי ערך לעומת לפי הפניההפונקציה new()איסוף אשפה ב-Goסיכום - בונה רשימה מקושרת6פולימורפיזם ב-Go
פולימורפיזם באמצעות ממשקיםטיפוס ברווז ב-Goכללי התאמה לממשקאוספים פולימורפייםהזרקת תלויותסיכום - מעבד תשלומים9מקביליות ו-OOP
יסודות GoroutinesChannels ותקשורתChannel עם חוצץ לעומת ללא חוצץפקודת Selectsync.Mutex ו-sync.RWMutexsync.WaitGroupתכנון Struct בטוח לשימוש במקבילסיכום - מאגר עובדים12מושגים מתקדמים בתכנות מונחה עצמים
דפוס האפשרויות הפונקציונליותדפוס Builder ב-Goשרשור מתודותכינויי טיפוסים לעומת הגדרותיסודות Reflectionסקירה כללית של יצירת קודתרגלו בעצמכם: קומפיילר Go אונליין