2015-12-26

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




תגובה 1: