איך (לא) לבצע בחירות טכנולוגיות עבור המערכת שלכם?

עוד בחודש הראשון לקיומו של הבלוג, כתבתי פוסט על בחירת טכנולוגיות (בורסת הטכנולוגיה: איך מחליטים על השקעה בתחום לא מוכר?).המשימה של בחירת כלי-עבודה היא שלב חשובה בהנדסת התוכנה – ולכן ראוי לעסוק בה.
בפוסט ההוא, התייחסתי לבחירת טכנולוגיה כתהליך לוגי… מה שלכאורה נכון.

בפועל, בבחירת טכנולוגיות / כלי-עבודה יש הרבה מהפסיכולוגיה והסוציולוגיה. האלמנטים הרגשיים והחברתיים הולכים ונעשים משמעותיים יותר – כנראה על חשבון האלמנטים הלוגיים.

ישנו ניסוי מוכר בו מציבים דוכנים עם פריטים למכירה (בווריאציה שאני מכיר: תכשיטים):

  • דוכן ראשון – עם 6 פריטים בלבד.
  • דוכן שני – עם מבחר של כ-30 פריטים.

כיצד יהיה שונה דפוס הקנייה בשני הדוכנים?

על פניו: אנשים אוהבים זכות בחירה. כשיש יותר פריטים – יש יותר סבירות שאמצא משהו שתואם לטעמי (הגיוני).

התוצאה המפתיעה היא שבדוכן בו 6 פריטים בלבד (פחות מבחר) – אנשים קונים יותר. מדוע?

כי אנשים מתקשים לבחור. בחירה בין 30 פריטים היא קשה יותר מבחירה בין 6 פריטים – כך שהדוכן בן ששת הפריטים מצליח למכור יותר (חיזוק חיובי לחנויות מעצבים / high-end).

באופן דומה, קרה בעשור האחרון תהליך דומה בעולם התוכנה:

  • אם פעם היה Framework דומיננטי לכל שפת תכנות (STL ל++C, בג׳אווה JEE, ובדוטנט – מה שמייקרוסופט סיפקה) – עם הזמן מבחר ה frameworks הלך וגדל.
    ג׳אווהסקריפט היא הדוגמה הקיצונית לשימוש ב micro-frameworks. הבדיחה הנפוצה היא על מפגש מתכנתים שמתנהל כך: ״שלום, אני ליאור – וגם אני כתבתי Framework לג׳אווהסקריפט…״.
  • כיום, ה Technology Stack – "מתפוצץ מאפשרויות": יש לנו את Nano Server, Deis, Fastly, את Spark (על כל חלקיו) ואת Kubernetis. כל שבוע יוצאת טכנולוגיה / ספריה חדשה ומדליקה!
    האם אתם כבר מכירים את Axton, Sleepy Puppy, שפת Nim ושפת Julia? אל תחשבו אפילו לומר שלא בדקתם את OneOps של וולמארט…
  • בסיסי נתונים: אם פעם הבחירה הייתה ברורה (אורקל – באנטרפרייז, SQL-Server – ל Microsoft-shop, ו MySQL לסאראט-אפים) היום יש יותר מכמה בסיסי נתונים רלוונטיים לכל סביבת עבודה. למשל: MongoDB, Redis, ו Cassandra יכולים לשמש את כולם – בנוסף לבחירות המקוריות. בעולם ה open source כבר אין בסיס נתונים דומיננטי אחד: MySQL, אולי PostgreSQL ואולי MariaDB? מה עם Aurora?
  • המושג ״Polyglot Persistence״ או ״Polyglot Programming״ (המתאר סביבה רבת-טכנולוגיות) עלול לעזור ולהצדיק מצב של שימוש בריבוי טכנולוגיות במקביל [ד] – אבל הוא לא מפשט את הבחירה.
    Stack טכנולוגי רחב מידי – הוא עדיין בעיה תפעולית ממעלה ראשונה. איזו רמת עומק ממוצעת אתם רוצים בטכנולוגיות שאתם עובדים עימן? ככל שיהיו לכם יותר טכנולוגיות – רמת העומק הממוצעת תפחת.

בעיית הבחירה, שפעם אפיינה אותנו כצרכנים בחיינו הפרטיים – הפכה עכשיו לבעיה שרלוונטית עבורנו כמקצועני-תוכנה.

כיצד אנו מתמודדים עם הקושי לבחור – כאשר המבחר גדול?

כבני-אנוש, כיצד אנו מתמודדים עם סיבוכיות בבחירה מתוך מבחר גדול?
– בעזרת פישוט המציאות, פישוט שלעתים מוביל לפשטנות.

בעולם הצרכנות יש לנו יוריסטיקות לקנייה:

  • המלצה של מגזין / מקור ״בעל סמכות״ (לכאורה!) – תשפיע עלינו לטובה.
  • המלצה של אדם ממשי אחד שאנו מרגישים שהוא דומה לנו – לרוב תהיה חזקה יותר מהמלצה של "מקור סמכותי".
  • מה שיקר יותר – בטח טוב יותר [א].
    • טרנד של הדורות האחרונים: מה שחדש יותר – בטח טוב יותר [ב].
  • הצמדות למותגים ידועים.
    • למרות שמי שבחן פעם מוצרים לעומק – יודע שזו יוריסטיקה חלשה: מותגים ״חזקים״ לעתים קרובות מייצרים גם מוצרים חלשים / לא-תחרותיים / יקרים ללא הצדקה.
מה אנו עושים כ״מקצועני-תוכנה״? – בערך את אותו הדבר!
אנו נצמדים לשמות גדולים, למותגים, להמלצות של אתר / בלוג , והמלצות של חבר, לדברים חדשים ויקרים (במידת האפשר). הבחירות שלנו הופכות יותר "אופנתיות", ופחות "לוגיות".
השיטות הללו הן יוריסטיקות לא-מעולות, אבל גם לא בהכרח גרועות: להיות מ Mainstream אומר שיש יותר רפרנסים וחומר על הטכנולוגיה, יותר אנשים שמכירים (ואפשר גם לגייס), ואם יש בעיה ממש מציקה – אז יש ecosystem שיש לו מניע כלכלי לפתור את הבעיה הזו.

אז זו לא בחירה "מקצועית נטו"?

