שליחה וקבלה
channel הוא צינור עם טיפוס בין goroutines. ch <- v שולח, <-ch מקבל. יוצרים channel עם make:
ערך האפס של טיפוס channel הוא nil, ולכן var ch chan string בלי make נותן לכם channel שנחסם לנצח. צרו channels תמיד עם make.
channels בלי buffer מסנכרנים
make(chan T) יוצר channel בלי buffer. שליחה נחסמת עד שמקבל לוקח את הערך, וקבלה נחסמת עד ששולח מספק ערך. שתי ה-goroutines נפגשות בנקודה הזאת, וזה הופך channel בלי buffer לכלי סנכרון לא פחות מאשר לצינור נתונים. כש-<-done חוזר בדוגמה הבאה, אתם יודעים שה-worker סיים את כל מה שעשה לפני השליחה:
קריאת result בתוך main בטוחה כאן גם בלי mutex. מודל הזיכרון של Go מבטיח שכל מה שה-worker עשה לפני השליחה נראה ל-main אחרי הקבלה המתאימה. chan struct{} הוא הטיפוס המקובל לאות טהור, כי struct{} לא תופס זיכרון.
channels עם buffer
make(chan T, n) נותן ל-channel מקום ל-n ערכים. שליחות מצליחות בלי מקבל עד שה-buffer מתמלא; קבלות מצליחות עד שהוא מתרוקן. הערכים יוצאים באותו סדר שבו נכנסו.
buffer מנתק בין השולח למקבל, כך שפרצים קצרים לא עוצרים את השולח. הוא לא פותר מצב שבו היצרן מהיר מהצרכן באופן קבוע; הוא רק דוחה את הרגע שבו השולח נחסם. בחרו גודל buffer מסיבה מסוימת (מספר השולחים, גודל אצווה ידוע), לא כדי להעלים deadlock.
len(ch) הוא תמונת מצב. עד שתפעלו לפיו goroutine אחרת כבר יכולה לשנות אותו, ולכן אל תשתמשו בו כדי להחליט אם שליחה תיחסם. לשם כך השתמשו ב-select עם מקרה default.
close ו-range
close(ch) אומר למקבלים שלא יישלחו עוד ערכים. אחרי סגירה:
- ערכים שכבר נמצאים ב-buffer עדיין נמסרים,
- אחר כך כל קבלה מחזירה מיד את ערך האפס,
v, ok := <-chמדווחok == false,for v := range chמסתיימת.
הקבלה הראשונה אחרי close עדיין מקבלת "last" עם ok == true. השנייה מקבלת את ערך האפס "" ו-false.
כללים שגורמים ל-panic:
- שליחה ל-channel סגור גורמת ל-panic עם
send on closed channel, - סגירה של channel שכבר נסגר גורמת ל-panic,
- סגירה של channel שהוא
nilגורמת ל-panic.
לכן רק צד השליחה סוגר, ורק פעם אחת. כשיש כמה שולחים, אף שולח לא יודע מתי האחרים סיימו; תנו ל-goroutine נפרדת לחכות לכל השולחים (עם sync.WaitGroup) ולסגור את ה-channel אחרי ש-Wait חוזרת. סגירה נחוצה רק כשמקבל מחכה לסוף. channel שלא נסגר ושאף אחד לא מפנה אליו נאסף על ידי ה-garbage collector כמו כל ערך אחר.
טיפוסי כיוון
פונקציה יכולה להצהיר שהיא רק שולחת ל-channel או רק מקבלת ממנו. הקומפיילר דוחה אז את הפעולה השנייה.
| טיפוס | משמעות | מותר |
|---|---|---|
chan T | דו-כיווני | שליחה, קבלה, סגירה |
chan<- T | לשליחה בלבד | שליחה, סגירה |
<-chan T | לקבלה בלבד | קבלה |
chan T מומר באופן מרומז לכל אחד מהטיפוסים המוגבלים כשמעבירים אותו לפונקציה. בגלל זה produce שלמעלה יכולה להחזיר <-chan int: הקוראים יכולים לעבור עליו עם range אבל לא לשלוח אליו או לסגור אותו. השתמשו בטיפוסי כיוון בכל פרמטר של פונקציה שבו הם רלוונטיים. הם מתעדים בעלות והופכים שימוש שגוי לשגיאת קומפילציה.
Deadlock
אם כל ה-goroutines חסומות ושום דבר לא יכול להעיר אף אחת מהן, סביבת הריצה מפילה את התוכנית:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/main.go:7 +0x38
exit status 2
ה-dump של ה-goroutines מראה איזו פעולה תקועה (כאן chan send, או chan receive, sync.WaitGroup.Wait, select). הסיבות הנפוצות:
- שליחה ל-channel בלי buffer כשאף מקבל לא רץ,
rangeעל channel שאף פעם לא נסגר,- מונה
WaitGroupשאף פעם לא מגיע לאפס, - שתי goroutines שכל אחת מחכה לשנייה.
סביבת הריצה מזהה רק את המקרה שבו כל ה-goroutines תקועות. בשרת שבו goroutines אחרות עדיין חיות (listener של HTTP, ticker), אותו באג לא מפיל כלום; ה-goroutine החסומה פשוט דולפת.
channels שהם nil
שליחה וקבלה ב-channel שהוא nil נחסמות לנצח. זה נשמע חסר תועלת, אבל זה טריק מקובל בתוך select: השמת nil למשתנה channel מבטלת את המקרה שלו. הקוד הבא ממזג שני channels ומפסיק להאזין לכל אחד מהם ברגע שהוא נסגר:
בלי ההשמות של nil, channel סגור תמיד מוכן, והלולאה הייתה מסתובבת על ערכי אפס.
Pipeline
channels מתחברים ל-pipelines: כל שלב הוא goroutine שמקבלת מ-channel אחד ושולחת לבא אחריו, וסוגרת את הפלט שלה כשהקלט שלה מסתיים.
כל שלב רץ במקביל, וסדר הפלט דטרמיניסטי (1, 16, 81) כי כל שלב הוא goroutine יחידה ששומרת על הסדר. הסגירות מתגלגלות בשרשרת: generate נסגרת, מה שמסיים את ה-range של square, שסוגרת את הפלט שלה, וכך הלאה עד main.
נקודת התורפה של ה-pipeline הזה: אם main הפסיקה לקרוא מוקדם, השלבים היו נחסמים על השליחות שלהם לנצח. pipelines אמיתיים מקבלים context.Context או channel בשם done ומבצעים עליו select לצד כל שליחה.
Channel או mutex
channels נועדו להעברת בעלות על נתונים ולאיתות על אירועים. sync.Mutex פשוט יותר להגנה על מצב משותף שהרבה goroutines קוראות ומעדכנות במקום, כמו cache או מונה. struct עם mutex בתוכו לעיתים קרובות ברור יותר מ-goroutine שמחזיקה את המצב ומשרתת בקשות דרך channels. בחרו במה שהופך את הקוד לקצר יותר ואת הבעלות לברורה.
סיכום מהיר
| פעולה | channel שהוא nil | channel פתוח | channel סגור |
|---|---|---|---|
ch <- v | נחסם לנצח | נחסם עד שהערך מתקבל או שיש מקום ב-buffer | panic |
<-ch | נחסם לנצח | נחסם עד שיש ערך זמין | ערכים מה-buffer, ואז ערך האפס |
v, ok := <-ch | נחסם לנצח | ok הוא true | ok הוא false אחרי שהתרוקן |
close(ch) | panic | סוגר | panic |
len(ch), cap(ch) | 0, 0 | ערכים ב-buffer, גודל ה-buffer | ערכים שנשארו, גודל ה-buffer |
שאלות נפוצות
מה ההבדל בין channel עם buffer ל-channel בלי buffer ב-Go?
ל-channel בלי buffer (make(chan int)) אין אחסון: שליחה נחסמת עד ש-goroutine אחרת מקבלת את הערך, כך שכל שליחה היא גם מסירה וגם נקודת סנכרון. channel עם buffer (make(chan int, 3)) מחזיק עד 3 ערכים; שליחות נחסמות רק כשה-buffer מלא, וקבלות נחסמות רק כשהוא ריק.
מה קורה כשקוראים מ-channel סגור ב-Go?
קבלה מ-channel סגור אף פעם לא נחסמת. קודם היא מרוקנת את הערכים שעוד נשארו ב-buffer, ואחר כך מחזירה את ערך האפס של טיפוס האיבר לנצח. השתמשו ב-v, ok := <-ch כדי להבחין בין המצבים: ok הוא false ברגע שה-channel סגור וריק. לולאת for v := range ch נעצרת בנקודה הזאת.
מי צריך לסגור channel ב-Go?
השולח, ורק כשהמקבלים צריכים לדעת שלא יגיעו עוד ערכים (למשל כדי לסיים לולאת range). שליחה ל-channel סגור גורמת ל-panic, וכך גם סגירה כפולה של channel, ולכן מקבל שסוגר את ה-channel נכנס למרוץ עם השולחים. לא חייבים לסגור channel כדי לשחרר אותו; ה-garbage collector מפנה channels שאין אליהם הפניה בכל מקרה.
מה המשמעות של "fatal error: all goroutines are asleep"?
כל goroutine בתוכנית חסומה על פעולת channel או על נעילה ששום דבר לא יוכל להשלים, ולכן סביבת הריצה עוצרת את התוכנית. הסיבה הנפוצה ביותר היא שליחה ל-channel בלי buffer בתוך main כשאין goroutine אחרת שמקבלת, או מעבר עם range על channel שאף פעם לא נסגר.