Menu

פונקציות ב-Golang: הצהרה, קריאה והעברת פונקציות

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

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

הצהרה על פונקציה

הצהרת פונקציה היא func, שם, הפרמטרים, טיפוס התוצאה וגוף. הטיפוס בא אחרי שם הפרמטר, לא לפניו.

add מקבלת שני ערכי int ומחזירה int. greet לא מחזירה כלום, ולכן בחתימה שלה אין טיפוס תוצאה והגוף שלה לא צריך return.

פונקציה שמצהירה על תוצאה חייבת להסתיים בהוראה מסיימת. אם מסלול כלשהו יכול להגיע לסוגר המסולסל הסוגר בלי לחזור, הקומפיילר עוצר עם missing return.

פרמטרים שחולקים טיפוס

פרמטרים עוקבים מאותו טיפוס יכולים לחלוק שם טיפוס אחד. func add(a, b int) int זהה לחלוטין ל-func add(a int, b int) int. גם שילוב מותר:

func scale(x, y float64, factor int) (float64, float64)

כאן x ו-y הם float64 ו-factor הוא int.

ערכים מוחזרים

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

הקוד מדפיס 3 2. הזוג (value, error) הוא השימוש הנפוץ ביותר בתכונה הזאת ב-Go, ויש לו דף משלו על ערכים מוחזרים מרובים.

ארגומנטים מועתקים

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

הפלט הוא:

{1 2}
{100 2}
[99 2 3]

השורה השלישית מפתיעה אנשים. ערך slice הוא כותרת קטנה (מצביע, אורך, קיבולת). העותק מצביע על אותו מערך בסיס, ולכן כתיבה ל-s[0] כותבת לאיבר של מי שקרא. אבל append בתוך הפונקציה עשויה להקצות מערך חדש, ומי שקרא אף פעם לא רואה את הכותרת החדשה. אם פונקציה מגדילה slice, החזירו את ה-slice החדש. אותו היגיון חל על maps: הפונקציה יכולה להוסיף מפתחות שמי שקרא רואה, כי ערך ה-map מפנה לנתונים משותפים.

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

פונקציות הן ערכים

לפונקציה יש טיפוס, שנכתב בלי השם: func(int, int) int. אפשר לשמור פונקציה במשתנה, לשים אותה ב-map או ב-slice, ולהעביר אותה לפונקציה אחרת.

square היא פונקציה אנונימית שהושמה למשתנה. פונקציות אנונימיות יכולות גם לקרוא ולשנות משתנים מההיקף שסביבן, וזה הופך אותן ל-closures.

טיפוס פונקציה בעל שם הופך חתימות לקריאות יותר כשאותה צורה מופיעה בהרבה מקומות:

type Transform func(int) int

func apply(xs []int, f Transform) []int

ערך האפס של טיפוס פונקציה הוא nil. קריאה לפונקציה שהיא nil גורמת ל-panic עם invalid memory address or nil pointer dereference, ולכן בדקו callbacks אופציונליים לפני שקוראים להם. אפשר להשוות ערכי פונקציה רק ל-nil, אף פעם לא זה לזה.

רקורסיה

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

for i := range 6 סופר מ-0 עד 5 ודורש Go 1.22 ומעלה.

מה שאין לפונקציות ב-Go

  • אין overloading. שתי פונקציות באותה חבילה לא יכולות לחלוק שם, גם עם פרמטרים שונים.
  • אין ארגומנטים עם ערך ברירת מחדל או ארגומנטים בעלי שם. כל פרמטר מועבר בכל קריאה, לפי הסדר. struct של קונפיגורציה נותן לכם שדות בעלי שם ואופציונליים: NewServer(Config{Port: 8080}).
  • אין פונקציות בעלות שם מקוננות. בתוך גוף פונקציה אפשר להצהיר רק על פונקציות אנונימיות, שבדרך כלל מושמות למשתנה.

רשימות ארגומנטים באורך משתנה נתמכות באמצעות פונקציות variadic.

פונקציות מיוצאות ולא מיוצאות

פונקציה ששמה מתחיל באות גדולה (Parse) היא מיוצאת, ואפשר לקרוא לה מחבילות אחרות. שם באות קטנה (parse) גלוי רק בתוך החבילה שלו. אין מילות מפתח public או private; האות הגדולה היא הכלל כולו.

טעויות נפוצות

  • לצפות ששינוי בפרמטר יגיע למי שקרא. השמה לפרמטר משנה רק את העותק. החזירו את הערך החדש או קבלו מצביע.
  • הוספה עם append בתוך פונקציה והתעלמות מהתוצאה. func add(s []int) { s = append(s, 1) } לא משפיעה על האורך של ה-slice של מי שקרא. כתבו func add(s []int) []int והשתמשו ב-s = add(s).
  • תוצאות שלא נעשה בהן שימוש. Go מאפשרת להתעלם מערך מוחזר. go vet לא מסמן שגיאה שנזרקה, אבל linters כמו errcheck (חלק מ-golangci-lint) כן. השתמשו ב-_ = רק כשההתעלמות היא החלטה.

שאלות נפוצות

איך מגדירים פונקציה ב-Go?

השתמשו במילת המפתח func, בשם, ברשימת פרמטרים שבה הטיפוסים באים אחרי השמות, ובטיפוס התוצאה: func add(a, b int) int { return a + b }. פונקציה בלי תוצאה משמיטה את הטיפוס, ופונקציה עם כמה תוצאות מפרטת אותן בסוגריים: func divmod(a, b int) (int, int).

האם Go מעבירה ארגומנטים לפי ערך או לפי הפניה?

תמיד לפי ערך. הפונקציה מקבלת עותק של כל ארגומנט. עבור int או struct, העותק הזה בלתי תלוי במשתנה של מי שקרא. slices, maps, channels ומצביעים הם ערכים קטנים שמפנים לנתונים משותפים, ולכן שינויים באיברים שהם מצביעים עליהם נראים למי שקרא, בעוד שהשמה מחדש לפרמטר עצמו לא נראית. העבירו מצביע (*T) כשהפונקציה צריכה לשנות את המשתנה של מי שקרא.

האם אפשר להעביר פונקציה כפרמטר ב-Go?

כן. פונקציות הן ערכים עם טיפוס כמו func(int) int. הצהירו על פרמטר מהטיפוס הזה והעבירו כל פונקציה עם חתימה תואמת, עם שם או אנונימית: func apply(xs []int, f func(int) int).

האם Go תומכת ב-overloading של פונקציות או בפרמטרים עם ערך ברירת מחדל?

לא. כל שם פונקציה בחבילה חייב להיות ייחודי, וכל פרמטר חייב להיות מועבר בכל קריאה. התחליפים המקובלים הם שמות שונים (NewServer, NewServerWithTLS), struct של קונפיגורציה, פרמטרים variadic או functional options.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל