בחברת Gett אנו מתחילים לאחרונה לעבוד יותר ויותר עם שפת Go (לחיפוש בגוגל, חפשו: "golang" [א]).
מדוע דווקא Go?
בסיבוב הראשון (עוד מזמן) היה לנו קטע קוד ברובי - שלא רץ בצורה מספיק יעילה. ארגנו בחברה תחרות בין node.js ו elixir ו-2 מפתחים מימשו את הקוד בשפות הנ"ל. קוד ה node.js היה יעיל יותר - וזכה, אך לאורך השבועות הבאים גרם ל memory leaks שלא הצליחו לפתור. מתכנת שלישי עשה את אותו התרגיל בשפת Go - והקוד אכן היה מהיר ויציב.
מאז Go הייתה חלק מה Stack הטכנולוגי של חברת Gett, כשפה ל"מודולים שדורשים יעילות גבוהה במיוחד". השימוש ב Go היה מוגבל - עד לאחרונה שהבנו שאנו רוצים לקחת את נושא האופטימיזציות מעט קדימה.
לפני שהמשכנו להתקדם עם Go (ולהשקיע בה), שאלנו את עצמנו את השאלה: האם באמת Go היא השפה איתה אנו רוצים להתקדם? האם בחרנו אותה בהליך סביר?!
בחנו עוד אופציות: מ JRuby, סקאלה, דרך Groovy, node.js (שמקלה על המעבר מרובי) ועד Java8.
המועמדות הסופיות היו Go ו Java8.
במשפט אחד: Go היא הרכבה בין ++C ל Python.
בשני משפטים: Go היא שפת System ו General Purpose ששואבת מרכיבים מכמה שפות, בעיקר C ו Python. היא סטטית כמו C, אבל כוללת ספריות וקיצורים בשפה (כמו השמה מרובה) המקובלים ב Python - שהיא שפה "גבוהה".
מכנה משותף בין Go לג'אווה:
באופן לא סטנדרטי, שפת Go מצפה בסביבת הפיתוח למבנה תיקיות מאוד מסוים.
עבור סביבת פיתוח של גו, עליכם להגדיר את שני משתני הסביבה הבאים:
רוב הניסיונות להתחכם, ולנהל קוד Go בתיקיות שונות במערכת - יסתיימו באכזבה (שלכם). הדרך היחידה המקובלת, שאני מכיר, לנהל את הקוד בתיקיות שונות הוא לייצר symbolic links תחת GOPATH לתיקיות בהן אתם רוצים לנהל את הקוד.
תחת התיקייה GOPATH ישנן 3 תיקיות:
תחת תיקיית ה src ניתן לשים packages ישירות, אך יותר מקובל לנהל תיקיות לכל פרוייקט ("workspaces") ורק מתחתיהן התיקיות של ה packages:
כאשר נפעיל ב command line את הפקודה go install hello/web/server, הכלי לא ישאל היכן החבילה (package) נמצאת - הוא ימצא אותה בעצמו.
reference רלוונטי
הנה כמה תכונות בשפה שבוודאי יקסמו למשתמשי פייטון (וגם לאחרים):
האם הבחירות של שפת גו (טיפול בשגיאות, חוסר סלחנות על משתנים שלא בשימוש, וכו') היא ריאליזם בהתגלמותו (כי בסוף אנחנו מגיעים לשם בכל מקרה), או הגזמה? - קשה לומר עדיין. בפרספקטיבה של זמן, נוכל כנראה לומר.
אם אתם מתעניינים בשפת גו, אתם בוודאי רוצים לדעת, ומהר - קצת יותר על תכונות ה concurrency שלה, שהן כ"כ מדוברות.
אני מניח שכולנו מכירים היטב את מודל ה Threads של שפת ג'אווה, ואולי אף את מודל ה Actors שבשימוש בשפת סקאלה. גו משתמשת במודל אחר.
טיפה רקע תיאורטי:
מודל ה goroutines של שפת גו הוא בעצם מימוש של רעיון שנקרא coroutines: הבסיס ל concurrency הוא פונקציות, ולא "אובייקטים פעילים" (כמו threads).
מודל המקביליות עצמו מתבסס על מודל ה Communicating Sequential Processes (בקיצור CSP) של טוני אוהרה משנת 1978, שלא היה בשימוש נרחב עד שפת גו הציגה אותו כחלק מהשפה עצמה (ולא כספריה חיצונית).
המודל של Actors, המודל "של סקאלה" (בעצם Akka? או PID של שפת Erlang), הוא פרי עבודתו של קארל הייט מ1973.
בין המודלים של ה Actors וה CSP יש דמיון לא קטן - אבל יש גם כמה הבדלים. החפירה בנושא זה היא מעבר להישג ידו של פוסט זה, מן הסתם.
חזרה לת'כלס:
ה goroutines של שפת גו הם בעצם סוג של Green-threads, נימים (threads) המנוהלים ע"י סביבת הריצה או ה VM - ולא ע"י מערכת ההפעלה.
יתרה מכך: greenthreads לא משתמשים באף מנגנון של מערכת ההפעלה. כל הניהול הוא אפליקטיבי ומתרחש ב user space (ללא context switch ל kernel).
בואו נראה קצת קוד:
התוכנית שלנו קוראת ל-2 פונקציות: printHello ו printWorld.
בפוסט המשך, אמשיך ואצלול לתוך השפה.
שיהיה בהצלחה!
[א] השם "Go" הוא כללי מדי. מזכיר לי ספריית בדיקות ל Delphi שעבדתי איתה, שקראו לה: "Want".
אתם יכולים להבין איזה סיוט זה היה למצוא עליה משהו בגוגל?!
[ב] הפקודה go install my_package לא תתן שום פידבק שמשהו לא תקין קרה. נראה ש Go לא מוצא כזו חבילה - וחבל שהוא לא מתריע.
ניתן להשתמש בפקודה go build -v my_package לבנות את הקוד, כאשר שמות ה packages אמורים להופיע כפלט. אם לא הופיעו - סימן שמשהו בקומפילציה כשל.
מדוע דווקא Go?
בסיבוב הראשון (עוד מזמן) היה לנו קטע קוד ברובי - שלא רץ בצורה מספיק יעילה. ארגנו בחברה תחרות בין node.js ו elixir ו-2 מפתחים מימשו את הקוד בשפות הנ"ל. קוד ה node.js היה יעיל יותר - וזכה, אך לאורך השבועות הבאים גרם ל memory leaks שלא הצליחו לפתור. מתכנת שלישי עשה את אותו התרגיל בשפת Go - והקוד אכן היה מהיר ויציב.
מאז Go הייתה חלק מה Stack הטכנולוגי של חברת Gett, כשפה ל"מודולים שדורשים יעילות גבוהה במיוחד". השימוש ב Go היה מוגבל - עד לאחרונה שהבנו שאנו רוצים לקחת את נושא האופטימיזציות מעט קדימה.
לפני שהמשכנו להתקדם עם Go (ולהשקיע בה), שאלנו את עצמנו את השאלה: האם באמת Go היא השפה איתה אנו רוצים להתקדם? האם בחרנו אותה בהליך סביר?!
בחנו עוד אופציות: מ JRuby, סקאלה, דרך Groovy, node.js (שמקלה על המעבר מרובי) ועד Java8.
המועמדות הסופיות היו Go ו Java8.
- שתי השפות יכולות להתמודד עם ה throughput הגבוה שנדרש לנו.
- בשתיהן יהיה על רוב המתכנתים אצלנו ללמוד שפה (יותר מזה: סביבת-ריצה) חדשה.
- לשפת ג'אווה (החלק החשוב: ה JVM) יש הרבה יותר כלים, בשלים ומוכרים. שפת Go היא עוד מתפתחת. למשל: אין לה עדיין ספריה סטנדרטית ל New Relic, וה SDK של AWS שוחרר רק לפני כחודש.
- בשפת Go היה לנו הרבה פחות דילמות לגבי Frameworks. השפה וגם הספריות / Frameworks הזמינים בה הם דיי Low Level ושקופים למשתמש - מה שמגביר ומשפר את הלמידה. (את דעתי בנושא אפשר ללמוד בפוסט: מהו ה Framework הטוב ביותר)
חלק מהמפתחים שהתייעצנו איתם לא הרגישו נוח עם השימוש ב Java8. הרתיעה משפת ג'אווה הוא דבר נפוץ למדי בקהילת הרובי - קשה לי להסביר בדיוק למה.
הפתיחות לשפת Go הייתה שונה לחלוטין - וחיובית בעיקרה. גם בקרב המפתחים, וגם בקרב המנהלים.
בסופו של דבר, ההתאמה הארגונית - היא שהכריעה. Go היא שפה שרבים הרגישו שמתאימה יותר ל DNA הארגוני של החברה: שפה חלוצית (עדיין), פשוטה, מודרנית ומגניבה. עם החולשות שלה (שיש, כמובן) - אנחנו נתמודד ביחד, כארגון.
הערה: פוסט זה יוצא מנקודת הנחה שאתם מפתחים מנוסים, מכירים את שפת ג'אווה, ואולי קצת C ופייטון.
הפתיחות לשפת Go הייתה שונה לחלוטין - וחיובית בעיקרה. גם בקרב המפתחים, וגם בקרב המנהלים.
בסופו של דבר, ההתאמה הארגונית - היא שהכריעה. Go היא שפה שרבים הרגישו שמתאימה יותר ל DNA הארגוני של החברה: שפה חלוצית (עדיין), פשוטה, מודרנית ומגניבה. עם החולשות שלה (שיש, כמובן) - אנחנו נתמודד ביחד, כארגון.
הערה: פוסט זה יוצא מנקודת הנחה שאתם מפתחים מנוסים, מכירים את שפת ג'אווה, ואולי קצת C ופייטון.
הלוגו של שפת Go - ה Gopher (סנאי ערבה). אין קשר לפרוטוקול Gopher (המתחרה הקדום של FTP). gopher הוא גם כינוי למתכנת Go. |
הצצה ראשונית לשפת Go
מה יותר טוב מלראות מעט קוד?
הקוד (הפשוט) הזה הוא web server מינימלי שמחזיר את הטקסט "!Hello world".
- הגדרה של package בתוכנית שלנו.
הקונבנציה לשמות של package בשפת Go היא מילה בודדת ב lowercase, וללא קווים תחתונים. שם החבילה (package) אמור להתאים לשם התיקייה בו הקובץ נמצא.
אזהרה: אם לא תעקבו אחר כללים אלו, התוכנית עלולה לא להתקמפל, וללא אזהרות מיוחדות [ב]. - אנחנו מייבאים 2 חבילות סטנדרטיות של Go: לטיפול ב I/O (כללי), וטיפול ברשת.
התחביר של הסוגריים (שמייתר את הצורך בפסיק או נקודה פסיק) נקרא Grouping Declaration. - פונקציית main היא זו שממנה התוכנה מתחילה לפעול. עליה להיות שייכת גם לחבילה בשם "main".
- קריאה למתודה HandleFunc רושמת כניסה חדשה ב DefaultServeMux.
Mux, בטרמינולוגיה של Go היא כמו Router ב MVC: אובייקט שתפקידו למען בקשה לאובייקט (במקרה שלנו: פונקציה) הנכונה שתטפל בה.
אנו רושמים את ה default path (כלומר: "/") למתודה sayHello. - המתודה ListenAndServe מפעילה את "שרת הווב" של Go, ורושמת אותו ל Port ו Mux.
מכיוון שלא ציינו Mux (ה nil) - יעשה שימוש ב DefaultServeMux. - הפונקציה sayHello היא פונקציה פשוטה. עצם כך שהאות הראשונה בשמה היא אות lowercase גורם לפונקציה להיות private. ליתר דיוק: להיות זמינה רק באותה החבילה, כמו default visibility בשפת ג'אווה.
מתודה או טיפוס שמתחיל באות גדולה הוא "exported" - כלומר זמין לחבילות אחרות (כמו public בג'אווה). - אנו רואים שאנו מקבלים את הפרמטר מסוג http.Request כפויינטר (מצביע). פויינטר הוא הדרך להעביר אובייקט by reference - אך אין "pointer calculus" כמו בשפת C. כלומר: לא מבצעים פעולות חשבוניות על הכתובת שבפויינטר.
- המתודה io.WriteString מבצעת כתיבה של slice of bytes ל Writer.
לשם הפשטות, נסתפק כרגע בידיעה שמחרוזת בשפת Go היא slice of bytes שהוא read-only.
slice הוא מערך, או חלק ממערך - אכסה את הנושא של slices בפוסט המשך.
כאשר אקמפל את התוכנית אראה שנוצר קובץ בינארי של בערך 6MB.
6MB? זה לא קצת הרבה לעשר שורות קוד?
כאשר אני מצמצם את התוכנית ל ("print("hello, הקובץ קטן - אך הוא עדיין בגודל של 2MB.
הסיבה לכך היא שיש לנו static linking. הקובץ כולל את כל הספריות להן הקוד זקוק + את סביבת ה runtime של Go.
להעביר קובץ של 6MB לשרת (או אפילו 60MB) זו לא בעיה גדולה היום.
היתרון הגדול בכך הוא ש Deployment נעשה ללא התעסקות בתלויות בשרת היעד: אם יש לי קובץ בינארי שמתאים למערכת ההפעלה (תזכורת: מדובר בקובץ בשפת מכונה: הוא צריך להתאים לארכיטקטורת המעבד ומערכת ההפעלה) - אני יכול פשוט "לזרוק" אותו לאיזו תיקיה ולהריץ. זה הרבה יותר פשוט ומהיר מלהכין סביבה.
אפיון שפת Go
במשפט אחד: Go היא הרכבה בין ++C ל Python.
בשני משפטים: Go היא שפת System ו General Purpose ששואבת מרכיבים מכמה שפות, בעיקר C ו Python. היא סטטית כמו C, אבל כוללת ספריות וקיצורים בשפה (כמו השמה מרובה) המקובלים ב Python - שהיא שפה "גבוהה".
מכנה משותף בין Go לג'אווה:
- תחביר של C-Syntax
- Statically Typed
- יש Garbage collector
- memory-safe - אי אפשר לדרוס זכרון של קטע קוד אחר (כמו ב C), מצד שני יש null.
- יש interfaces וניתן לזהות אותם בעזרת instanceof.
- יש Reflection
יש דברים שונים:
- הקוד בגו מתקמפל ישר ל Machine code, ולא ל "bytecode".
- יש static linking של ספריות (כלומר: לקובץ הבינארי - כמו ++C) ולא dynamic linking (טוענים jar. בצורה דינאמית).
- יש שימוש ב pointers (אבל לא תכוף ומשמעותי כמו בשפת C).
- בגו יש מודל Concurrency מפותח יותר מג'אווה, שהוא חלק מהשפה.
- הספריות ה default שמגיעות עם השפה, מקיפות סט שימוש רחב יותר (בדיקות, עבודה עם json, תכנות ווב, וכו') מאלו של ג'אווה. זה דיי מפתיע, כי גם לג'אווה יש סט עשיר למדי של ספריות.
בכדי לשמור על השפה פשוטה, וויתרו במתכוון על כמה תכונות של שפה:
- אין מחלקות (אלא Structs עם מתודות "מקושרות")
- אין בנאים (constructors) - משתמשים במקום ב Factory methods
- אין הורשה (לא יחידה, לא מרובה)
- אין exceptions
- אין annotations
- אין Generics (לפחות לא כאלו שהמתכנת יכול להגדיר).
- יש צמצום בכלים של השפה, שיש להם תחליף (למשל יש for, אבל אין while...until).
אלמנטים משותפים בין שפת Go ושפת Python:
- שפה פשוטה ומינימליסטית.
- השפעות רבות בתחביר: func במקום def (אבל מרגיש אותו הדבר), מבני נתונים כחלק מהשפה, השמה מרובה, slicing, ממשקים כ Duck Typing ("אם הוא עושה קול של ברווז, והולך כמו ברווז - אז הוא ברווז"), ועוד.
- "יש דרך אחת מומלצת לעשות דברים" - Go היא שפה מאוד opinionated, יותר מפייטון - וההיפך הגמור משפת רובי.
למשל: הדילמה האם לפתוח סוגריים מסולסלים בשורה קיימת או חדשה - נפתרת במהירות ע"י הקומפיילר: סוגריים מסולסלים בשורה חדשה זו שגיאת קומפילציה! - גישה זה עלולה להישמע קיצונית בהתחלה, אך בסופו של דבר, כשהמערכת מתחילה להגיע לבגרות, רבים מאוד מאיתנו משתמשים בכלים של Static code analysis בכדי למנוע מעצמנו שונויות של סגנונות בקוד. אפשר לראות בזה כלי static analysis שמוטמע כבר בקומפיילר.
- יש לציין ששפת Go היא יותר verbose מ Python. יש לכתוב יותר קוד בכדי להשיג תוצאה דומה.
מבנה תיקיות של פרוייקט בשפת Go
באופן לא סטנדרטי, שפת Go מצפה בסביבת הפיתוח למבנה תיקיות מאוד מסוים.
עבור סביבת פיתוח של גו, עליכם להגדיר את שני משתני הסביבה הבאים:
- GOROOT - המצביע לתיקיה בה נמצא ה GO SDK
- GOPATH - המצביע למקום בו נמצא קוד ה GO.
רוב הניסיונות להתחכם, ולנהל קוד Go בתיקיות שונות במערכת - יסתיימו באכזבה (שלכם). הדרך היחידה המקובלת, שאני מכיר, לנהל את הקוד בתיקיות שונות הוא לייצר symbolic links תחת GOPATH לתיקיות בהן אתם רוצים לנהל את הקוד.
GOPATH יכול להיות גם תיקיית המשתמש (~).
תחת התיקייה GOPATH ישנן 3 תיקיות:
- src - התיקייה מתחתיה נמצא הקוד שלכם.
- pkg - התיקייה מתחתיה נמצאים package objects שמתארים את הקוד מצב הקוד המקומפל (כך שלא צריך לקמפל בשנית). כל צמד "מערכת הפעלה"_"ארכיטקטורת מעבד" מנוהל בתיקיה משלו.
- bin - התיקייה אליה נשלחים תוצרי הקומפליציה (קבצים בינאריים). הקומפילציה עצמה מתרחשת בתיקיה tmp של מערכת ההפעלה (התיקיה המדוייקת - תלוי במערכת ההפעלה).
תחת תיקיית ה src ניתן לשים packages ישירות, אך יותר מקובל לנהל תיקיות לכל פרוייקט ("workspaces") ורק מתחתיהן התיקיות של ה packages:
כאשר נפעיל ב command line את הפקודה go install hello/web/server, הכלי לא ישאל היכן החבילה (package) נמצאת - הוא ימצא אותה בעצמו.
reference רלוונטי
הגדרת משתנים
בואו נמשיך להתבונן בקוד, ונגש ליסודות של כל שפה - הגדרת משתנים:
- בשפת Go, מגדירים משתנה בעזרת המילה השמורה var. קודם מופיע שם המשתנה - ורק אז הטיפוס (הפוך מ Java או C - מה שמבלבל בהתחלה).
- ניתן באותה השורה גם לאתחל ערך. אם לא מאתחלים ערך, הקומפיילר של Go יקבע "ערך אפס" (המספר 0 עבור מספר, nil עבור אובייקט, וכו')
- בהשראת Python - מנסים ב Go לייתר הגדרות לא הכרחיות: אם אנו מציבים ערך ברור (במקרה שלנו: 1 שהוא int) אז הקומפיילר של Go יקבע את הטיפוס בעצמו ע"פ כללים מסוימים (במקרה שלנו: int).
- ניתן באופן מקוצר לבצע כמה הגדרות של משתנים כ Grouping declaration.
- בתוך פונקציה, הגדרת משתנה שלא בשימוש (במקרה שלנו: f) - היא שגיאת קומפילציה.
- בתוך פונקציה, ניתן להשתמש בתחביר מקוצר =: המייתר את השימוש ב var, במידה ואנו מציבים ערך במשתנה.
הנה עוד כמה התנהגויות:
- ב Go אין casting אוטומטי של טיפוסים. חיבור של int ו float גורר שגיאת קומפילציה. הגישה של Go היא "אנחנו לא רוצים אקראיות - תגדיר בדיוק למה התכוונת". זו גישה מאוד בוגרת - לשפה "צעירה ומגניבה".
- ניתן לבצע casting בקלות יחסית (הסוגריים הם על המשתנה ולא על הטיפוס - הפוך מג'אווה), ולקבל תוצאות בהתאם.
- ישנם גם קבועים. הם דומים להגדרת משתנים ב var, אך לא ניתן להשתמש בתחביר המקוצר =:, וכמובן שלא ניתן לדרוס את הערך לאחר שהוגדר. נסיון לדרוס קבוע - מסתיים בשגיאת קומפליציה, כמובן.
- אם אתם לא מעוניינים להשתמש כרגע במשתנה, אבל גם לא רוצים שגיאת קומפילציה - הציבו את ערך המשתנה בתוך ה blank identifier שהוא קו תחתון. ה blank identifier הוא משתנה לכתיבה בלבד, כאשר רוצים להיפטר מערך כלשהו. ממש כמו dev/null/ בלינוקס.
טיפול בסיסי במחרוזות
הנה כמה תכונות בשפה שבוודאי יקסמו למשתמשי פייטון (וגם לאחרים):
- ניתן להגדיר טווח בסוגריים, בכדי לבצוע sub_string מתוך מחרוזת.
- for ... range הוא ה "foreach" של שפת גו. ניתן להשתמש ב range על מערך, slice (חלק ממערך), מחרוזת, map או channel (קונסטרנט לתקשורת בין goroutines, נגיע אליהן בהמשך).
range בעצם מחזיר 2 ערכים: key ו value, מה שלא מוצג בדוגמה למעלה (יש שימוש רק ב key, שהוא האינדקס של ה char במחרוזת). - ב go יש שימוש נרחב ב Printf, לביצוע הדפסה עם formatting - ממש כמו בשפת C. הפקודה Printf מתחילה באות גדולה - מכיוון שהפונקציה היא public לחבילת "fmt". ניתן לגשת ל character במחרוזת בעזרת אינדקס (כמו שפות רבות אחרות).
- איזה כיף! יש הגדרה של מחרוזת multi-line!
שימו לב ששורה 2 עד 4 יכילו מספר רווחים לפני הטקסט, כי העימוד מתחיל מעמודה 0 ולא מהעימוד של שורת הקוד הקודמת (התנהגות זהה לפייטון, אם אני זוכר נכון).
Result מרובה וטיפול בסיסי בשגיאות
- הפונקציה Printf מחזירה כתשובה את אורך המחרוזת בבייטים. בנוסף: היא מחזירה שגיאה אם הייתה בעיה בכתיבה (nil במידה ואין שגיאה).
השמה מרובה נעשית כמו בפייטון, עם פסיק בין הערכים. - קוד לדוגמה בו אנו בודקים את השגיאה ומתנהגים בהתאם.
- הנה כתיבה מקוצרת ומקובלת למדי בשפה: אנו מבצעים את הפעלת הפונקציה וההשמה בתוך משפט ה if התחום בסימן ; (תוחם statement). היא שקולה לסעיפים 1 + 2.
טיפול השגיאות בשפת Go הוא נושא שנוי במחלוקת:
- מצד אחד, האווירה בגו (אפס סובלנות של הקומפיילר למה שעשוי להיות שגיאה) מכוונת את המשתמשים לכיסוי גבוה של טיפול בשגיאות - מה שמוביל לקוד לא כ"כ אסתטי, שחוזר על עצמו ומעט מרגיז.
- מצד שני, גם בשפות כמו #C או ג'אווה שבהן יש מנגנון exceptions - כשהתוכנה מתבגרת, אנו כותבים כמעט אותה כמות של קוד לניתוח ה exceptions. האידאל לפיו "זרוק exception עמוק בפנים, וטפל בו רק ברמת ה UI (כהודעה למשתמש)" - לרוב לא מתממש.
- מצד אחד, האווירה בגו (אפס סובלנות של הקומפיילר למה שעשוי להיות שגיאה) מכוונת את המשתמשים לכיסוי גבוה של טיפול בשגיאות - מה שמוביל לקוד לא כ"כ אסתטי, שחוזר על עצמו ומעט מרגיז.
- מצד שני, גם בשפות כמו #C או ג'אווה שבהן יש מנגנון exceptions - כשהתוכנה מתבגרת, אנו כותבים כמעט אותה כמות של קוד לניתוח ה exceptions. האידאל לפיו "זרוק exception עמוק בפנים, וטפל בו רק ברמת ה UI (כהודעה למשתמש)" - לרוב לא מתממש.
האם הבחירות של שפת גו (טיפול בשגיאות, חוסר סלחנות על משתנים שלא בשימוש, וכו') היא ריאליזם בהתגלמותו (כי בסוף אנחנו מגיעים לשם בכל מקרה), או הגזמה? - קשה לומר עדיין. בפרספקטיבה של זמן, נוכל כנראה לומר.
יאללה כיף - goroutines
אם אתם מתעניינים בשפת גו, אתם בוודאי רוצים לדעת, ומהר - קצת יותר על תכונות ה concurrency שלה, שהן כ"כ מדוברות.
אני מניח שכולנו מכירים היטב את מודל ה Threads של שפת ג'אווה, ואולי אף את מודל ה Actors שבשימוש בשפת סקאלה. גו משתמשת במודל אחר.
טיפה רקע תיאורטי:
מודל ה goroutines של שפת גו הוא בעצם מימוש של רעיון שנקרא coroutines: הבסיס ל concurrency הוא פונקציות, ולא "אובייקטים פעילים" (כמו threads).
מודל המקביליות עצמו מתבסס על מודל ה Communicating Sequential Processes (בקיצור CSP) של טוני אוהרה משנת 1978, שלא היה בשימוש נרחב עד שפת גו הציגה אותו כחלק מהשפה עצמה (ולא כספריה חיצונית).
המודל של Actors, המודל "של סקאלה" (בעצם Akka? או PID של שפת Erlang), הוא פרי עבודתו של קארל הייט מ1973.
בין המודלים של ה Actors וה CSP יש דמיון לא קטן - אבל יש גם כמה הבדלים. החפירה בנושא זה היא מעבר להישג ידו של פוסט זה, מן הסתם.
חזרה לת'כלס:
ה goroutines של שפת גו הם בעצם סוג של Green-threads, נימים (threads) המנוהלים ע"י סביבת הריצה או ה VM - ולא ע"י מערכת ההפעלה.
יתרה מכך: greenthreads לא משתמשים באף מנגנון של מערכת ההפעלה. כל הניהול הוא אפליקטיבי ומתרחש ב user space (ללא context switch ל kernel).
- green threads הם מהירים יותר ליצירה, לתזמון, ולסנכרון. עבודת CPU תהיה לרוב יעילה משמעותית יותר איתם, מאשר עם native threads.
- צריכת הזיכרון של green threads יכולה להיות משמעותית קטנה (בשפת go, ה stack ההתחלתי של goroutine הוא 2KB זכרון, מול 1MB ב thread של מערכת ההפעלה) - מה שמאפשר להחזיק הרבה מאוד goroutines במקביל.
- כאשר מדובר בהרבה פעולות I/O, דווקא native threads נוטים להיות יותר יעילים.
- green threads לא יכולים להשתמש בריבוי מעבדים, וזה כולל את יכולת ה Hyper-Threading של אינטל, שמדמה מעבדים וירטואליים.
- כאשר / אם green thread מבצע פעולת blocking ברמת מערכת ההפעלה (ולא הדמיה שלה סינכרוניות ברמת סביבת הריצה), לא רק ה green thread נחסם - אלא נחסם ה thread של מערכת ההפעלה עד סיום פעולת ה I/O (שכעת לא יכול לתזמן green threads אחרים). לשפת Go ספציפית יש ייתרון שהיא חדשה, ותוכננה לתסריט זה. היא לא מספקת גישה לקריאות blocking I/O (אולי רק ממשק שנראה שכזה) כמו שפות קיימות (למשל ג'אווה) - ולכן זה עניין שהתמכנת לא צריך לדאוג לו.
בואו נראה קצת קוד:
התוכנית שלנו קוראת ל-2 פונקציות: printHello ו printWorld.
- בשלב זה אנו קוראים ל printWorld, אבל שימו לב למילה השמורה go: היא גורמת לפונקציה הרגילה לגמרי לרוץ כ goroutine על greenthread של סביבת הריצה של גו!
קרוב לוודאי שיש קשר חזק בין שם השפה - לשם המילה השמורה go. - הפונקציה (שרצה ב greenthread נפרד) תמתין עכשיו כ 2 שניות. זה לא יפריע לשאר התוכנית להמשיך ולרוץ.
לאחר ההמתנה היא תדפיס את הטקסט "!World". - בזמן ש printWorld ממתינה, אנו קוראים לפונקציה printHello, גם כ goroutine.
יכולתי להגדיר את printHello כעוד פונקציה של ה package, אבל חבל לי להעמיס על המרחב הציבורי עבור פונקציה של שורה אחת.
למה אני זקוק לעוד פונקציה עבור שורת קוד בודדה? כי אני רוצה להריץ אותה כ goroutine, כמובן! - שפת גו מאפשרת להגדיר פונקציה ולהריץ אותה מיד, בעזרת כתיבת סוגריים מיד לאחר הגדרת הפונקציה - ממש כמו בשפת javaScript.
זה התרגיל שעשיתי - ואני מקווה שלא הקשיתי מדי לעקוב (אני גם לא רוצה לשעמם...) - פה יש קטע: מכיוון ש printHello ו printWorld רצות במקביל כ goroutines, הפונקציה main הגיעה לסופה - מה שיגרום לתוכנה להסתיים, ולא אוכל לראות את הפלט של התוכנית.
אני יוצר המתנה יזומה של 3 שניות בפונקציה main (שלצורך העניין היא goroutine בפני עצמה), בכדי לתת דיי זמן ל printHello ו printWorld להתבצע בבטחה.
הפלט של התוכנית יהיה כמובן: "!Hello World" (כאשר המילה השניה מודפסת לאחר כ-2 שניות).
קצת מפריע לי שהייתי צריך להמתין כ 3 שניות שלמות, על פעולה שמסתיימת כנראה לאחר כ 2.000001 שניות, לערך. האם אין דרך נבונה יותר מ Sleep לסנכרן בין goroutines?!
בוודאי שיש:
- אנו יוצרים אובייקט מסוג WaitGroup שיקרא מעתה "המוניטור" (ע"ש דפוס העיצוב)
- אנו מודיעים למוניטור שתנאי הסיום שלו הוא 2 הודעות Done.
- כל אחת מהפונקציות printHello ו printWorld יודיעו למוניטור כשהן סיימו.
אנו כמובן עושים זאת בדרך של גו: בעזרת המילה השמורה defer - שמבטאת רעיון יפה מאוד:
התפקיד של defer היא כמו finally בשפת ג'אווה - לבצע דברים בסיום ההרצה של הפונקציה, גם במקרה סיום תקין וגם במקרה של סיום עם שגיאה.
נקודת חולשה של finally היא שההקשר הסיבתי אבד: כל הפעולות שיש לבצע ביציאה מהפונקציה מרוכזות בבלוק ה finally ללא קשר ממה הן נובעות. הפקודה defer מאפשרת לנו להכריז על הפעולה שיש לבצע בעת יציאה מהפונקציה - בהקשר שקל להבין. ניתן לקרוא ל defer בכל מקום בפונקציה, והפעולה תמיד תרשם לזמן היציאה מהפונקציה. ממש יפה! - אנו אומרים למוניטור להמתין לתנאי הסיום שלו.
פלט התוכנית הוא כמובן "!Hello World", והביצועים מצוינים: לא יותר מ 2.000001 השנייה!
בפוסט המשך, אמשיך ואצלול לתוך השפה.
שיהיה בהצלחה!
----
לינקים רלוונטיים
לינק ל Effective Go - מדריך מהיר לשפה מבית גוגל.
לינק ל Go Language Specification - למי שרוצה ״לחפור״.
לינק ל Cheat Sheet מוצלח על התחביר של Go.
למה Go? (כתבה של גוגל) - http://talks.golang.org/2012/splash.article
---לינק ל Go Language Specification - למי שרוצה ״לחפור״.
לינק ל Cheat Sheet מוצלח על התחביר של Go.
למה Go? (כתבה של גוגל) - http://talks.golang.org/2012/splash.article
[א] השם "Go" הוא כללי מדי. מזכיר לי ספריית בדיקות ל Delphi שעבדתי איתה, שקראו לה: "Want".
אתם יכולים להבין איזה סיוט זה היה למצוא עליה משהו בגוגל?!
[ב] הפקודה go install my_package לא תתן שום פידבק שמשהו לא תקין קרה. נראה ש Go לא מוצא כזו חבילה - וחבל שהוא לא מתריע.
ניתן להשתמש בפקודה go build -v my_package לבנות את הקוד, כאשר שמות ה packages אמורים להופיע כפלט. אם לא הופיעו - סימן שמשהו בקומפילציה כשל.
מעולה כרגיל, תודה!
השבמחקבאיזו סיטואציה אתה חושב שכדאי לשקול שימוש ב go במקרה של מערכת שכתובה ב- java?
איך go מתקשרת עם שאר חלקי המערכת שלכם?
היי יניב,
מחקהייתרונות היחסיים של גו על ג'אווה הם:
א. כתיבה של קוד highly concurrent ומורכב. ניתן להגיע לאותן תוצאות בג'אווה בעזרת ספריות 3rd party, אבל הקוד בגו כנראה יהיה משמעותית יותר אלגנטי.
ב. כאשר יש צורך ב system level operations בצורה משמעותית (למשל: utility שעובדת הרבה עם מערכת ההפעלה). החיבור בגו הוא טוב יותר מ JNI.
ג. כאשר רוצים memory footprint נמוך במיוחד. ג'אווה, מתוך בחירה מודעת איננה חסכנית בזכרון. תהליך מינימליסטי בג'אווה יכול בקלות לצרוך 100MB זכרון, ואם אתה משתמש ב frameworks כמו Play או Spring - גם 400-500MB הוא לא מראה נדיר. לגו אין את ה footprint ההתחלתי הזה.
המודל בו אנו עובדים הוא מודל של Dual Stack. על אותה מכונה פיסית יש שני תהליכים: אחד ברובי, ואחד בגו. התהליך ברובי אחראי על ה UI ורוב ה endpoints, והתהליך בגו אחראי על כל ה endpoints שדורשים high throughput או מקביליות מורכבת. הצרכנים של השירות יודעים שה endpoints של רובי נמצאים בפורט x ואלו של גו בפורט y. אנחנו לא מנסים להסתיר זאת. שני התהליכים עובדים כמובן עם אותו בסיס הנתונים. אם התהליכים של רובי וגו צריכים לתקשר זה עם זה - הם עושים זאת על גבי HTTP, כצרכנים "חיצוניים" של ה API לכל דבר.
ליאור
"כאשר green thread מבצע פעולת I/O סינכרונית ברמת מערכת ההפעלה (ולא הדמיה שלה סינכרוניות ברמת סביבת הריצה), לא רק ה thread נחסם - אלא כל התהליך (עם כל ה green threads האחרים) נחסם עד סיום פעולת ה I/O. המשמעות היא שעל green threads להשתמש ב I/O אסינכרוני בלבד, או לשלם מחיר יקר מאוד של חוסר-יעילות."
השבמחקלמה זה נכון? אם יש לך n חוטי קרנל בתהליך, אז למה שכולם ייחסמו?
היי,
מחקאני אתקן 2 חוסרי דיוקים במה שכתבתי:
1. יותר מדוייק יהיה להגדיר את העניין כ non-blocking I/O ע"פ I/O אסינכרוני.
2. "אלא כל התהליך (עם כל ה green threads..." התכוונתי לומר "כל ה os thread עם כל ה green threads שהוא מריץ".
העניין הוא כמובן שאם green thread יחיד מבצע פעולת blocking I/O אזי ה *thread של מערכת ההפעלה* נחסם, ואותו thread לא יכול להמשיך להריץ green threads אחרים שהוא תכנן להריץ.
הבעיה הזו רלוונטית בעיקר לספריות של green threads שנכתבות עבור סביבות ריצה קיימות (למשל: ג'אווה), בהן ניתן לבצע פעולת blocking i/o.
סביבת הריצה של גו, משתמשת רק בקריאות non-blocking i/o למערכת ההפעלה, וברגע ש goroutine בצעה אותה - היא תנסה תתזמן את ה goroutine הבא לרוץ (כי פעולת i/o אורכת זמן). זה ייתרון ממשי של שפה חדשה, ששמה דגש על מקביליות בתכנון שלה: היא פשוט לא מייצרת אפשרות להשתמש ב blocking i/o (לפחות לא בצורת העבודה הסטנדרטית. אולי יש איזה תרגיל לעשות זאת).
תודה על ההערה, ואני מקווה שהסברתי את הדברים.
מה היו המחשבות שלכם על סקאלה באותו זמן?
השבמחקשזו שפה מורכבת, שמאפשרת לשני מפתחים לכתוב בסגנון שונה מאוד זה מזה - מה שיקשה על מפתחים להכנס אליה לעבור בין קוד של שירותים שונים.
מחקלהזכיר: זו שפה שניה לרובי - מה שיגביל את זמן ההתעמקות של מפתחים בה.
ההרגשה הייתה שהיא מורכבת מדי בכדי להיות יעילה כשפה שניה.
תגובה זו הוסרה על ידי המחבר.
השבמחקמדוע GO מתאימה פחות מ Ruby לשימוש ב UI ?
השבמחקהכוונה היא ל Ruby on Rails.
מחקRails הוא פריימורק מאוד פרודקטיבי לייצור UI (בעיקר CRUD). הוא מסתמך על יכולות שפת רובי בכדי "להרחיב" את השפה לדומיין הספציפי של אפליקציית ה UI - בצורה שלא ניתן היה בשפה סטטית.
אין אלטרנטיבה דומה ב Go, מבחינת productivity.