מה אני פחות אוהב ב golang

בשנים האחרונות אני רואה המון "תלונות" על שפת golang וכמה התחביר שלה נוראי.

אישית אני מאוד אוהב את השפה, ומעדיף לכתוב בה המון דברים.
יותר מזה, קשה לי מאוד עם מרבית הפוסטים השונים המביאים "תלונות".

הסיבה ל"קושי" הזה, היא שהתלונות אינן ענייניות, אלא תלונות בסגנון של "אני רגיל לבצע לולאת while ובשפת go יש רק לולאת for המחליפה הכל".

במידה והתלונה היתה "אני זקוק ללולאת while כי הצורה לבצע איטרציה עם לולאת ה for ב go מונעת ממני לבצע …", הייתי מאוד שמח לביקורת שכזו, כי היא יכולה לתרום להתפתחות השפה ומתן מענה למקרים שכנראה כיום צריך לעקם אותם בשביל להשיג את אותה המטרה.

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

זו לא רשימה מלאה, אבל כן רשימה שלדעתי מפריעה להרבה אנשים במצבים שונים.

עבודה עם channels

כאשר חושבים על coroutines (ב go השם או המונח הוא goroutines) יש יכולות לקבל יכולת לתקשר בצורה מאובטחת (מבחינת threads) עם משתנה מסוג channel שעליו מרכיבים טיפוס נתונים כלשהו.

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

ניתן "לסגור" את ה channel ואז ההקצאה שלו למעשה משוחררת וניסיון לעבוד איתו יגרום ל runtime error.

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

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

שימוש ב Interface

בשפת Go יש משהו הנקרא interface. למונח של interface מספר שימושים שונים, אלו הם העיקריים שבהם:

  1. יצוג טיפוס נתונים משתנה- לפעמים מוכר בשם variant בעולם התכנות.
  2. ליצור מבנה המייצג פונקציות למבנה נתונים או במילים אחרות ABI.
  3. יצירת הגדרה של Generics.

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

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

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

בעיה שנייה אני לוקח כערבון מוגבל, כי יכול להיות שאני מפספס משהו בהבנה שלי של המימוש של Generics (שהוא חדש מאוד בשפת Go) בשפה.

יש מצב שבו ניתן לקבל JSON שהוא מחרוזת בודדת, או רשימה של מחרוזות (אל תשאלו למה – משהו שנכפה עלי להתמודד איתו).
לא ניתן ליצור מערכת כזו עם Generics. כלומר ניתן ליצור הכרזה של תמיכה לזה, אבל הכרזה של Struct לא תדע להתמודד עם הנושא, ולכן צריך לממש struct אחד לכל סוג המקרים ולבצע המון "עיקומים" להתמודדות עם הנושא.
כלומר לא ניתן לדעת מראש מה ה JSON שיתקבל ואיך הוא יגיע אותו, ולמעשה ה generics לא פתר את הבעיה.

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

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

ביצוע Binding

אסיים את הפוסט בנושא הזה.

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

יש עם זה כל כך הרבה בעיות שרק זה דורש סדרת פוסטים משל עצמו.

אבל הנה מספר רשימות קטנות:

  • ניהול זיכרון זה דבר נורא, ולא כזה ברור מה ואיך קורה במצבים מסוימים.
  • "מחרוזות" זו עוד בעיה קשה ולא ברורה תמיד איך להמיר נכון בין תווי Go ותווי C ואיך המערכת תגיב באופן כללי כאשר יש תווי utf-8 (מחרוזות Go מקודדות ב UTF-8) וכו'. התיעוד לא מספק מענה לכלל המצבים שמגלים אותם רק תוך כדי עבודה על ביצוע ה binding.
  • כיצד לבצע נכון data alignment של מידע, למשל union, או "שמירת מקום" בתוך struct במבנה נכון. זה כן אפשרי, אבל לא תמיד בצורה פשוטה, וזה תלוי מצב של קוד C.
  • תרגום של macro לא תמיד כזה פשוט או ברור, וגם לא תמיד נחוץ, וקשה מאוד לדעת מראש את זה.
  • הפניות מעגליות. כאשר מדובר ב C ו C++ אין namespace של קבצים (האחרון עוד יודע איכשהו ליצור namespace של קוד), וביצוע include בנקודה מסויימת פשוט שופך את התכולה של הכל. זה גורם למצב שבו יש בקוד C שורות עם ifndef להגדרה שתהיה מתחתיה שכל הקוד יהיה תחת אותו ifndef.
    העניין הוא שב go אנחנו יוצרים "חבילות" ולפעמים יש סמתוכה של תלויות מעגליות שצריך לחשוב ולתכנן איך להתמודד איתן.
  • אין בשפת Go יכולת להגדיר calling convention.
    לפעמים צריך לכתוב wrapper בC שיתמודד עם זה בשבילי, אם כי לא בכל מצב. לפעמים זה תלוי אפילו בסוג הקומפיילר של C שיהיה בשימוש בזמן ביצוע ה binding ולפעמים צריך לבחור בקומפיילר אחר של Go, דבר שמסבך מערכות בגלל צורך מסוים בספרייה חיצונית.

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

סיכום ביניים

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

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

תכנות נעים 🙂

2 מחשבות על “מה אני פחות אוהב ב golang

  1. baruchiro

    לי חסרים כלים חיצוניים מבוססי AST כמו לינטרים וsecurity.
    אני למשל לא יכול לוודא שאין שימוש בפונקציה מסוימת (למשל fmt.Print במקום logger כלשהו), וגם כשאני כבר מקבל הערות security, הן פשוט לא רלוונטיות כי הן מסתכלות על שורה אחת של קוד ולא על הAST.

    מעניין אותי אם אתה מכיר את המסמך הזה, אגב.
    https://github.com/uber-go/guide/blob/master/style.md

כתיבת תגובה

אתר זו עושה שימוש ב-Akismet כדי לסנן תגובות זבל. פרטים נוספים אודות איך המידע מהתגובה שלך יעובד.