Channels ותקשורת
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 60 מתוך 107.
ערוצים הם המנגנון העיקרי של Go לתקשורת בין גורוטינות. במקום לשתף זיכרון ישירות, גורוטינות שולחות ומקבלות ערכים דרך ערוצים, בהתאם לפילוסופיה של Go: "אל תתקשרו באמצעות שיתוף זיכרון; שתפו זיכרון באמצעות תקשורת."
צרו ערוץ באמצעות הפונקציה make עם מילת המפתח chan:
messages := make(chan string)השתמש באופרטור החץ <- כדי לשלוח ולקבל ערכים. החץ מצביע בכיוון זרימת הנתונים:
func main() {
messages := make(chan string)
go func() {
messages <- "hello" // שלח לערוץ
}()
msg := <-messages // קבל מהערוץ
fmt.Println(msg) // פלט: hello
}כברירת מחדל, שליחה וקבלה חוסמות עד שהצד השני מוכן. כאשר הגורוטינה שולחת "hello", היא ממתינה עד ש־main מוכן לקבל. ההתנהגות החוסמת הזו מספקת סנכרון מובנה: אין צורך ב־time.Sleep.
אפשר לסגור ערוץ כדי לסמן שלא יישלחו עוד ערכים. המקבלים יכולים לזהות זאת:
close(messages)
msg, ok := <-messages
if !ok {
fmt.Println("Channel closed")
}ערוצים הם מטיפוס מוגדר: chan string יכול לשאת רק מחרוזות. בטיחות הטיפוסים הזו מבטיחה שגורוטינות מתקשרות באמצעות סוגי הנתונים הנכונים, ומאתרת שגיאות בזמן הידור במקום בזמן ריצה.
אתגר
קלבואו נבנה מערכת להעברת הודעות שמדגימה כיצד goroutines מתקשרים באמצעות ערוצים. תיצרו צינור עיבוד שבו הודעות זורמות בין שלבי עיבוד שונים, וכל שלב פועל במקביל.
תארגנו את הקוד שלכם בשני קבצים:
relay.go: הגדירו את צינור עיבוד ההודעות שלכם.צרו struct בשם
Messageעם השדותID(int) ו-Content(string).ממשו שלוש פונקציות שמייצגות שלבים שונים בצינור העיבוד שלכם:
Producer(messages []Message, out chan Message)- מקבלת פרוסה של הודעות ושולחת כל אחת מהן לערוץ הפלט. לאחר שליחת כל ההודעות, סגרו את הערוץ כדי לאותת על השלמת הפעולה.Transformer(in chan Message, out chan Message)- מקבלת הודעות מערוץ הקלט, ממירה את התוכן של כל הודעה לאותיות גדולות ושולחת את ההודעה שעברה המרה לערוץ הפלט. כאשר ערוץ הקלט נסגר (מזהים זאת באמצעות צורת הקבלה בעלת שני הערכים), סגרו את ערוץ הפלט.Consumer(in chan Message) []string- מקבלת את כל ההודעות מערוץ הקלט ואוספת אותן לפרוסה של מחרוזות בפורמט מוגדר. כל מחרוזת צריכה להיות בפורמט:Message [ID]: [Content]. החזירו את הפרוסה כשהערוץ נסגר.
main.go: הגדירו את צינור העיבוד ותזמרו את זרימת ההודעות המקבילית.קראו את מספר ההודעות, ואז קראו את המזהה והתוכן של כל הודעה. צרו את ההודעות והגדירו שני ערוצים שיחברו בין שלושת שלבי הצינור. הפעילו את Producer ואת Transformer כ-goroutines, ואז הריצו את Consumer ב-goroutine הראשי כדי לאסוף את התוצאות. הדפיסו כל תוצאה בשורה נפרדת.
הקלטים הבאים יסופקו:
- שורה 1: מספר ההודעות (מספר שלם)
- השורות הבאות: עבור כל הודעה, שתי שורות — מזהה ההודעה (מספר שלם), ואז התוכן שלה (מחרוזת)
לדוגמה, בהינתן:
2
1
hello world
2
go channelsהפלט שלכם צריך להיות:
Message 1: HELLO WORLD
Message 2: GO CHANNELSצינור העיבוד פועל כך: Producer שולח הודעות → Transformer ממיר אותן לאותיות גדולות → Consumer אוסף ומעצב את התוצאות. כל חץ מייצג ערוץ שמחבר בין שלבים מקבילים. השתמשו בחבילה strings כדי לבצע את ההמרה לאותיות גדולות.
נסו בעצמכם
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// קרא את מספר ההודעות
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// קרא את ההודעות
messages := make([]Message, n)
for i := 0; i < n; i++ {
idLine, _ := reader.ReadString('\n')
id, _ := strconv.Atoi(strings.TrimSpace(idLine))
contentLine, _ := reader.ReadString('\n')
content := strings.TrimSpace(contentLine)
messages[i] = Message{ID: id, Content: content}
}
// TODO: צור שני ערוצים לחיבור שלבי הצינור
// TODO: הפעל את Producer כגורוטינה
// TODO: הפעל את Transformer כגורוטינה
// TODO: הרץ את Consumer בגורוטינה הראשית ואסוף את התוצאות
// TODO: הדפס כל תוצאה בשורה נפרדת
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
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 אונליין