הרבה מהדיונים על בחירת טכנולוגיות כוללים פרטים טכניים שונים – מה שעלול לגרום לשומע התמים מהצד להאמין שמדובר בדיון מקצועי גרידא.מה שהרבה פעמים מתרחש הוא שבחרנו כבר את הטכנולוגיה אותה אנו רוצים (מותג / שם גדול / חדש / וכו') עוד לפני שהתחלנו בתהליך, ובמקום תהליך בחינה – אנו מצויים בתהליך הצדקה, המבוסס על נתונים טכניים.

הנה כמה קריטריונים שעוזרים להבין אם מדובר בתהליך בחירה או תהליך הצדקה:
  • אם הדיון הוא קיצוני, והאלטרנטיבות מתוארות לחלופין כנפלאות ("Rocks") או איומות ("Sucks") – זהו כנראה דיון רגשי.
  • אם יש אלנטרטיבה יחידה: "Angular.js זו הבחירה הנכונה", אבל לא מוכנים לדון ברצינות ב Ember.js או React+Flux (שהן דיי מקבילות) – כנראה מדובר בדיון רגשי.
  • אם מוכרים לנו את הטכנולוגיה כמו שמוכרים ביטוח – אז זה כנראה תהליך הצדקה.
    • איך מוכרים ביטוח? מתמקדים בתסריט קיצוני ומבהיל ("ואם הבית שלכם יהרס ברעידת אדמה…") בכדי להצדיק מחיר מסוים ("לא היה עדיף לשלם 3000 ש"ח בשנה?"). כאילו שאם הבית יהרס ברעידת אדמה חברת הביטוח לא תציב בפנינו כל קושי אפשרי בכדי שנקבל כמה שפחות כסף… תוך התעלמות מסבירות (מעולם לא קרה…) תוך התעלמות מנזקים אחרים (שחלילה – עלולים להיות קשים יותר)….
    • איך מוכרים טכנולוגיה כמו ביטוח?
      • "אבל בריילס יש הגנה בפני cross request forgery – וב Java האתר יהיה פרוץ להתקפות (!!)"
      • או: "אם אתה צריך לטעון סט של 5GB נתונים זה ייקח פי 10 זמן (!!)" (בעוד אנו לא חיים עם dataset בגודל דומה לזה בכלל). "חבל לאבד את כל הלקוחות שמייד יעזבו את החברה, וישמיצו אותה בפייסבוק ללא הפסקה, יום – ולילה!"
מק עם סטיקרים – הצהרה אופנתית!

לעתים אנשים נצמדים לבחירה אופנתית / רגשית כי הם השקיעו כמה ימים בלמידת הטכנולוגיה והם מרגישים נוח איתה. לפעמים זו "תחושת נוחות עם הטכנולוגיה", ולעתים "תחושת יתרון יחסי" – בלהיות המוביל של הטכנולוגיה. תופעה פסיכולוגית ידועה היא שהשקעה אישית, אפילו לא-גדולה, יכולה להפוך אותנו בצורה מובהקת לתומכים גדולים יותר של מה שהשקענו בו [ג].

ניתן "להעריץ" ספריות ומותגים ("גוגל", "HashiCorp"), גם מבלי להבין או להתנסות במוצר.
כיום אנו נוטים להעריץ חברות אינטרנט בעלות Scale גדול ושווי בורסאי גבוה ("חדי-קרן"). העתקת הארכיטקטורה של חברה שמטפלת בנתונים בנפח 20PB ביום – לא בהכרח ישרת את הצרכים העסקיים של החברה שלנו. הדרך להגיע ל Scale גבוה היא בפשרות כואבות. האם אנו מעתיקים פשרות שהארגון שלנו לא צריך לעשות?פידבקים חיוביים מחברים / עמיתים ("וואו! אתם עובדים עם unikernel? דוקר זה באמת כ"כ 2015!!!!") – תבצר את עמדתנו. האם לא נרצה עוד מהתחושה הטובה הזו?!

מתכנת (או מנהל) יכול לבחור טכנולוגיה "להעשיר קורות חיים" או בכדי "להפיג את השעמום" – המטרה הגדולה של החברה פחות נמצאת לנגד עיניו, באופן טבעי – כי הוא כבר הבין שזה "לא התפקיד שלו" (לא מצב אידאלי – אבל מצב נפוץ).
שמירה על מוטיבציה גבוהה בקרב המפתחים הוא אתגר שמעסיק כמעט כל חברה. גישה ניהולית מסוימת היא "לתת למתכנתים את הטכנולוגיה שהם רוצים" – עבור המוטיבציה שתנבע מכך. מתכנתים רבים באמת מאמינים ששימוש בטכנולוגיה מגניבה הוא שיגרום להם לאושר.
המודל (המעודכן) של מסלו, הטוען שאדם ייגש לצרכים גבוהים (למשל: הגשמה עצמית), רק במידה והצרכים הבסיסיים סופקו.

ראיתי את הניסיון לתת למפתחים "חופש טכנולוגי כמעט-מלא" בפעולה, ואני חושב שזו גישה שכדאי לנקוט בה במשנה זהירות:

  • האמונה שטכנולוגיה מגניבה תגרום לאושר – לעתים מתבדה במהירות. בכל טכנולוגיה יש גם צדדים פחות נעימים ויפים. פעם ראיתי מתכנת שדחף לעבוד בטכנולוגיה x, אבל אחרי שבועיים בלבד של עבודה – כבר היה מבואס ממנה.
    אולי חוסר הסיפוק של המתכנת נובע ממשהו אחר? יכולת השפעה / הכרה בעבודה שלו / הרצון להתפתח – ויש דרכים טובות יותר להשיג זאת?
  • הסיפוק הרב של מפתחים מטכנולוגיה חדשה מתרחש הרבה פעמים מעצם כך שהם אלו שמובילים את ההכנסה של הטכנולוגיה (בייחוד אם אחרים גם ישתמשו בה). על כל מתכנת מוביל יש כמה מתכנתים מובלים, והם עשויים להינות פחות מהמצב שנוצר. כלומר: סיפקנו אדם או שניים, וביאסנו אחד או שניים אחרים (בעוד לעוד כמה זה לא ממש משנה…).
    זה מזכיר לי את הבן הקטן שלי, שמתעקש לבחור את הגופיה הלבנה בעצמו (כולן זהות) – וחס וחלילה לא ללבוש את זו שאני בחרתי עבורו. העניין הוא עצם ביצוע הבחירה – ולא הבחירה עצמה.
  • מה קורה, שכעבור שנתיים – הטכנולוגיה הזו כבר נחשבת "ישנה", ומתרבים המאמרים שמשמיצים אותה? מי יתחזק את הטכנולוגיה הזו, ואולי עוד טכנולוגיות רבות אחרות כשאנשים יתחלפו / יהיו עוד ועוד טכנולוגיות במערכת?
אני לא טוען שיש להתעלם מהצורך של מפתחים בטכנולוגיות חדשות. זה צורך שכנראה קיים, במיוחד אצל מפתחים שעדיין לא ניסו את החוויה. האם יש לכם חברה מלאה במפתחים צעירים, אולי?
חשוב לספק את הצורך הזה אצל המפתחים, אבל לנצל את ההזדמנויות בהן "הטכנולוגיה המגניבה" באמת מתאימה לצרכים – ולעתים יש לחכות להזדמנות הזו זמן-מה. הכנסה של טכנולוגיה מגניבה כאשר הצידוק היחידי הוא "מפתחים רוצים" (והם מוכנים גם להמציא עבורה צורך…) – היא גישה מאוד לא מומלצת.לאחר שהצורך בהתנסות ב"טכנולוגיה מגניבה" סופק – אולי היה למפתחים יותר קשב ורצון להתקדם למחוזות נוספים חשובים…

פירמידת הצרכים של המתכנת?!

בענייני אופנה ניתן לזהות עוד 2 תופעות:

ישנן אופנות מתפרצות, שקצב האימוץ שלהן הוא לא-רגיל. פעם זה היה Rails, מאוחר יותר אולי MongoDB והיום זה Docker וגם Node.js ו Angular.js. הן לרוב פורצות בהובלת מובילים טכנולוגיים (שהם גם כריזמטיים), אבל לא דיי בזה להסביר את התרחבות התופעה – אולי ניתן להסביר אותה בעזרת שילוב של כמה מרכיבים [ה].
בקצב אימוץ של אופנה מתפרצת, סביר שרוב האנשים שהולכים לאחר האופנה לא ידעו להסביר מדוע באמת הכלי המדובר טוב יותר עבור המקרה שלהם. הם גם כנראה לא יהיו בעלי הבנה עמוקה בטכנולוגיה. בעיניים נוצצות הם יאמרו ("אני? אני עוד לא כזה מבין… יש לי עוד הרבה מה ללמוד"). הם יידעו בעיקר לדקלם כמה סיסמאות נפוצות "ב node.js אפשר לפתוח מיליון (!!) connections מלפטופ פשוט. אתה יכול לעשות את זה בג'אווה?!?".

כמו בכל אופנה יש אנטי-אופנה. שימוש ב Erlang או שימוש ב MySQL כפי שמתארים Wix או Uber כ"בסיס הנתונים ה NoSQL-י הטוב ביותר". אנטי-אופנה זה לבחור את הכלים הכי לא-נוצצים, ולהוכיח לכולם שבעזרת קצת כשרון – ניתן לעשות איתם הכל, ואפילו יותר מ buzz.js@node.

אני, דווקא מעריך יותר את האנטי-אופנה. אולי זה פשוט הסגנון האופנתי האישי שלי….

מי בוחר את הטכנולוגיה?

זהות הבוחר בטכנולוגיה היא גם סממן למאזן כוחות בארגון. פעם זה היה "ההנהלה" או "הארכיטקט הבכיר".
מאז גם מספר האלטרנטיבות כ"כ גדול שההנהלה לא יכולה לעקוב, וגם יחסי הכוחות התאזנו יותר לכיוון המפתחים. כשהמערכות מורכבות יותר – יש להסתמך יותר על המפתחים, והם הרי אלו שיאכלו חצץ עם טכנולוגיה שאולי לא מתאימה – הרי הם אלו שעושים את העבודה.

"חצץ"?!
פעם עבדתי עם צוות שנדרש לשכתב את כל קוד הג'אווה שלו ל Stored Procedures בתוך בסיס הנתונים. זו הייתה החלטה של ארכיטקט מאוד מאוד בכיר – ולמרות שהייתה התנגדות רבה, ההחלטה עמדה בעינה.
הצוות היה מאוד מתוסכל (ראיתם פעם אלפי שורות קוד של stored procedures?!) – ולאחר כמה חודשים של עבודה מייסרת, הארכיטקט המאוד מאוד בכיר נאלץ לוותר על ה"אוטופיה ארכיטקטונית" שלו (שלא היה בה שום דבר להתגאות …).

בסטארט-אפים קטנים דווקא למפתחים יש חופש כמעט בלתי-מוגבל. מתכנת מוביל יכול להכניס כלי חדש מבלי שתהיה עליו ביקורת עניינית.

המצב הבריא הוא כנראה איפשהו באמצע: לאזן בין הקולות המנוסים (לרוב: הנהלה) והמחויבים לתוצאות החברה לטווח ארוך, לבין הקולות בשטח – אלו שעושים את העבודה.

מקור: Technology Radar של Thoughtworks

ומה אומרת הארכיטקטורה?

אם הבנתם שאותו סיפור על "ארכיטקט מאוד מאוד בכיר" שבחר טכנולוגיה מול התנגדות של כל הדרג המבצע היא מעשה סביר – חשבו שוב.

אנשים רבים נוטים לחשוב שבחירת טכנולוגיות היא חלק מרכזי ביצירת הארכיטקטורה. דווקא כמעט-ההיפך הוא הנכון:
ארכיטקטורה טובה היא כזו שמצמצת את הצורך והתלות בבחירה טכנולוגית. השאיפה הארכיטקטונית היא לדחות החלטות טכנולוגיות ובחירת כלים – עד כמה שאפשר, ואם אפשר – אז לעד.

הארכיטקטורה של תוכנה באה לארגן את הקוד שפותר בעיה עסקית. כיצד לקחת ערימת קוד – ולארגן אותו בצורה יעילה, קלה לתחזוקה, וגמישה לשינויים.

הטכנולוגיה? אולי אי-אפשר בלעדיה – אבל היא גם מפריעה!

  • למה אכפת לי איזה בסיס נתונים נשתמש? – עדיף להגדיר ארכיטקטורה שלא תלויה בכך. בפרויקט FitNesse המפתחים דחו את הבחירה בבסיס נתונים ועבדו עם מערכת קבצים "בינתיים". השנים עברו – ולא התגלה צורך מעשי בבסיס נתונים – וכך נותרה המערכת.
  • למה משנה שפת התכנות? כל בחירה תציב לנו מגבלות אחרות, ומי רוצה מגבלות?!

בסופו של דבר חשוב בצד הארכיטקטורה:

  • לצמצם את מספר הטכנולוגיות החופפות – כי ריבוי טכנולוגיות מקשה על תחזוקת המערכת, והארגון שלה.
  • למצוא טכנולוגיות "מספיק טובות" – שלא יגבילו אותנו יותר מדי.
    • ריילס – מציבה Overhead גדול של ביצועים. היא לא מספיק טובה למערכות עם High Throughput.
    • כתיבה ב C לא תציב מגבלות High Throughput אבל הקוד ייכתב באטיות ויהיה קשה לארגון ותחזוקה. היא לא מספיק טובה לצרכים עסקיים של מערכת שתפותח מהר.
אבל לבחור IDE? לבחור Application Server? ארכיטקטורה שכוללת או לא כוללת Docker? להתעקש רק על שפת תכנות ספציפית? – זו כנראה לא ממש "ארכיטקטורה", אלא ארכיטקט שמנצל את כוחו – כי גם הוא רוצה לבחור טכנולוגיות! מה לעשות – גם הוא בן-אדם שחובב טכנולוגיה.
יש כמובן גם שיקולים מעשיים / פרויקטליים:
  • איזו טכנולוגיות אנשים כבר מכירים – ויש להן תמיכה רחבה?
  • אלו טכנולוגיות יעזרו למשוך לחברה את ה talent הנדרש – לכתיבת מערכת מורכבת ומאתגרת?
  • עלויות של טכנולוגיות שאינן חופשיות.
  • לא פחות חשוב: כיצד בונים מעורבות של הצוות הקיים? לרוב ע"י מתן משקל לדעתם בבחירת הטכנולוגיה. אפשר וחשוב להציב תנאי סף: מחיר, מגוון, או כל מגבלה שהיא עניינית וניתן לשכנע בה את "הרוב השקול".

זו איננה ארכיטקטורה!

האם ראיתם פעם תרשים שכזה לתיאור ארכיטקטורה של מערכת? אני מניח שפעמים רבות.

האם אתם יכולים לומר מה המערכת הזו עושה? האם זו מערכת פיננסים – או מערכת פרסום באינטרנט? אולי תוכנת אבטחה? האם זו מערכת לצרכן – או ללקוחות עסקיים? מהם האתגרים העיקריים שהמערכת מתמודדת איתם?

אי אפשר כמובן לומר – כי לא מספרים לנו בתרשים על המערכת, אלא על סט הכלים שמרכיב אותה.
זה כמו לנסות ולהבין חפץ – ע"פ הכלים שבהם הוא נבנה: "משור, פטיש ומסמרים" – מה זה יכול להיות?! כסא, שולחן, אולי ספרייה או סוכה? האם כל מה שנכון וטוב לבניית כסא – נכון ומתאים גם לבניית סוכה?

במשך תקופה ניסיתי להימנע מלהציג תרשימים המדגישים את הטכנולוגיה – ולהתמקד בתרשימים המתארים את המודל העסקי איתו המערכת צריכה להתמודד. המאזינים שלי היו קצרי-רוח, ולא הקשיבו עד שלא הצגתי להם את סט הטכנולוגיות ("נו… אבל תראה כבר את הארכיטקטורה!").

אני מבין את זה: סט הטכנולוגיות כן נותן כיוון כללי לתיאור אופי המערכת (ביצועים, כמויות נתונים, סוג ממשקים, וכו') אבל הוא בעיקר מסר בשפה שאנו מכירים – אנו מכירים ("בגדול") את הטכנולוגיות.
יותר קל לעכל משהו בעולם שאנו מכירים – מאשר מודל עסקי שזר לנו כמעט ולחלוטין.

מאז אני פותח כמעט כל הצגה ארכיטקטונית בתיאור הטכנולוגיות המעורבות. שמתי לב שגם לי זה נוח, וגם כאשר אני מנסה ללמוד מערכת חדשה – אני מעדיף קודם לשמוע אילו טכנולוגיות מעורבות.

סיכום

בחירה טכנולוגית היא לא מה שהייתה פעם (קרי: לפני עשור). פעם היו מעט טכנולוגיות, ותהליך בחירה שמפשפש בפרטים ועושה השוואות בין 2-3 אלטרנטיבות היה אפשרי. גם בחירה טכנולוגית הייתה נעשית פעם בשנה, ולא פעם בחודשיים.

היום, עם התפוצצות האפשרויות הטכנולוגיות – לא ניתן לעקוב ולבחור בצורה שכלתנית בין כל מגוון האפשרויות. אנו משתמשים ביוריסטיקות, ומבצעים בחירות שהן יותר אופנתיות, יותר רגשיות – ופחות לוגיות ומתודיות.

מה אפשר לעשות?
להיות מודעים לכך – ולנסות להפיק את המיטב, תחת המגבלות.
לזכור גם שעם כמעט טכנולוגיה – ניתן להשיג כל דבר, והשאלה היא שאלה של יעילות. מפתחים מלאי מוטיבציה לשימוש בטכנולוגיה שהיעילות הגולמית שלה היא 0.9X כנראה יתקדמו בצורה מהירה ויעילה יותר, מאשר מצב בו ישתמשו בטכנולוגיה שהיעילות הגולמית שלה היא 1.1X – אבל הם הם לא רוצים להשתמש בה.

האם בחירה טכנולוגית היא "החלטה ארכיטקטונית?"
לטכנולוגיה יש השפעה על הארכיטקטורה – אך זו בחירה משנית. סוד: ארכיטקטים גם אוהבים לעסוק בטכנולוגיות – אנחנו חבר'ה טכנולוגים. כנראה ייטב לו אנו הארכיטקטים נזכיר את זה לעצמנו מדי-פעם, ונשתדל לא להשתלט על הדיון, אלא לנצל את ההזדמנות בכדי לערב את המפתחים, אלו שעושים את עיקר העבודה.

שיהיה בהצלחה!

—-

לינקים רלוונטיים

על העוצמה הטמונה ב"טכנולוגיות משעממות" [דעה] (פוסט שלי)

Carburetor 21: Predictions for 2016 פרק (מוצלח!) של הפודקאסט "רברס עם פלטפורמה", שהחלק הראשון שלו עוסק בבחירת טכנולוגיות.

—-

[א] – בספר Influence מספר ציאדיני על חנות תכשיטים שלא הצליחה למכור סדרה מסוימת של תכשיטים. ההנהלה החליטה לחתוך את המחירים בחצי, אבל טעות בתקשורת גרמה לעובד בדלפק להכפיל את המחירים פי-2, והפלא ופלא – סדרת התכשיטים אזלה מהחנות תוך זמן קצר!

[ב] – בכנס GOTO; Berlin 2014 הייתי בהרצאה על big data של הוצאת ספרים גדולה. נתון מעניין שהם הביאו שספרי מחשבים נמכרים בעיקר בשנתיים-שלוש לאחר שיצאו. באופן דומה – גם ספרי ניהול אימצו דפוס דומה, אם כי שיטות הניהול לא משתנות משמעותית במחזורים הקצרים מ 10-15 שנים.

[ג] למשל: 2 פסיכולוגים קנדיים הראו את ההתנהגות הבאה: במירוצי הסוסים הם תשאלו מהמרים רגע לפני ההימור, וגם רגע לאחר ההימור (לאחר שהמהמרים כבר "שמו" את הכסף).
לפני ההימור היה להם בטחון נמוך יחסית לגבי איזה סוס ינצח.

אולם, באופן עקבי, מיד לאחר שהמהמרים שמו כסף על אחד הסוסים – המגמה התהפכה. מאותו הרגע ואילך הם הפכו להרבה יותר אופטימיסטיים ובטוחים בעצם שהסוס עליו הימרו אכן ינצח.

עצם ההימור עצמו, כמובן, לא משנה את סיכויי הזכייה של הסוס – אבל ברגע שבני אדם משקיעים במשהו, הם באופן
פסיכולוגי נוטים להתחיל ולנהוג בצורה שתחזק ותצדיק את הבחירה / ההשקעה שעשו.

עקרון זה נקרא בפסיכולוגיה עיקרון העקביות (The consistency principle). הצורך של אנשים בעקביות – וההתנהגות שלהם שנוטה באופן טבעי לחזק את העקביות בעולם האישי שלהם.

מקור: Influence של ציאדיני, פרק 3.

[ד] הבהרה: אנשים רציניים שידברו על Polyglot Programming יציגו גישה לפיה עובדים עם מספר כלים – השונים מהותית זה מזה.

"node.js ו Java" או "פייטון, רובי, ו PHP" – הן לא דוגמאות ל Polyglot Programming בריא – אלו שפות דומות מדי זו לזו, וריבוי שלהן בעיקר יציב בעיות תחזוקה. דווקא שילוב כמו "PHP וג'אווה" או "רובי ו ++C" – הם שילובים רלוונטיים.

[ה] הספר "The Tipping Point" (המעניין!) של מלקולם גלוודוול מנסה לנתח את הדינמיקה של תופעות שכאלו.

מבוא ראשוני לשפת Go, חלק 3 – תכנות בעזרת אובייקטים

פוסט זה הוא פוסט המשך לפוסטים:

בפוסט זה נדון באספקט ה"אובייקטים" בשפה.

האם Go היא בכלל שפת Object-Oriented?

בכדי לענות על השאלה, בואו נבחן את שלושת התכונות העיקריות של שפות Object Oriented:

  • הכמסה (encapsulation): מוגבלת. יש הכמסה ברמת החבילה, אך לא ברזולוציה קטנה יותר.
  • ריבוי צורות (polymorphism): יש. בעזרת ממשקים – שנראה בפוסט זה.
  • הורשה (inheritance): אין. משום סוג. הדרך ללכת בה היא composition.
50% התאמה לעקרונות הבסיס => Go איננה שפת Object Oriented.
בכל זאת, מי שרגיל לתכנות OO, וארגון הקוד ע"פ אובייקטים הכוללים נתונים והתנהגות יחדיו – יגלה שעקרונות ארגון הקוד גם לא מאוד שונים.
אם אומרים על שפת JavaScript שהיא שפה "מבוססת אובייקטים" (היא לא מונחית-אובייקטים) כי יש בה אובייקטים, אך לא מחלקות – שפת Go היא רחוקה יותר מזה. ב Go ישנם "אובייקטים פתוחים", שרואים להם את השלד והאיברים הפנימיים: Structs ומתודות מקושרות.
הייתי מתאר את Go כשפה הנעזרת באובייקטים Object Aided Programming. היא מאמצת רק חלק מהרעיונות והחשיבה של תכנות מונחה-עצמים. האובייקטים הם כבר לא במרכז, אלא הפונקציות וה Structs – כשני אלמנטים נפרדים.
מקור: spf13

אובייקטים… מבנים ב Go

בואו נסתכל על קוד:

  1. אנו מגדירים טיפוס חדש בשם Rectangle שהוא בעצם struct עם 2 שדות: width ו height.
    1. שימו לב שאנו מתעדים את ה Struct ע"פ קונבנציית התיעוד של גו: …a Rectangle is
  2. בשלב זה אנו מגדירים מתודה, שמקבלת Rectangle ומחזירה int שהוא שטח המרובע.
    1. בהגדרה, מתודה היא פונקציה עם receiver (לפעמים נקרא גם handler).
    2. מתודה היא כזו שמשנה State (את זה של ה Struct = מבנה), פונקציה היא stateless.
    3. מתודה ניתן להפעיל רק דרך רפרנס למבנה (כמעט אמרתי… אובייקט) כלומר (…)myStuct.myMethod, בניגוד לפונקציה שמפעילים ללא תלות, כלומר (…)myFunction.
  3. הסוגריים המשונים הללו לפני שם המתודה הם ה receiver.
    1. גו מחברת את הפרמטר של ה receiver (תמיד יש אחד) לשאר הפרמטרים של הפונקציה – כך שהוא תמיד יהיה ראשון. זה קורה מאחורי הקלעים.
      1. כלומר: החתימה של המתודה area היא בעצם, מתחת למכסה המנוע:
        area(r * Rectangle) int.
      2. הרבה יותר נוח לקרוא קונטקסטואלית את הביטוי ()rect.area, מאשר את (area(rect – ולכן ה syntactic sugar הזה.
    2. הקונבנציה המקובלת היא לפרמטר של ה receiver באות הראשונה של הטיפוס (אות קטנה), או אולי קיצור של כמה אותיות קטנות. למשל: s או sb עבור stringBuffer.
      אם אתם מנסים לקרוא ל reference לטיפוס בשם גנרי – זה סימן שלא השתחררתם עדיין משפת התכנות הקודמת שלכם:

      1. this (ג'אווה או #C?)
      2. that (ג'אווהסקריפט, אולי?)
      3. self (רובי?)
      4. __self__ (פייטון! – תפסנו אתכם)
    3. אם בקוד המתודה אתם לא משתמשים בפרמטר של ה receiver (בדוגמה שלנו: r)  – זהו smell שאתם זקוקים כאן ל function ולא למתודה.
    4. אם בקוד המתודה אתם לא משתמשים ב fields פנימיים של ה struct (אלא רק מתודות אחרות) – ייתכן שעדיף להשתמש בפונקציה רגילה שמקבלת את ה struct כפרמטר.
  4. הנה שימוש בקוד. אנו יוצרים שני מבנים: r ו r2.
    שימו לב ש r2 הוא מצביע ל Rectangle – כי השתמשנו במילה השמורה new.
  5. אנו מדפיסים את תוכן ה Structs. הסימן & בהערה מציין שמדובר במצביע – אך שימו לב שאופן השימוש הוא זהה. בעזרת ה dot notation לא היינו צריכים לכתוב ()r2.area* מתוך ההבנה שזה פויינטר, וגם לא בקיצור כמו ()r2->area של ++C. הקומפיילר מזהה שהשתמשנו ב dot notation על מצביע, ומוסיף את ה * עבורנו, מאחורי הקלעים.
  6. עכשיו בואו נענה על השאלה: מדוע ה receiver (סעיף #3) קיבל את Rectangle כפונייטר (קרי Rectangle*)? האם חובה להשתמש בפויינטר?
    1. לצורך התרגיל נגדיר את המתודה Width (אות גדולה = שלפן) שמקבלת את Rectangle לא כפויינטר.
  7. כאשר אנו משתמשים במתודה – אנו רואים שהערך לא השתנה: הצבנו 4, אך הערך נשאר 0.
    מדוע זה קרה?! בגלל שהעברנו למתודה העתק של r2 עליו התבצע השינוי. r2 עצמו לא השתנה.
  8. הנה הבדיקה שאכן כך הדבר: כאשר אנו משנים את r2.width ישירות – הערך משתקף בהדפסה.
מפה עולה השאלה: מדוע בכלל לאפשר ליצור value receiver? מדוע הקומפיילר לא מגן עלינו ומחייב ש receiver יהיה רק pointer receiver? האם מתודה לא נועדה לשנות את ה state של המבנה המדובר?
התשובה היא לא-בהכרח.
  • value receiver הוא שימושי כאשר מעבירים פונקציה, map, או channel (שהם כבר בעצמם מצביעים). ההעתקה של הערך יכולה להיות יותר יעילה מגישה לפויינטר שמצביע לאזור שאינו ב cache של המעבד.
    slice הוא גם מצביע, אלא אם המתודה עשויה להקצות ל slice מערך חדש – ואז יש להשתמש בפויינטר.
  • value receiver הוא שימושי כאשר מדובר במתודה שהיא immutable, כלומר – העברה כ value (העתקה) מבטיחה שלא נוכל לשנות את ה state של ה struct.
בקיצור: אם אתם לא בטוחים – השתמשו ב pointer receiver. אם הזמן אולי תגלו את הצרכים ב value receiver גם כן.
אני רוצה שנייה לחזור למילה השמורה type ולהראות שאנו יכולים להגדיר aliases (כמו ב C) או מבנים שלא מבוססים בהכרח על struct. הנה דוגמה למבנה שמבוסס על slice:

הרכבת טיפוסים (Composition)

כפי שציינו, שפת גו מאמצת במלואה את ההמלצה "prefer composition over inheritance".
אם ניסיתם להימנע מהורשה ולהשתמש רק ב composition בשפות תכנות אחרות (אני ניסיתי בג'אווה) – בוודאי אתם יודעים שזה לא הדבר הכי אלגנטי שקיים:

  • נניח שאנו רוצים לדמות מחלקה B שיורשת ממחלקה A.
  • במחלקה B נוסיף שדה a שיכיל מצביע לאובייקט מסוג A.
  • למחלקה B נוסיף את כל המתודות של מחלקה A, ובכל מתודה x נקרא בעצם ל a.x עם הפרמטרים שהעבירו לנו. זהו קוד boilerplate שמרגיש דיי מיותר – במיוחד ככל שלמחלקה A יש יותר מתודות.

בשפת גו יצרו תחביר של Composition שחוסך את ה boilerplate code. במבט ראשון – הוא נראה כמו הורשה, אך זו לא הורשה. מבנה B ש composes את מבנה A – לא יכול לגשת או להשפיע על מבנה A שלא דרך המתודות הציבוריות שלו. הבאגים שיכולים להיווצר בגלל תלות שכזו בהורשה – לא יתרחשו בצורת העבודה הזו. מצד שני צורת ה composition לא יכולה להשיג חסכון בקוד-כפול בכמה צורות שהורשה יכולה.

איך עושים זאת?

מבנה בשפת גו יכול להכיל fields שהם embedded (נקראים גם אנונימיים). יכולת זו נקראת Embedded Types.

  1. אנו מגדירים את המבנה Person שמכיל שם, ומתודה sayHello.
  2. אנו מגדירים את המבנה Student שמכיל Person ושם מחלקה, ומתודה sayHello.
  3. שימו לב שבהגדרת ה Person לא ציינו שם של המשתנה (רק טיפוס). זהו embedded type שגורם להרחבה (extend).
    1. בפעולה זו אנו גורמים לכל ה fields והמתודות של Person להופיע ברשימת ה fields והמתודות של Student – דומה מאוד ל extend של מודול בשפת רובי. ה fields והמתודות עדיין נשמרים במבנה Person ומתנהגים כפי שהיו מתנהגים לו Person היה field מהמניין. נראה את ההתנהגות המדויקת מייד.
    2. אם למבנה החיצוני (Student) יש מתודות / fields עם שם זהה למבנה הפנימי (Person) – הם תמיד יקבלו קדימות, ובפועל "יסתירו" את המתודות / fields של המבנה הפנימי.
    3. ניתן עדיין לגשת למתודות של המבנה הפנימי בצורה מפורשת, קרי: student.Person.Name.
    4. בשונה מהורשה, אם מתודה של Person תקרא ל field או מתודה שקיימים גם ב Student – היא תמיד תגיע ל field / מתודה ב Person, ואף-פעם לא ל Student. ניתן לומר ש invocation "הועבר" למבנה הפנימי (Person) ופועל עליו.
      חלק מהכוח של הורשה, בוא בכך שמחלקה היורשת מחליפה התנהגויות שמשפיעות גם על המתודות של מחלקת האב. התנהגות זו עלולה לגרום לבאגים מתסכלים, אם כי היא גם מאפשרת דפוסי-עבודה שיכולים לחסוך לא-מעט קוד.
  4. הנה אנו יוצרים מופע של המבנה Student, ובתוכו את המבנה של Person. שם ה field שמחזיק את המבנה הפנימי הוא כשם הטיפוס של אותו מבנה. הבחור שלנו, ג'ון, נרשם באקדמיה בצורה לא מוסברת בשם "פופה".
  5. תזכורת: ברשימות של איברים בגו יש להוסיף פסיק גם לאחר האיבר האחרון. זה מקבל על הוספת / הסרת איברים ללא שבירת הקומפילציה.
  6. הנה אנו מדפיסים את student, ורואים כיצד הוא מיוצג בזיכרון – כמבנה מקונן.
  7. כאשר אנו קוראים ל sayHello אנו מגיעים למתודה של המבנה החיצוני בהכרח (יש לה קדימות). הערך של שדה Name הוא זה של אותו המבנה.
  8. כאשר אנו קוראים ל sayHello של Person בצורה מפורשת – אנו מגיעים למתודה של המבנה Person. הערך של שדה Name הוא זה של אותו המבנה, קרי Person.

ממשקים (Interfaces)

ממשקים הם הכלי בשפת Go לריבוי-צורות. ממשקים נפוצים בשפה הם io.Reader ו io.Writer שמסתירים מהקוד שלנו את פרטי המימוש הקונקרטי שאנו עובדים איתו (למשל: אמצעי קלט/פלט – דיסק, רשת, מבנה בזיכרון). מי שמגיע משפת ג'אווה, בוודאי מורגל לממשקים דומים. ממשק מפורסם אחר הוא error, המאפשר לנו להתייחס לכל השגיאות, אם אנו רוצים – באותה הצורה.

הבדל חשוב של ממשקים בגו מ"הקלאסיקה של ה OO", היא הרעיון שהם מבטאים:
ב OO מלמדים אותנו לחפש קשר של is-a או לפחות a-substitute-of בין הממשק ומי שמממש אותו, ושפות שונות מספקים כלים שונים לאכוף שאובייקט באמת רשאי למממש ממשק מסוים.

הגישה של גו היא גישה של Duck Typing שאומרת כך: "אם זה נראה כמו ברווז, ומשמיע קול של ברווז – אז זה ברווז!"

Duck Typing. לצורך העניין – זה יכול להיחשב כברווז.

כלומר: אין צורך להגדיר שמבנה מסוים מממש ממשק (למשל: כמו המלה השמורה implements בג'אווה).
אם למבנה יש את התכונות הנדרשות (מתודות עם אותה החתימה שהוגדרה בממשק) – אז הוא נחשב אוטומטית לטיפוס הזה.

מצד אחד – זה חוסך את הצורך להגדיר "כן, אני רוצה להיות חלק מהממשק". העניין הוא לא כתיבת עוד מילה או שתיים בקוד, אלא אמור למנוע פתיחת קוד קיים (שאולי לא בשליטתנו, למשל: ספרייה שאנו משתמשים בה).
מצד שני – זה מקום לטעויות. יכול להיות שיש חתימה של מתודה עם שם מסוים, ולי כמכנת, נדמה שאכן המבנה יתמוך בהתנהגות שאני מצפה לה. נדמה, אולם אם הייתי מתייעץ אם מי שכתב את הקוד הייתי מבין שמסיבה כלשהי – לא כך הדבר.

גישת ה Duck Typing אם כן, מציגה tradeoff עם יתרונות וחסרונות – אולם זו גישה מקובלת ושנחשבת בהחלט סבירה. בשפת פייטון גישה זו הוכיחה את עצמה כישימה, גם בפרספקטיבה של זמן.

בואו נראה כיצד משתמשים בממשקים בקוד:

  1. אנו משתמשים במילה השמורה type בכדי להגדיר טיפוס מסוג ממשק בשם statusReporter, המכיל 2 מתודות.
  2. אנו עכשיו מגדירים פונקציה שמקבלת statusReporter כפרמטר, ומבצעת עליה פעולה כלשהי – בהתבסס על מתודות של הטיפוס (Status ו LastErrors).
  3. הנה הגדרנו איזה מבנה (=~אובייקט) שיש לו, במקרה או לא במקרה – מתודות עם חתימה זהה.
  4. אנחנו יכולים לשלוח כל טיפוס שעונה לתנאי הממשק – כפרמטר לפונקציה printStatus שהגדרנו.

מבט מתקדם על Interfaces בשפת Go

עד כאן הכל נראה פשוט. אם אתם מגיעים מג'אווה / #C, אתם בוודאי אומרים לעצמם: "זהו: את העניין של ממשקים ב Go – כבר הבנתי!".

קחו עוד מעט אוויר. נותרו עוד מספר נקודות שעליכם לדעת על interfaces בגו בכדי לא-להסתבך איתם. התבוננו על דוגמת הקוד הבאה:

  1. אנו מגדירים interface בשם HelloSayer.
  2. אנו יוצרים מבנה בשם מר זאב, העונה לממשק (עם value receiver).
  3. אנו יוצרים מבנה בשם גברת נץ, העונה לממשק (עם pointer receiver).
  4. אנו מאחסנים 2 זאבים (מצביע, ומופע קונקרטי) ו2 נצים (גם מצביע ומופע קונקרטי – שתי האפשרויות הרלוונטיות) – בתוך slice מטיפוס HelloSayer. עצם ההשמה גורמת ל upcasting של ה references ל ממשק.
מה זה משנה אם יש לנו value receiver או pointer receiver?
האם בכלל לא צריך להתאים בין הערך ששולחים ל receiver?!
התשובה היא שמקרים A-C יעבדו בצורה תקינה, אך מקרה D יגרום לשגיאת קומפילציה "SayHello has pointer receiver" – שגיאה שבוודאי תתקלו בה בעבודה שלכם בגו, שגיאה שהיא קשה להבנה ללא הרקע המתאים.
מדוע דווקא מקרה D הוא זה שנכשל?
האם נחליף את הביטוי D ב "{}Hawke&", כלומר נשלח מצביע למבנה – הדברים ייסתדרו? (כן)
מדוע זה עובד כך?
יש יותר מדרך אחת להסביר זאת, אני אבחר בהסבר של הברירות שהיו למתכנני שפת Go – נראה לי שזה האופן הפשוט ביותר להסביר זאת.
בגדול אנו רואים שניתן לשלוח למתודה עם value receiver גם מופע (A) וגם פויינטר (C), אך למתודה עם pointer receiver ניתן לשלוח רק פויינטר (B).
שפת גו מנסה:
  • לחסוך למתכנת "עבודה שחורה" – כל עוד הדבר לא פוגע בביצועי ריצה או ביצועי ההידור בצורה מיוחדת.
  • להיות עקבית וצפויה, ולחסוך מהמתכנת "באגים חמקמקים".
כאשר שולחים מצביע ל value receiver הקופיילר יכול לתקן זאת בקלות – ולשכתב את הקוד כך שישלח את הערך עליו מצביע הפויינטר. מי שכתב את המתודה מצפה לקבל עותק – וזה מה שהוא קיבל.

אם הקופיילר יתערב בשליחה של מבנה קונקרטי ל pointer receiver ויהפוך אותו למצביע, הוא מטעה את אחד הצדדים: או שהמתודה תניח שיש לה פויינטר ותבצע שינויים שיתעלמו מהם (כי מה שהועבר הוא עותק), או שמי ששולח ערך מניח שלא יכולים לשנות לו את הערך (הוא לא שלח פויינטר) – אך פתאום הערך יישתנה.

בכל אחד המקרים הנ"ל יכול להיווצר באג קשה ומתסכל למציאה. היוצרים של Go העדיפו לייצר שגיאת קומפילציה ולתת למתכנת להחליט איזו התנהגות הוא מעדיף.

{}interface

הטיפוס {}interface הוא הממשק שאינו מכיל שום מתודות. ע"פ Duck Typing – מי מספק ("מממש") אותו? כולם!

זה אומר שזהו טיפוס-על שמייצג את כל הטיפוסים בשפה. ניתן להשוות אותו לביטוי *void (הידוע כ generic data pointer) בשפת C, אם כי {}interface הוא בטוח יותר לשימוש.

אילו טיפוסים ניתן להעביר בעזרת {}interface? האם אפשר להעביר int?
בואו נראה את דוגמת הקוד הבאה:

אפשר!

  • האם {}interface מאפשר dynamic typing בשפת Go?
    • לא. יש לו טיפוס סטאטי ומוגדר היטב. אפרט מייד.
  • מהו, אם כן, הטיפוס של i בדוגמה לעיל?
    • הטיפוס הוא {}interface. גו מבצעת type conversion על 1 ל {}interface, ואז חזרה בהדפסה.
  • האם אפשר להחיל את הכלל הזה גם על מערך / רשימה? כלומר: להעביר int[] כ {}interface[]?
    • לא. ניתן להעביר int[] כ {}interface (כי הוא טיפוס בשפה – slice) – אך הוא שונה מהותית "מערך של טיפוסים". אם רוצים לבצע את ההעברה יש לעשות עבודת העתקה ידנית.

interface בשפת גו הוא בעצם סוג של פויינטר כפול. ניתן לתאר אותו כמבנה (הדמיוני) הבא:

מצביע ראשון: interface-table (מסוג itab) – מבנה המכיל metadata של הטיפוס (שם הטיפוס, הגודל שלו בזיכרון) ואת רשימת המתודות המשויכות לטיפוס הזה.
מצביע שני: data – הכתובת בזיכרון מה מאוחסנים הנתונים. "200" עבור int או רצף התווים של struct מסוים.

הנה pitfall בשפה שניתן להיתקל בו:
  1. נגדיר קבוע שמגדיר האם אנו אוספים נתונים ל BI או לא.
  2. נגדיר את buf מטיפוס Buffer. הטיפוס Buffer מספק את הממשק io.Writer.
  3. אם אנו במצב של איסוף נתונים – ניצור את ה buf, אחרת – אין טעם.
  4. פונקציית Perform מבצעת איזו לוגיקה עסקית, וכותבת נתונים שנאספו ל io.Writer.
    יש הגנה במידה וה io.Writer שנשלח לה הוא nil.
מה תוצאת הריצה?
  • אם collectData == true – התוכנה תתנהג כצפוי.
  • אם collectData == false, אנו נקבל שגיאת panic שאומר כך: invalid memory address or nil pointer dereference. כלומר: ניסינו להשתמש במצביע שהוא nil.
איך זה קרה?
מה יכולנו לעשות טוב יותר??

התהליך שגרם לשגיאה הוא כזה:

  • במידה ו collectData הוא false, אין השמה ל buf – והוא נשאר nil.
  • כפי שאמרנו ממשק הוא לא מצביע. כאשר אנו קוראים ל perform סביבת הריצה מבצעת עבורנו type conversion מ Buffer ל io.Writer.
    out, שהוא מטיפוס ממשק io.Writer, מכיל עכשיו שני מצביעים: iTable שמצביע על Buffer (טיפוס + רשימת מתודות), ו data שהוא nil.
  • בשלב #4 בקוד לעיל אנו בודקים אם out הוא nil, אבל הוא לא. הוא מבנה תקין מסוג ממשק שמכיל מצביע אחד לטיפוס, ומצביע שני שהוא nil.
באסה… מה עושים? איך לא נופלים בפח הזה?
ראשית – אתם צודקים. זו התנהגות "מכשילה" של השפה. לזכותה של Go אציין שיש לה הרבה פחות התנהגויות מכשילות מרוב השפות שאני מכיר. בעצם: ככל הנראה זו השפה הכי פחות מכשילה שאני מכיר.
אז מה עושים?
  • לבצע בשלב #4 (וכל מקום אחר בקוד שמקבל רפרנס) בדיקה עמוקה יותר של nil. ג'אווהסקריפט מישהו? שיש בה גם null וגם undefined? – ממש לא! זו אופציה גרועה למדי.
  • ניתן ליצור את Buffer בכל מקרה, ולבצע עבודה שלא נדרשת בכדי להימנע מטעויות. זו גם אופציה גרועה.
  • הדרך הנכונה לתקן זאת היא להגדיר מראש (בשלב #2) את buf כממשק (קרי io.Writer ולא כ Buffer).
    buf יישאר nil עד שיושם לו ערך (שלב #3), ואז שני המצביעים של הממשק יתמלאו במקביל בערכים.

שימושים של {}interface

אז מה בעצם השימושים הפרקטיים של {}interface? כנראה שלא לעשות המרה מטיפוס ל {}interface, ואז מ{}interface לטיפוס בחזרה…

מייד נראה שימוש מעט אחר בממשקים, המשמש עבור נקודות בקוד בהן אנו כן רוצים לדעת את המימוש הקונקרטי שמאחורי הממשק – אך לטפל ביחד בכמה טיפוסים שונים.

הנה פונקציה שממירה משתנה כלשהו (לא טיפלתי בכל המקרים) – לערך שניתן לשלוח לשאילתת SQL.

  1. למשל: לא ניתן לשלוח nil ולכן הפכתי אותו למה שבסיס הנתונים מכיר – "NULL".
  2. הביטוי (x.(int בוודאי נראה מעט מוזר. זהו ביטוי מיוחד בשפה הנקרא type assertion שבודק בו-זמנית שני דברים: ש x הוא לא nil, וגם ש x מתאים לטיפוס (או הממשק) שצוין – במקרה שלנו: int.
    הביטוי מחזיר שני ערכים כתשובה:

    1. הערך השני ("ok") הוא bool שמציין אם ה assertion הצליח – כלומר: x הוא int שאינו nil.
    2. הערך הראשון הוא הערך של x על פי הטיפוס שהגדרנו (קרי int). הוא יקבל ערך אפס – אם ה assertion אינו נכון.
  3. Sprintf או כמו Printf, אך במקום לשלוח את הקלט ל default output הוא מחזיר אותו כמחרוזת.
  4. עתה אנו עושים אותו תרגיל על bool, אך במקרה זה אנו רוצים לשמור את הערך.
    מדוע אנו משתמשים ב val? מדוע לא לשאול if x? – הקומפיילר לא יאפשר לנו ויכשיל אותנו בבדיקת טיפוסים. הוא יאמר שלא ניתן להגדיר תנאי if על {}interface. המשתנה val מוכר לקומפיילר כ bool.
  5. ב Go אין ternary if (כלומר: : ?). חבל.
  6. panic הוא סוג של Exception פנימי של סביבת הריצה של גו. שההנחיה היא לא להשתמש בו – אולי רק במקרים קיצוניים. האם יש במקרה זה צידוק להשתמש ב panic?
    מצד אחד: כאשר משתמשים ב type assertion מבלי לשמור את ערך ההחזרה השני ("ok") – גו תזרוק panic.
    מצד שני: זה לא המקרה אצלנו, ויש לנו בחירה מה לעשות.
    זהו דיון ראוי שאינני רוצה לנהל כרגע. בעיקר רציתי לציין את התנהגות ה panic של ה type assertion.
משהו היה מוזר לכם בדוגמה לעיל? כמובן – הקונבנציה בגו היא להימנע מ if else if!
בשפה יש אופרטור דומה מאוד ל type assertion הנקרא type switch.

הנה דוגמת הקוד כאשר אנו משתמשים ב type switch:

  1. הביטוי הוא דיי דומה, כאשר בתוך הסוגריים אנו מציינים את המילה השמורה type. ביטוי זה יכול לעבוד רק כחלק מ ל switch statement.
  2. זה idiom מעט מבלבל: יכולנו לכתוב (val := x.(type ואז להשתמש בהמשך הפונקציה ב val – מה שהיה מפחית כנראה את הבלבול.
    בשפת Go ביטוי switch מגדיר scope משלו, scope שמתחיל שמתחיל מייד לאחר המילה switch. לכן ניתן להגדיר (x := x.(type, כאשר ה x הימני הוא זה שהוגדר ב scope הפונקציה, מטיפוס {}interface, וה x השמאלי הוא ב scope של ה switch בלבד – ויוכר כטיפוס שהוא בפועל. זה ה idiom.
  3. המשך הפונקציה כנראה ברורה – וגם פשוטה יותר מהגרסה הקודמת.

סיכום קצר

ממשקים הוא אחד מהיסודות החשובים בשפת Go. אם אתם מגיעים משפת C – זהו אלמנט חדש. אם אתם מגיעים מ ++C, ג'אווה או פייטון – הוא כנראה מוכר יותר, אך עדיין מעט שונה.
מבנים והרכבת טיפוסים – גם הם חשובים, ומשמשים ככלי מרכזי לפתרון בעיות ב Go.

לאחר פוסט זה אמור להיות לכם מושג דיי טוב על תיאור "אובייקטים" וממשקים בשפת גו – והשימוש בהם.

שיהיה בהצלחה!

—-

לינקים רלוונטיים

Drill Down על ממשקים בגו:
פוסט של Jordan Relly (כיסינו את הרוב בפוסט) ופוסט של Russ Cox (ממש כיצד מומשו בשפה).

מבוא ראשוני לשפת Go – חלק ב' (מבנים בסיסיים, וקצת סגנון)

זהו פוסט המשך שני, לפוסט מבוא ראשוני לשפת Go למפתחים ותיקים.
נמשיך לבחון את הרכיבים של שפת גו, בהתבסס על מה שעברנו עליו בפוסט הקודם.

לולאות

  1. פקודת for יכולה להכיל תנאי בינארי יחיד – ואז היא מתנהגת כמו while בשפת ג'אווה. ניתן להשמיט את התנאי בכלל – על מנת לקבל התנהגות "while true".
  2. הצורה הקלאסית – גם היא נתמכת. אין צורך בסוגריים.
  3. ניתן להשתמש בצורה הקלאסית עם השמה מרובה. הלופ הוא על המשתנה x, אך במקביל אנו מקדמים גם את הערך של משתנה y.
  4. בלולאת for ניתן להשתמש ב break ו continue בדומה לג'אווה. שימו לב שאין צורך בסוגריים מסביב לתנאי ה if, אך יש צורך בסוגריים מסולסלים לתוצאת התנאי (אפילו אם זה ביטוי קצר). אני בעד!
    1. אם יש לנו לופ מקונן, ניתן להגדיר Label ואז לציין בפקודת ה break – את הרמה אליה אנו רוצים לעשות break.
  5.  במקום להשתמש בהשמה מרובה – השתמשתי כאן ב map (המקבילה של HashMap). הטיפוס שבסוגריים מבטא את טיפוס ה key, והטיפוס אחריו – את טיפוס ה value.
  6. השתמשתי ב range על מנת "לטייל" על ה map. אני מעוניין רק ב values ולא ב key, אז שלחתי את ה key ל bulk identifier.
  7. שימו לב: הפקודה range תטייל על ה map בסדר מעט אקראי. זהו פיצ'ר של השפה שנועד להזכיר לנו של Map (או Dictionary)  – אין באמת סדר.

פונקציות

  1. הגדרת הפונקציה main, שכבר מוכרת לנו. בשפת גו אין ל main פרמטרים או ערך החזרה (כמו ב C, או ג'אווה).
    המילה השמורה להגדרת פונקציה היא func. לא עוד function ארוך או def בפייטון שנשמע כמו "death". אהבתי! חבל שלא קיצרו עוד יותר וקראו לה fun…
  2. לפונקציה יכול להיות ערך החזרה, הוא מוגדר מימין.
    למה מימין? האם זה לא קצת מוזר? הסיבה היא עבור מקרים בהם חתימת הפונקציה היא מורכבת:

    1. הנה תראו את הפונקציה שמקבלת כפרמטרים "פונקציה שמקבלת 2 ints ומחזירה int" ו int, ומחזירה "פונקציה שמקבלת 2 ints ומחזירה int". לוגי, וקריא בצורה מפתיעה – לא?
    2. הנה דוגמה שלילית בה הגדרנו את טיפוס ההחזרה בהתחלה (נניח, כמו בג'אווה). האם אתם מסוגלים לקרוא את זה בלי מאמץ ניכר?
  3. הפונקציה יכולה לקבל כמובן כמה פרמטרים. אם הם מאותו טיפוס – ניתן לקצר.
  4. ניתן לכתוב גם בכתיבה מלאה. float32 הוא 32 ביט – כך שלא צריך להיזכר כל פעם מהו "double".
  5. ניתן להחזיר מפונקציה מספר ערכים. במידה ויש יותר מערך החזרה אחד – יש לשים את ערכי ההחזרה בסוגריים.
  6. ניתן לתת שמות לערכי ההחזרה (למשל s1, s2). השמות הללו יוגדרו כמשתנים בפונקציה ולכן אינם יכולים לחפוף לשמות הפרמטרים. הגדרת שמות לערכי ההחזרה מאפשרת לנו להשתמש ב naked return שמחזיר את ערכי המשתנים הרלוונטיים. שימוש אפשרי הוא ערך שאפשר לקבוע בכל מקום בפונקציה, ולעשות return בלי לציין אותו במפורש (למשל error, אבל לא רק).
    הערה: השמות היא שימושיים רק ב scope של הפונקציה, ולא כערך החזרה / חתימה של הפונקציה.
  7. פונקציה שמקבלת מספר לא מוגבל של פרמטרים נקראת variadic function. המשתנה שיכיל את "שאר הפרמטרים" מוגדר ע"י שלוש נקודות והטיפוס הרצוי (כמו varargs בשפת ג'אווה). בפועל זה יהיה slice של אותו הטיפוס (מייד נגדיר לעומק מהו slice), והוא חייב להיות הפרמטר האחרון בחתימה הפונקציה – כמובן.
ערכים בגו תמיד מועברים לפונקציה by value (כלומר: כעותק של הנתון). אם רוצים להעביר משתנה by reference – יש להשתמש בפויינטר (& בכדי לקבל פויינטר מערך, ו * בכדי להתייחס לערך שהפויינטר מייצג – כמו בשפת C).
פרימיטיביים מספריים בשפת גו

מערכים ו Slices

מערכים שפת Go הם כמו שאנו מכירים אותם משפת ג'אווה: הם מוגדרים לטיפוס מסוים (מערך של int, מערך של Person) ואורכם הוא קבוע. האיברים ממוינים ע"פ אינדקס שמתחיל ב 0.

Slices הם יותר כמו ArrayList – ניתן דינאמית להוסיף / להסיר איברים מהם.
כמו בשפת ג'אווה – Slices ממומשים על גבי מערך. בעצם ה slice הוא כמו View על ה Array:

מקור: הספר החינמי Build web application with Golang

מבנה הנתונים של Slice מכיל בעצמו:

  • שם
  • מצביע למערך
  • טיפוס האובייקטים במערך
  • offset
  • length של ה slice
כל האובייקטים עצמם מאוחסנים בעצם רק במערך – לא ב slice. זהו גם מקור השם slice.
אם אתם זוכרים – כל הערכים מועברים בשפת גו by value. אם אתם רוצים להעביר reference – עליכם להעביר פויינטר.
Slices הם סוג של פויינטר. ההעתקה שלהם היא זולה (זה struct קטן עם 5 שדות) ולכן ניתן להעביר אותם בקלות "by value", אבל לקבל התנהגות, על מערך הנתונים, של העברה "by reference". שינוי של ערך ב Slice ישנה כמובן את הערך במערך – בו שמורים הנתונים. ניתן להחזיק כמה slices על גבי אותו המערך, כך ששינוי ערך ב slice אחד ישפיע על ערכים של slices אחרים מעל אותו המערך.
בואו נראה קצת קוד:
  1. הדרך ה"פורמלית" להגדיר slice הוא בעזרת פקודת make (המייצרת עוד כמה מבנים בשפת גו). הפרמטרים שאנו שולחים הם:
    1. טיפוס – כדי ליצור slice עלינו לציין "מערך" של טיפוס, במקרה שלנו int[].
    2. length – אורך ה Slice שאנו רוצים לייצר.
    3. Capacity – אורך המערך שגו תייצר, מאחורי הקלעים, עבור ה slice. ה slice לא יכול לחיות ללא מערך מאחוריו, כמו ש ArrayList או Vector לא יכולים לחיות ללא מערך (למי שמכיר את המימוש).
  2. אנו בודקים את תוצר היצירה שלנו
    1. len היא פקודה בשפה שבודקת אורך של מבנה, במקרה הזה ה slince
    2. cap (קיצור של capacity) היא פקודה בשפה שבודקת את אורך התכולה השל המבנה – במקרה שלנו: אורך המערך.
  3. צורת הגדרה אחרת היא ה slice literal.
    תחביר ההגדרה דומה מאוד להגדרה של מערך, ההבדל הוא שבהגדרה של מערך עלינו לציין בסוגריים את מספר האיברים (או אפשר פשוט לכתוב שלוש נקודות )
  4. במקרה זה גו ייצור מערך בדיוק באורך של ה slice (כלומר: 5 איברים).
  5. כעת אנו מבצעים פעולת slice על ה slice שלנו. נזכיר שהאינדקס לפני ה סימן : בפעולה – הוא איבר שייכלל ב slice שייווצר, והאינדקס שלאחר סימן ה : – לא ייכלל ב slice שייווצר.
  6. התוצאה יכולה לרגע מעט להפתיע – אך היא הגיונית: הנה תרשים שיסביר אותה:

האורך של sliceOfSlice הוא אכן 3. ה capacity שלו הוא 4 מכיוון שיש לו עוד תא אחד לגדול במערך.

הוספת איברים ל slice

ציינו, קודם לכן, ש Slice הוא המקבילה בשפת גו ל ArrayList. עד רגע זה לא ראינו כיצד מוסיפים איברים. הנה דוגמת קוד:

  1. אנו יוצרים מערך של int בגודל 4, ומאתחלים אותו בערכים (השמה מרובה). יכולתי להשתמש בתחביר של array literal, אך מכיוון שהוא דומה כ"כ ל slice literal בחרתי בצורה המגושמת, אך המפורשת יותר.
  2. מתוך המערך, אנו מבצעים פעולת גזירה – שיוצרת את slice. להזכיר: האינדקס הראשון כלול, והאינדקס השני לא. תחביר מקוצר לפעולה היה יכול להיות [2:]
  3. כפי שאנו רואים slice הוא בגודל 2, וה capacity שלו הוא 4 – בגלל המערך שעליו הוא מבוסס.
  4. אם אנו מציבים ערך במערך – הוא משתקף מיד ב slice. המערך הוא האחסון של ה slice.
  5. כאשר אנו מוסיפים איבר ל slice, היכן הוא יאוחסן? – במערך, כל עוד הוא גדול מספיק.
    המשמעות היא שאנו דורסים את הערך הישן (100) בערך שנוסף ל slice. בעבודה עם מספר slices מעל אותו מערך – יש סכנה של דריסת ערכים הדדית.
  6. הנה אני מדפיס את מצב המערך. v% הוא הסימן להציג את הטיפוס בפורמט ברירת-המחדל שלו. זו תצוגה טובה לצורך debugging.
  7. ה slice כרגע בגודל 3 ו capacity של 4. מה יקרה אם נוסיף ל slice שני איברים, מערך ל capacity הנתון?
    הפעולה מצליחה, ואנו רואים שה capacity קפץ ל 8 (כמו Vector ב ++C, ג'אווה, וכו' – מגדילים את שטח האחסון פי 2).
  8. כפי שאנו רואים ה slice גדל, אך לא המערך. מערך איננו יכול לגדול – בהגדרה.
    פקודת append הגדירה מערך חדש גדול פי 2, והעתיקה אליו את כל הערכים הקיימים. הקשר בין slice ל array ניתק – ו slice עכשיו מצביע (ומאחסן נתונים) במערך חדש שהוקצה עבורו.
  9. הנה וידוא נוסף, שזה אכן מה שהתרחש: אנו מציבים ערך ב array – ורואים שהוא לא השפיע על הערך ב slice.

כפי שאתם רואים, ה slice מכסה לנו את ההתנהגות הכל-כך נדרשת של ArrayList.

Slice הוא בבירור האלמנט השימושי יותר בשפה, איתו תעבדו רוב הזמן – ולא עם מערכים. חשוב אבל להבין כיצד slices עובדים.
כאשר מנהלים slices גדולים, כדאי לשים לב ש "slice של איברים ספורים" לא מחזיק בזיכרון מערך של אלפי תאים.

איך מסירים איבר מ slice? הרי המערך הוא קבוע ורציף בזיכרון… הנה דוגמה:

  1. הדרך להסיר איברים היא בעצם ליצור slice חדש, שיכיל פחות איברים.
    התחביר של הוא expansion שהופך את ה slice לרשימה של פרמטרים – הצורה לה הפונקציה append מצפה.
  2. אנו יכולים לראות שהפעולה הצליחה והמספר 10 נמחק מהרשימה.
    מדוע, אם כן, ל slice יש capacity של 4?
  3. בגלל שפקודת append לא יצרה מערך חדש. היא בכלל לא מודעת לזה שביצענו מחיקה.
    כמו שראינו את הדוגמה הקודמת – הוספה של איבר ל slice מוסיף את הערך במקום הבא במערך – אם יש מקום לכך.

Structs

Struct הוא טיפוס מורכב להרכבה אישית. אם אנו מתארים Struct של אדם הוא יכול לכלול מחרוזת עם שם ושם משפחה, מספר זהות, ותאריך לידה.

אפשר לחשוב על Struct כאובייקט ללא מתודות – רק members.

  1. אנו מגדירים struct חדש בעזרת המילים השמורות type ו struct. בניגוד לג'אווה או #C בהם מקובל ששם של טיפוס (מחלקה, struct ב #C) מתחיל באות גדולה, בשפת גו האות הגדולה מציינת האם הטיפוס הוא פרטי לחבילה (אות קטנה) או ציבורי (אות גדולה)
    1. בתוך ה struct אנו מגדירים את השדות (נקראים fields).
  2. יש לנו 3 דרכים ליצור instance חדש של ה struct שהגדרנו
    1. ע״י שימוש ב var – ה struct יאותחל לערכי 0.
      שפת גו לא מגדירה אם בצורה זו האובייקט ייווצר על ה stack או על ה heap – זה פרט מימוש של הקומפיילר. בד״כ הקומפיילר יבצע תהליך שנקרא escape analysis ויראה אם יש חובה להגדיר את המופע, במקרה הספציפי, על ה heap. העדפת ברירת המחדל היא להגדיר אובייקטים על ה stack (מכיוון ששחרור הזיכרון הוא אפקטיבי יותר).
    2. ע״י שימוש במילה השמורה new, שיגרום ל:
      1. האובייקט ייווצר על ה heap. כל הערכים יאותחלו לערכי אפס.
      2. המשתנה sprite2 יכיל פויינטר לאובייקט, ולא את האובייקט עצמו.
    3. ע״י צורת ה shorthand של יצירה והשמת ערכים. בצורה זו אנו מציבים ערכים בעצמנו. כמו צורה #1 – אין הבטחה אם האובייקט ייווצר על ה stack או ה heap.
כמה מלים על קונבנציית השמות בשפה:
  • שם חבילה (package) משמש כ namespace דרכו ניגשים לאלמנטים בחבילה. על כן חשוב שזה יהיה שם קצר וברור. מקובל ששם לחבילה היא מילה בודדת באותיות קטנות, למשל "strings".
    • כנ"ל לגבי שמות התיקיות המכילות את החבילה.
      • הנה פרויקט של ה NYTimes שהחליטו לשבור את הכלל הזה – פשוט כי קשה להם לקרוא את השם כ nytimes. הם לא רגילים. כל כלל ניתן לשבירה, אם מוכנים לשלם את המחיר (כרגע זה מציף כמה מקרי קצה ב tooling של go).
  • שלפנים (Getters/Setters) – אלו לא כ"כ מקובלים בשפה, אך אם אתם משתמשים בהם – עליהם להיות כשם ה filed עם אות ראשונה גדולה.
    • השדה: user (אות ראשונה קטנה = פרטי לחבילה)
    • השלפן: User (אות ראשונה גדולה = ציבורי)
  • כל הטיפוסים / משתנים האחרים הם ב MixedCaps, שזו צורה כמו CamelCase, עם ההחרגה שהאות הראשונה מציינת את הנראות.
    • טיפוס/משתנה פרטי בחבילה: myUniqueType
    • טיפוס/משתנה ציבורי: MyUniqueType
  • ממשקים (interfaces) עם מתודה יחידה יקראו כמו שם המתודה, עם er של "התמחות":
    • ממשק Reader המכיל את המתודה Read.
    • ממשק Shopper המכיל את המתודה Shop.
    • לא הוגדר, אך אני הייתי קורא לממשק עם מתודה יחידה ReadString בשם StringReader.
  • אפשר למצוא עוד פירוט על ההגדרות הללו ב Effective Go.
Gopher עם סטייל

עניין של סגנון

אז איך סגנון הכתיבה "הנכון" או "המקובל" בשפת Go?
כיצד מתכנני השפה התכוונו שנכתוב תכניות, או אולי אפשר לשאול: כיצד המתכנתים הממש טובים כותבים קוד?
ראשית, ניתן לשים לב שסגנון הקוד ב Go הוא דיי דומה בין תכנית לתכנית. יש לכך כמה סיבות מרכזיות:
  • הקומפיילר ו gofmt (שאני מקווה שרוב ה IDEs עובדים איתה) – מכתיבים חלק גדול מהסגנון. רוצים סוגריים מסולסלים בשורה חדשה? – אין.
  • שפת Go היא מינימליסטית: אין Generics, ואין Meta-programming – כלים שלרוב הם הבסיס ליצירת סגנונות בשפה (ע"י Frameworks, בד"כ). אין הורשה, אין מחלקות מקוננות, אין מחלקות אבסטרקטיות. בעצם: אין מחלקות. כל אלו, כלים שעוזרים להגדיר "DSL" על גבי השפה – ואין אותם ב Go.
  • הספריות הסטנדרטיות של שפת גו מכסות מרחב גדול של נושאים, שלרוב מגיעים כספריות צד-שלישי: בדיקות (יחידה/אינטגרציה/APIs, ועוד), Templating, עבודה עם HTTP, שרת ווב, טיפול ב JSON/XML, ועוד.
    ספריות, ובעצם ה APIs שלהם – נוטים להכתיב חלק גדול מהסגנון הכתיבה שלנו. אם כולם משתמשים באותן ספריות – הקוד הוא יפה.
ניתן לראות תכונה זו של השפה כ"אובדן אינדוודואליזם", או מצד שני – כ"היופי שבפרגמטיות". כתיבת קוד שיהיה עקבי במערכת גדולה (גוגל) – היה אחת ממטרות השפה. אני באופן אישי, אוהב את ה trade-off שנלקח – ומעדיף שפה מגבילה, אך שמכתיבה סגנון אחיד.בכל זאת, יש כמה ענייני סגנון מקובלים ב Go, שאינם מוכתבים או אפילו מושפעים מהגורמים הנ"ל. זו הקהילה שמאמצת אותם. הנה כמה:

  1. נהוג להשתמש בתחביר של if עם השמה מרובה, ובדיקת שגיאה  -באותה השורה. לרוב עבור טיפול בשגיאות.
  2. מה שלא נהוג לעשות הוא להיכנס לקינון של if או בלוקים מסוג אחר. יש הרבה מפתחי ג'אווה שכותבים כך קוד – אז זה מצב לא רצוי ב Go.
  3. מה שיש לחתור אליו הוא להישאר, עד כמה שאפשר, ברמת אינדנטציה מינימלית. זה אומר:
    1. לפצל פונקציות לפונקציות קטנות יותר.
    2. במקרה לעיל (#2) הסיבה לכתוב קוד בתוך ה else היה ה scope בו הוגדר המשתנה err. אין בעיה: ניתן להגדיר אותו ב scope רחב יותר – ואז להיפטר מהאינדנטציה הנוספת.
    3. עוד דבר שמצמצם את כמות שורות הקוד באינדנטציה במשפטי if הוא, לטפל קודם במקרה הפשוט (במקרה שלנו: return x – שורה אחת באינדנטציה), ואז במקרה המורכב יותר – מחוץ ל if (במקרה שלנו: הטיפול בשגיאה – 4 שורות שעכשיו אינן באינדנטציה).
הרעיון הזה הוא לא מקורי. הוא אפילו לא חדש: ניתן למצוא אותו בספר Code Complete (עליו כתבתי פוסט) שנכתב אי שם בשנות ה-90 המוקדמות. פשוט הקהילה של גו (כנראה בעקבות צורת הקוד בה כתובות הספריות של גו) – החליטה לאמץ אותו.
את ה import נהוג לסדר בקבוצות, שמפרידות ביניהן רווח – כאשר הקבוצה הראשונה היא של הספריות הסטנדרטיות:
לא מופרך להאמין שבגו תשתמשו ביחידות זמן שונות (שניות, מילי-שניות, ננו-שניות). במקום ליצור (קבוע?) עם ערך מספרי ושם או הערה שלא תסביר אותו היטב, יש בשפה טיפוס בשם time.Duration שהופך את ההגדרה להרבה יותר אלגנטית:
עוד כמה נקודות קצרות:

  • מתכנתי Go אוהבים לכתוב בדיקות-יחידה, והרבה!
    • עושים הבחנה ברורה בין בדיקות יחידה (שם הפונקציה מתחיל ב Test), בדיקות ביצועים (שם הפונקציה מתחיל ב Benchmark) ובדיקות קבלה / לצורך תיעוד (שם הפונקציה מתחיל ב Example).
  • עדיף להשתמש ב switch ולא ב if else if.
    • אני במקור דווקא מחבב את if else if, אבל ב Go אין צורך ב break – אנסה באמת לזרום עם מה שמקובל.
  • עדיף להחזיר כמה ערכים מפונקציה, ולא להשתמש בפרמטר שהוא pointer כ "out value" (כמו שמקובל ב #C).
ניתן למצוא עוד טיפים והנחיות בלינקים שבתחתית הפוסט.

סיכום קצר

Go היא שפה פשוטה, אך ללמוד שפה בצורה מעמיקה זו לא משימה קצרה.
בפוסט זה כיסינו מבנים בסיסיים (לולאות, פונקציות, slices, ו structs), וגם דנו קצת בסגנון הכתיבה המקובל בשפת Go.
יש עוד כמה נושאים חשובים, אולי אמשיך אותם בפוסט המשך.

שיהיה בהצלחה!

—-

לינקים רלוונטיים

Slice Tricks in Go – לא ממש טריקים, אלא ריכוז הפעולות השונות על slices.

Best practices for a new Go developer – המלצות משורה של מתכנתים מנוסים ב Go, על מה כדאי לשים לב כשנכנסים לשפה. דוגמאות:

  • אנשים חדשים נוטים לעשות overuse למודל ה concurrency של Go (כי הוא "כ'כ מגניב"?!).
  • נסו להתמקד ולהבין את מודל ה interfaces לעומק – זה לב השפה (רלוונטי למי שלא הגיע מג'אווה או #C).
  • השתמשו ב gofmt, כהרגל – כבר מההתחלה.

twelve Go best practices – גם נחמד

50 גוונים של Go – טעויות נפוצות של מתכנתים מתחילים בגו. מקום טוב ללמוד ממנו "פינות" של השפה.

Code Review Comments – מדריך לכתיבת הערות בגו

ברומא, התנהג כרומאי, Organizing Go Code, שפת גו – ל Gophers  – מצגות טובות על סגנון כתיבה מקובל בגו.

מבוא ראשוני לשפת Go, למפתחים ותיקים

בחברת 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.

  • שתי השפות יכולות להתמודד עם ה throughput הגבוה שנדרש לנו.
  • בשתיהן יהיה על רוב המתכנתים אצלנו ללמוד שפה (יותר מזה: סביבת-ריצה) חדשה.
  • לשפת ג'אווה (החלק החשוב: ה JVM) יש הרבה יותר כלים, בשלים ומוכרים. שפת Go היא עוד מתפתחת. למשל: אין לה עדיין ספריה סטנדרטית ל New Relic, וה SDK של AWS שוחרר רק לפני כחודש.
  • בשפת Go היה לנו הרבה פחות דילמות לגבי Frameworks. השפה וגם הספריות / Frameworks הזמינים בה הם דיי Low Level ושקופים למשתמש – מה שמגביר ומשפר את הלמידה. (את דעתי בנושא אפשר ללמוד בפוסט: מהו ה Framework הטוב ביותר)
חלק מהמפתחים שהתייעצנו איתם לא הרגישו נוח עם השימוש ב Java8. הרתיעה משפת ג'אווה הוא דבר נפוץ למדי בקהילת הרובי – קשה לי להסביר בדיוק למה.

הפתיחות לשפת Go הייתה שונה לחלוטין – וחיובית בעיקרה. גם בקרב המפתחים, וגם בקרב המנהלים.

בסופו של דבר, ההתאמה הארגונית – היא שהכריעה. Go היא שפה שרבים הרגישו שמתאימה יותר ל DNA הארגוני של החברה: שפה חלוצית (עדיין), פשוטה, מודרנית ומגניבה. עם החולשות שלה (שיש, כמובן) – אנחנו נתמודד ביחד, כארגון.

הערה: פוסט זה יוצא מנקודת הנחה שאתם מפתחים מנוסים, מכירים את שפת ג'אווה, ואולי קצת C ופייטון.

הלוגו של שפת Go – ה Gopher (סנאי ערבה). אין קשר לפרוטוקול Gopher (המתחרה הקדום של FTP). gopher הוא גם כינוי למתכנת Go.

הצצה ראשונית לשפת Go

מה יותר טוב מלראות מעט קוד?
הקוד (הפשוט) הזה הוא web server מינימלי שמחזיר את הטקסט "!Hello world".
  1. הגדרה של package בתוכנית שלנו.
    הקונבנציה לשמות של package בשפת Go היא מילה בודדת ב lowercase, וללא קווים תחתונים. שם החבילה (package) אמור להתאים לשם התיקייה בו הקובץ נמצא.
    אזהרה: אם לא תעקבו אחר כללים אלו, התוכנית עלולה לא להתקמפל, וללא אזהרות מיוחדות [ב].
  2. אנחנו מייבאים 2 חבילות סטנדרטיות של Go: לטיפול ב I/O (כללי), וטיפול ברשת.
    התחביר של הסוגריים (שמייתר את הצורך בפסיק או נקודה פסיק) נקרא Grouping Declaration.
  3. פונקציית main היא זו שממנה התוכנה מתחילה לפעול. עליה להיות שייכת גם לחבילה בשם "main".
  4. קריאה למתודה HandleFunc רושמת כניסה חדשה ב DefaultServeMux.
    Mux, בטרמינולוגיה של Go היא כמו Router ב MVC: אובייקט שתפקידו למען בקשה לאובייקט (במקרה שלנו: פונקציה) הנכונה שתטפל בה.
    אנו רושמים את ה default path (כלומר: "/") למתודה sayHello.
  5. המתודה ListenAndServe מפעילה את "שרת הווב" של Go, ורושמת אותו ל Port ו Mux.
    מכיוון שלא ציינו Mux (ה nil) – יעשה שימוש ב DefaultServeMux.
  6. הפונקציה sayHello היא פונקציה פשוטה. עצם כך שהאות הראשונה בשמה היא אות lowercase גורם לפונקציה להיות private. ליתר דיוק: להיות זמינה רק באותה החבילה, כמו default visibility בשפת ג'אווה.
    מתודה או טיפוס שמתחיל באות גדולה הוא "exported" – כלומר זמין לחבילות אחרות (כמו public בג'אווה).
  7. אנו רואים שאנו מקבלים את הפרמטר מסוג http.Request כפויינטר (מצביע). פויינטר הוא הדרך להעביר אובייקט by reference – אך אין "pointer calculus" כמו בשפת C. כלומר: לא מבצעים פעולות חשבוניות על הכתובת שבפויינטר.
  8. המתודה 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 רלוונטי

הגדרת משתנים

בואו נמשיך להתבונן בקוד, ונגש ליסודות של כל שפה – הגדרת משתנים:
  1. בשפת Go, מגדירים משתנה בעזרת המילה השמורה var. קודם מופיע שם המשתנה –  ורק אז הטיפוס (הפוך מ Java או C – מה שמבלבל בהתחלה).
  2. ניתן באותה השורה גם לאתחל ערך. אם לא מאתחלים ערך, הקומפיילר של Go יקבע "ערך אפס" (המספר 0 עבור מספר, nil עבור אובייקט, וכו')
  3. בהשראת Python – מנסים ב Go לייתר הגדרות לא הכרחיות: אם אנו מציבים ערך ברור (במקרה שלנו: 1 שהוא int) אז הקומפיילר של Go יקבע את הטיפוס בעצמו ע"פ כללים מסוימים (במקרה שלנו: int).
  4. ניתן באופן מקוצר לבצע כמה הגדרות של משתנים כ Grouping declaration.
  5. בתוך פונקציה, הגדרת משתנה שלא בשימוש (במקרה שלנו: f) – היא שגיאת קומפילציה.
  6. בתוך פונקציה, ניתן להשתמש בתחביר מקוצר =: המייתר את השימוש ב var, במידה ואנו מציבים ערך במשתנה.
הנה עוד כמה התנהגויות:
  1. ב Go אין casting אוטומטי של טיפוסים. חיבור של int ו float גורר שגיאת קומפילציה. הגישה של Go היא "אנחנו לא רוצים אקראיות – תגדיר בדיוק למה התכוונת". זו גישה מאוד בוגרת – לשפה "צעירה ומגניבה".
  2. ניתן לבצע casting בקלות יחסית (הסוגריים הם על המשתנה ולא על הטיפוס – הפוך מג'אווה), ולקבל תוצאות בהתאם.
  3. ישנם גם קבועים. הם דומים להגדרת משתנים ב var, אך לא ניתן להשתמש בתחביר המקוצר =:, וכמובן שלא ניתן לדרוס את הערך לאחר שהוגדר. נסיון לדרוס קבוע – מסתיים בשגיאת קומפליציה, כמובן.
  4. אם אתם לא מעוניינים להשתמש כרגע במשתנה, אבל גם לא רוצים שגיאת קומפילציה – הציבו את ערך המשתנה בתוך ה blank identifier שהוא קו תחתון. ה blank identifier הוא משתנה לכתיבה בלבד, כאשר רוצים להיפטר מערך כלשהו. ממש כמו dev/null/ בלינוקס.

טיפול בסיסי במחרוזות

הנה כמה תכונות בשפה שבוודאי יקסמו למשתמשי פייטון (וגם לאחרים):

  1. ניתן להגדיר טווח בסוגריים, בכדי לבצוע sub_string מתוך מחרוזת.
  2. for … range הוא ה "foreach" של שפת גו. ניתן להשתמש ב range על מערך, slice (חלק ממערך), מחרוזת, map או channel (קונסטרנט לתקשורת בין goroutines, נגיע אליהן בהמשך).
    range בעצם מחזיר 2 ערכים: key ו value, מה שלא מוצג בדוגמה למעלה (יש שימוש רק ב key, שהוא האינדקס של ה char במחרוזת).
  3. ב go יש שימוש נרחב ב Printf, לביצוע הדפסה עם formatting – ממש כמו בשפת C. הפקודה Printf מתחילה באות גדולה – מכיוון שהפונקציה היא public לחבילת "fmt". ניתן לגשת ל character במחרוזת בעזרת אינדקס (כמו שפות רבות אחרות).
  4. איזה כיף! יש הגדרה של מחרוזת  multi-line!
    שימו לב ששורה 2 עד 4 יכילו מספר רווחים לפני הטקסט, כי העימוד מתחיל מעמודה 0 ולא מהעימוד של שורת הקוד הקודמת (התנהגות זהה לפייטון, אם אני זוכר נכון).

Result מרובה וטיפול בסיסי בשגיאות

  1. הפונקציה Printf מחזירה כתשובה את אורך המחרוזת בבייטים. בנוסף: היא מחזירה שגיאה אם הייתה בעיה בכתיבה (nil במידה ואין שגיאה).
    השמה מרובה נעשית כמו בפייטון, עם פסיק בין הערכים.
  2. קוד לדוגמה בו אנו בודקים את השגיאה ומתנהגים בהתאם.
  3. הנה כתיבה מקוצרת ומקובלת למדי בשפה: אנו מבצעים את הפעלת הפונקציה וההשמה בתוך משפט ה if התחום בסימן ; (תוחם statement). היא שקולה לסעיפים 1 + 2.
טיפול השגיאות בשפת Go הוא נושא שנוי במחלוקת:
– מצד אחד, האווירה בגו (אפס סובלנות של הקומפיילר למה שעשוי להיות שגיאה) מכוונת את המשתמשים לכיסוי גבוה של טיפול בשגיאות – מה שמוביל לקוד לא כ"כ אסתטי, שחוזר על עצמו ומעט מרגיז.
– מצד שני, גם בשפות כמו #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.

  1. בשלב זה אנו קוראים ל printWorld, אבל שימו לב למילה השמורה go: היא גורמת לפונקציה הרגילה לגמרי לרוץ כ goroutine על greenthread של סביבת הריצה של גו!
    קרוב לוודאי שיש קשר חזק בין שם השפה – לשם המילה השמורה go.
  2. הפונקציה (שרצה ב greenthread נפרד) תמתין עכשיו כ 2 שניות. זה לא יפריע לשאר התוכנית להמשיך ולרוץ.
    לאחר ההמתנה היא תדפיס את הטקסט "!World".
  3. בזמן ש printWorld ממתינה, אנו קוראים לפונקציה printHello, גם כ goroutine.
    יכולתי להגדיר את printHello כעוד פונקציה של ה package, אבל חבל לי להעמיס על המרחב הציבורי עבור פונקציה של שורה אחת.
    למה אני זקוק לעוד פונקציה עבור שורת קוד בודדה? כי אני רוצה להריץ אותה כ goroutine, כמובן!

    1. שפת גו מאפשרת להגדיר פונקציה ולהריץ אותה מיד, בעזרת כתיבת סוגריים מיד לאחר הגדרת הפונקציה – ממש כמו בשפת javaScript.
      זה התרגיל שעשיתי – ואני מקווה שלא הקשיתי מדי לעקוב (אני גם לא רוצה לשעמם…)
  4. פה יש קטע: מכיוון ש printHello ו printWorld רצות במקביל כ goroutines, הפונקציה main הגיעה לסופה – מה שיגרום לתוכנה להסתיים, ולא אוכל לראות את הפלט של התוכנית.
    אני יוצר המתנה יזומה של 3 שניות בפונקציה main (שלצורך העניין היא goroutine בפני עצמה), בכדי לתת דיי זמן ל printHello ו printWorld להתבצע בבטחה.
הפלט של התוכנית יהיה כמובן: "!Hello World" (כאשר המילה השניה מודפסת לאחר כ-2 שניות).
קצת מפריע לי שהייתי צריך להמתין כ 3 שניות שלמות, על פעולה שמסתיימת כנראה לאחר כ 2.000001 שניות, לערך. האם אין דרך נבונה יותר מ Sleep לסנכרן בין goroutines?!
בוודאי שיש:
  1. אנו יוצרים אובייקט מסוג WaitGroup שיקרא מעתה "המוניטור" (ע"ש דפוס העיצוב)
  2. אנו מודיעים למוניטור שתנאי הסיום שלו הוא 2 הודעות Done.
  3. כל אחת מהפונקציות printHello ו printWorld יודיעו למוניטור כשהן סיימו.
    אנו כמובן עושים זאת בדרך של גו: בעזרת המילה השמורה defer – שמבטאת רעיון יפה מאוד:
    התפקיד של defer היא כמו finally בשפת ג'אווה – לבצע דברים בסיום ההרצה של הפונקציה, גם במקרה סיום תקין וגם במקרה של סיום עם שגיאה.
    נקודת חולשה של finally היא שההקשר הסיבתי אבד: כל הפעולות שיש לבצע ביציאה מהפונקציה מרוכזות בבלוק ה finally ללא קשר ממה הן נובעות. הפקודה defer מאפשרת לנו להכריז על הפעולה שיש לבצע בעת יציאה מהפונקציה – בהקשר שקל להבין. ניתן לקרוא ל defer בכל מקום בפונקציה, והפעולה תמיד תרשם לזמן היציאה מהפונקציה. ממש יפה!
  4. אנו אומרים למוניטור להמתין לתנאי הסיום שלו.
פלט התוכנית הוא כמובן "!Hello World", והביצועים מצוינים: לא יותר מ 2.000001 השנייה!

בפוסט המשך, אמשיך ואצלול לתוך השפה.

שיהיה בהצלחה!

—-
לינקים רלוונטיים
לינק ל Effective Go – מדריך מהיר לשפה מבית גוגל. לינק ל 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 אמורים להופיע כפלט. אם לא הופיעו – סימן שמשהו בקומפילציה כשל.

Monitoring: מבוא ל Graphite ושימוש ב Time-Series

Graphite היא מערכת Open Source פופולארית למדי לניטור (Monitoring).
בצורה המקוצרת ביותר, היא מבצעת 2 פעולות בסיסיות:

  • אוספת מספרים שמשתנים לאורך זמן (להלן "time-series").
  • מציגה אותם כגרף.
המערכת מבוססת על עקרונות פשוטים למדי, ומסוגלת לטפל במליוני data points בשנייה.
Graphite זכתה לאימוץ נרחב בתעשייה, ככלי Monitoring חשוב. רבים משתמשים בה (ומספרים על זה): Github, Etsy, אובר, Electronic Arts, ועוד.הקהילה הגדולה הרחיבה את Graphite עם מרחב של פונקציות סטטיסטיות בעזרתן ניתן להגדיר את הגרפים: החל מממוצע או median, דרך ניתוח אחוזונים או סטיית תקן, ועד אלגוריתמים לחיזוי מסוג Holt-Winters.

מצד שני – צצו גם תלונות שונות על המערכת:

  • ניהול הנתונים כקבצים אינו יעיל כאשר מתשאלים הרבה מטריקות (metrics) במקביל. בהמשך נראה שזה מצב כמעט ובלתי נמנע…
  • ה UI הוא לא כ"כ יפה…
  • יש פונקציונליות חסרה, למשל: aggregation של נתונים, alerts, וגילוי אנומליות…
  • ההתקנה של Graphite היא לא פשוטה (הרבה תלויות), והתפעול השוטף שלה – גם עשוי להיות לא כ"כ פשוט.
מכיוון ש Graphite היא מערכת המורכבת ממודולים נפרדים עם APIs מוגדרים היטב – הצרכנים שהתלוננו פשוט החליפו חלקים שונים במערכת. בלי לשם לב, נוצרה אסופה גדולה של מודולים להחלפה, כאשר ה APIs שהגדירה Graphite  – הפכו לסוג של "סטנדרט" בתעשייה, אליו מתממשקים כלים רבים.

כיום כבר ניתן לראות הרבה תצורות בהן החליפו חלק מהרכיבים של ה "Graphite Stack", ואפילו תצורות ללא אף אחד מהרכיבים המקוריים. Graphite הוא עדיין אחד כלי ה Monitoring הנפוצים ביותר שקיימים.

אם אתם קוראים אדוקים של הבלוג, אתם בוודאי יודעים שאני מאוד אוהב כלי Monitoring בשם New Relic- בעבר כתבתי עליו פוסט.

מה הטעם להשתמש ב Graphite כאשר יש לנו כלי monitoring נהדר כמו New Relic?

  1. New Relic הוא כלי יקר [א], וייתכן שיש מערכות שלא משתלם לשלם עבורן את ה premium של New Relic.
  2. New Relic מגיש סט מאוד שימושי של מדדים וניתוחים, אבל לעתים אנו רוצים מדדים ש New Relic לא מספק.
    1. דוגמה קלאסית: מדדים עסקיים.
      בעולם של Gett: כמה הזמנות יש, כמה נהגים פנויים, כמה נוסעים שממתינים למונית כבר רבע שעה?
    2. ב New Relic קיימת פונקציונליות שדומה ל Graphite (עבור מערכות שמנוטרות ברישיון).
      New Relic מקשר את הנתונים בצורה קשיחה לאפליקציה, מה שעלול להקשות. אנחנו במקרה מסוים עזבנו אותו בנקודה זו לטובת Graphite, אם כי לא התאמצנו יותר מדי להשתמש בו. ייתכן ויש לזה פתרון.
Graphana – ה Dashboard ה"חם" בעולם ה Graphite כיום.

הארכיטקטורה של Graphite

בצורה פשטנית משהו, ניתן לתאר את הארכיטקטורה של Graphite בצורה הבאה:

  • ניתן לזהות ב Graphite שלושה רכיבי-על מרכזיים: whisper, carbon ו graphite-web.
  • המערכת (Application) שולחת נתונים של מטריקות (metrics) שונות ל carbon מעל tcp (פורט ברירת-מחדל: 2003).
    • הפורמט בו מועברים הנתונים הוא מאוד פשוט: some metric name : some value : timestamp.
  • קרבון שומר את הנתונים בבסיס הנתונים whisper, בסיס נתונים שמבוסס על קבצים ודומה לכלי ישן, יחסית, שנקרא RRD [ב].
    • whisper מנהל קובץ נפרד לכל מטריקה, הוא למעשה סוג של Time Series Database (פרטים בהמשך).
  • Graphite-web יודע לקבל קריאת GET (בעזרת endpoint בשם render/) כשהפרמטרים על ה URL מתארים query על מטריקה או מספר מטריקות.
    • למשל: הפרמטר הבא סוכם את כמות ה logins היומית:
target=summarize(my.metric.logins, "1d")
    • Graphite-web מרנדר PNG אותו הוא מגיש לדפדפן.
      הערה: זהו אלמנט של חוסר יעילות: היום קל יותר לקבל נתונים ולרנדר אותם ב javaScript – כך גם העבודה מתפזרת בין ה clients השונים.
Graphite כתובה ב Python, כאשר carbon ממומש ע"ג twisted (שהוא framework ל event-driven I/O) ו graphite-web כתוב ב django – ספריית MVC מפורסמת שדומה באופייה ל Rails.

Time Series Databases (להלן TSDB)

בשלב זה, ולפני שאנו צוללים לתמונת-מבט עמוקה יותר על הארכיטקטורה של Graphite, כדאי להבין מהם time-series – מכיוון שהם נמצאים בבסיסה של הארכיטקטורה של Graphite.

סדרה עתית (Time Series) היא סדרת נתונים, לרוב שנדגמו אחד לאחר השני, לרוב באינטרוולים קבועים, המסודרים לאורך ציר הזמן.
עצם הידיעה שמדובר בנתונים ששייכים לסדרה העתית, והם לא נתונים אקראיים מאפשרת לנו:

  1. לדחוס את הנתונים בצורה יעילה יותר.
  2. איתור מגמות (trends), מחזורים (cyclical fluctuation), ועונתיות (seasonal fluctuation) בנתונים.
  3. לנתח חריגות בנתונים (אנומליות).
  4. לנסות לחזות את המשך הסדרה.
  5. להיות מסוגלים לחשב (בדיוק כזה או אחר) את ההסתברות להתנהגויות עתידיות של הסדרה. למשל: הסבירות לקטסטרופה עתידית.
סדרות עתיות הן טובות לתאר מדדים של מזג אוויר, ונתונים כלכליים – מצד אחד, אך גם metrics של סביבת הפרודקשיין שלנו, וביצועים של מדדים עסקיים – מצד שני. ישנן טכניקות מתמטיות משמעותיות לניתוח נתונים של סדרות עתיות.

בסיסי נתונים רלציוניים (או KV/Document) מסוגלים בהחלט לנהל סדרות עתיות, במיוחד כאשר הם רצים על חומרה עם SSD (שמספקת ביצועים טובים בהרבה מדיסק מכאני לגישות אקראיות רבות).

נניח שאנו רוצים לאכסן סדרה עתית ב MySQL. באופן טבעי נרצה לצמצם את מספר הגישות לדיסק, ואת מספר הסריקות הארוכות של טבלאות בבסיס הנתונים (נקודות חולשה בביצועים של בסיס נתונים רלציוני). מימוש אפשרי הוא כזה:

  • ניצור טבלה לכל מדד (למשל: צריכת זכרון של שרת A, צריכת זכרון של שרת B, כמות פעולות IO של שרת A,… וכו')
  • בכל טבלה יהיו שתי עמודות: זמן, וערך המדד.
כאשר יש לנו *הרבה* נתונים (מה שקורה לעתים קרובות ב Monitoring), ניתן לבצע אחת מ-2 אופטימיזציות מקובלות:
  1. ליצור טבלה לא רק לכל מדד, אלא גם לכל מדד וטווח זמן – למשל מדד + תאריך (יום מסוים). כל יום יהיה טבלה חדשה.
    1. אם הטבלה מייצגת תאריך – אז שדה הזמן יכול עתה להיות מצומצם לשעה ביום (ברזולוציה הנדרשת: שניות או מילי-שניות, למשל), וכך לחסוך מקום רב של אכסון, בעיקר באינדקסים.
    2. התוצאה כמובן יכולה להיות שבמערכת יהיו לנו מאות או אלפי טבלאות שונות.
    3. לרוב מקובל לשמור את הערכים לטווח מוגדר (נאמר: 30 יום), ולמחוק טבלאות ישנות יותר.
  2. לצמצם את מספר השורות ע"י אכסון של טווח ערכים בכל שורה. למשל: יש לנו הרבה מדידות בשנייה (או בדקה), ואנו הופכים את הסכמה של כל שורה להיות: זמן, כמות דגימות, ממוצע דגימות, פיזור דגימות, ערך מקסימום, ערך מינימום ו BLOB עם כל ערכי הדגימות הספציפיים ומרכיב הזמן המתאים (שניה או מילי-שניה).
    1. אם עבור הרוב הגדול של השאילתות הרזולוציה אליה צמצמנו את הנתונים (נאמר: דקה) היא מספיק טובה, אז רוב השאילתות יוכלו לרוץ על סיכמוי הביניים (למשל: "ממוצע דגימות" ו "כמות דגימות") – מה שירוץ הרבה יותר מהר.
      1. צמצום שמצדיק את הטרחה הוא לרוב צמצום של כמה עשרות או מאות ערכים שונים או יותר – לתוך שורה בודדת.
      2. אם נרצה לגשת לכל הערכים המדויקים (כנראה שנרצה לעשות זאת ברזולוציה קטנה, למשל – כמה דקות) – כל הנתונים עדיים שם. לא יצרנו איבוד מידע.
דוגמה לדחיסה של נתוני סדרה עתית לרשומה / אובייקט יחיד. מקור: O'Rielly, באדיבות MapR.

כמובן שבמקום לנהל את הסדרה העתית בעצמנו – ניתן לבחור בבסיס נתונים שזו ההתמחות שלהם (ועושים בערך מה שתיארנו למעלה). בסיסי הנתונים העתיים המדוברים ביותר כיום הם:

  • Splunk (מוצר proprietary)
    • משמש בעיקר לאינדוקס וניתוח נתונים (לא ממש monitoring), יותר נכון להשוות אותו ל ELK Stack מאשר ל Graphite Stack (למרות שהוא נחשב מוצר TSDB).
  • InfluxDB (כתוב ב Go, יכול לחבר אותו ל Storage Engines שונים כמו LevelDB או RockDB).
    • ניתן לאפיין אותו בפשטות התקנה / תפעול. הוא מתיימר להחליף את כל ה Graphite Stack בעתיד, ובנוסף הוא תומך במודל של time-events, שהוא רחב יותר ממודל ה time-series הקלאסי (מה שבא קצת על חשבון יעילות אכסון וביצועים). אולי שווה להקדיש לו פוסט בעתיד…
  • OpenTSDB (הממומש מעל Hadoop HBase)
    • ניתן לאפיין אותו ביכולת לטפל בכמויות גדולות מאוד של נתונים, ובמהירות. הוא מהיר בסדר גודל מ InfluxDB – כאשר כמות הנתונים גדולה מאוד.
TSDB כוללים לרוב עוד כמה פונקציות מסביב למודל של סדרות עתיות:

  • downsampling – היכולת לשלוף נתונים ביעילות ברזולוציה קטנה יותר מזו שנשמרה (למשל: אנו שומרים מדד כל שניה, אך רוצים לשלוף את הערכים של פעם בחמש דקות).
  • ביצוע פעולות השוואה / חישוב בין סדרות עתיות שונות, או מקטעים שונים באותה סדרה עתית. למשל: מה ההבדל בין התנהגות המדד היום, להתנהגותו באותו היום – לפני שבוע?
  • אינדוקס הערכים ושליפה מהירה שלהם. למשל: שלוף את כל מקטעי הזמן בחודש האחרון בהם ה CPU utilization היה גבוה מ 70% – בצורה יעילה (ובהנחה שזה אירוע דיי נדיר).

סה"כ TSDBs כוללים tradeoff מאוד ברור: מודל נתונים פשוט ובסיסי של סדרות עתיות, עבורן ה TSDB יעיל בצורה לא-רגילה.

הנתונים שלכם הם קצת יותר מורכבים מסדרה עתית פשוטה? או שתעשו קצת תרגילים בכדי למדל אותם כ time-series (למשל: תשמרו את הנתונים ככמה סדרות עתיות שונות), או שתמצאו פתרון אחר שהוא יותר גמיש – אך פחות יעיל.

סה"כ אנו רואים היום מגמה של קצת פחות אופטימיזציה, וקצת יותר גמישות: ב RRD לא ניתן היה להוסיף נתונים שלא ע"פ הסדר, או שלא במרווחים קבועים. InfluxDB ו OpenTSDB מראים גמישות גבוהה הרבה יותר מזה.

סיכום

בפוסט זה הכרנו את Graphite, כלי (או אולי Stack) טכנולוגיות פופולארי מאוד, בקוד פתוח – לניטור מערכות. הכח שלו – הוא ביכולת ה customization: ניתן להשתמש בו ל System Monitoring, ל Application Monitoring ואף ל Business Performance Monitoring. הבסיס שלו הוא דיי פשוט ויעיל – מה שתרם לפופולאריות הרבה שלו.

תמונת הארכיטקטורה שהצגתי היא פשטנית, ואנסה לכתוב פוסט המשך בו אסביר קצת יותר על Graphite, כיצד הוא עובד, ועם אילו בעיות / trade-offs יש להתמודד עם סוג כזה של מערכת.

בינתיים, אתם יכולים לגשת ל-2 הלינקים העוסקים בארכיטקטורה למטה – הם לא-רעים.

שיהיה בהצלחה!

—-

[א] ע"פ מקורות זרים: כ 100$ לחודש לשרת. ע"פ מקורות זרים: ניתן להוריד את המחיר הנ"ל לבערך חצי – אם אתם צרכנים כבדים (יש לכם מאות שרתים מנוטרים). מה המחיר לאלפים? לרוב אם יש לכם יותר מכמה מאות שרתים – אתם לא תשלמו כבר ל NR, אלא תשתמשו ב open source כמו nagios עם הרחבות שלכם…

[ב] RRD הוא קיצור של Round Robin Database – על שם כך ששמר על קבצים בגודל קבוע בדיסק, ועשה שימוש חוזר בשטח שלהם.

—-

לינקים רלוונטיים

רשימת כלי 3rd-party שעובדים עם Graphite:
http://graphite.readthedocs.org/en/latest/tools.html

הארכיטקטורה של Graphite, כפי שמתוארת ע"י היוצר שלה, כריס דיוויס:
http://aosabook.org/en/graphite.html
תיאור טוב ממקור אחר:
https://grey-boundary.io/the-architecture-of-clustering-graphite/

דרכים למימוש סדרות עתיות על גבי בסיס נתונים רלציוני:
https://academy.datastax.com/demos/getting-started-time-series-data-modeling
http://dba.stackexchange.com/questions/7634/timeseries-sql-or-nosql

ספר מבית O'Reilly על TSDB, ו openTSDB בעיקר. ניתן לקבל בחינם במימון MapR (שמשקיעה ב OpenTSDB):
https://www.mapr.com/time-series-databases-new-ways-store-and-access-data

השוואה של 10 TSDBs:
https://blog.outlyer.com/top10-open-source-time-series-databases

Splunk vs. ELK
https://riskfocus.com/splunk-vs-elk-part-1-cost/ (והמשכים…)