Menu

Packages ו-imports ב-Golang: שמות מיוצאים וכמה קבצים

איך חבילות עובדות ב-Go: פסוקית ה-package, ייבוא חבילות מהספרייה הסטנדרטית ומהמודול, שמות מיוצאים מול לא מיוצאים, פיצול חבילה לכמה קבצים, חבילות internal, כינויים ל-import, blank imports והשגיאה "undefined" מ-go run main.go.

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

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

fmt, math ו-strings הן חבילות של הספרייה הסטנדרטית. הן מגיעות עם Go, כך ש"הוספה" של אחת מהן פירושה לכתוב את השם שלה בבלוק ה-import. שום דבר לא מורד. fmt.Println, math.Sqrt ו-strings.Repeat מתחילות באות גדולה כי הן מיוצאות.

פסוקית ה-package

השורה הראשונה שהיא לא הערה בכל קובץ .go היא package name. כל הקבצים בתיקייה אחת חייבים להשתמש באותו שם, ויחד הם יוצרים חבילה אחת. תיקייה עם שני שמות חבילה שונים לא מתקמפלת:

found packages greeting (b.go) and greet (greet.go) in /home/ana/shop/greet

(החריג היחיד הוא קובצי בדיקה, שמותר להם להשתמש ב-package name_test.)

package main מיוחדת: היא מייצרת קובץ הרצה, והתוכנית מתחילה ב-func main() שלה. כל חבילה אחרת היא ספרייה שנועדה לייבוא.

לפי המוסכמה, שם החבילה תואם לרכיב האחרון של נתיב התיקייה: קוד ב-shop/greet הוא package greet. שמות חבילות קצרים, באותיות קטנות, מילה אחת, בלי קווים תחתונים ובלי אותיות גדולות באמצע: strconv, httptest, greet, ולא string_utils או greetHelpers.

ייבוא חבילות

נתיב import הוא מחרוזת. בספרייה הסטנדרטית זה הנתיב של החבילה בעץ הקוד של Go; בכל השאר הוא מתחיל בנתיב של מודול:

import (
	"fmt"           // standard library
	"net/http"      // standard library, nested path
	"math/rand/v2"  // standard library, version 2 of math/rand

	"github.com/google/uuid"   // another module (needs go get)
	"example.com/shop/greet"   // a package in your own module
)

הנתיב הוא המקום שבו החבילה נמצאת; השם הוא מה שכותבים בקוד. ל-net/http השם הוא http, ל-math/rand/v2 הוא rand. כותבים http.Get, אף פעם לא net/http.Get.

gofmt ו-goimports ממיינים את הבלוק, ולפי המוסכמה מפרידים בשורה ריקה בין imports של הספרייה הסטנדרטית לשאר.

imports שלא בשימוש הם שגיאת קומפילציה:

./main.go:5:2: "os" imported and not used

עורכים עם תמיכה ב-Go מריצים goimports בכל שמירה, והוא מוסיף בשבילכם imports סטנדרטיים חסרים ומסיר כאלה שלא בשימוש.

שמות מיוצאים מול לא מיוצאים

הנראות ב-Go נקבעת לפי כלל אחד: אם שם מתחיל באות גדולה, הוא מיוצא. זה חל על פונקציות, טיפוסים, משתנים, קבועים, שדות של struct ומתודות. אין מילות מפתח public, private או protected.

שםנראה מחוץ לחבילה?
func Hello()כן
func hello()לא
type User structכן
השדה User.Nameכן
השדה User.emailלא
const MaxSizeכן
var defaultTimeoutלא

ניסיון להשתמש בשם לא מיוצא מחבילה אחרת נותן שגיאת undefined:

./main.go:11:20: undefined: greet.prefix

הכלל חשוב גם בתוך קובץ יחיד, כי חבילות אחרות בודקות את הטיפוסים שלכם. encoding/json יכולה לראות רק שדות מיוצאים:

token נשאר מחוץ ל-JSON, בשקט. זו תכונה כשהשדה הוא מידע פרטי, והפתעה כששכחתם לכתוב באות גדולה שדה שרציתם.

פיצול חבילה לכמה קבצים

חבילה יכולה להתפרס על כמה קבצים שתרצו. כל קובץ בתיקייה עם אותה שורת package רואה כל שם שהוצהר בקבצים האחרים, מיוצא או לא, בלי לייבא כלום:

shop/
├── go.mod        (module example.com/shop)
├── main.go
└── math.go
// main.go
package main

import "fmt"

func main() {
	fmt.Println(total(2, 3))
}
// math.go
package main

func total(a, b int) int { return a + b }

main.go קורא ל-total ישירות. אין #include ואין import בין קבצים של אותה חבילה.

השגיאה "undefined" מ-go run main.go

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

go run main.go
# command-line-arguments
./main.go:6:14: undefined: total

command-line-arguments הוא השם ש-Go נותנת לחבילה שנבנית מרשימת קבצים, ו-math.go לא נמצא ברשימה הזאת. הריצו במקום זה את כל החבילה:

go run .
5

אותו דבר חל על go build main.go. השתמשו ב-go run . וב-go build (או go build ./cmd/app) והבעיה אף פעם לא תופיע. אם היא נמשכת גם עם go run ., בדקו שבשני הקבצים כתוב package main ושלאף אחד מהם אין build constraint (//go:build) או סיומת _test.go שמוציאה אותו.

חבילות משלכם

תת-תיקייה של המודול שלכם היא חבילה נפרדת. ייבאו אותה עם נתיב המודול מ-go.mod ועוד התיקייה:

shop/
├── go.mod          (module example.com/shop)
├── main.go
└── greet/
    └── greet.go
// greet/greet.go
package greet

const prefix = "Hi, "

// Hello returns a greeting for name.
func Hello(name string) string {
	return prefix + name
}
// main.go
package main

import (
	"fmt"

	"example.com/shop/greet"
)

func main() {
	fmt.Println(greet.Hello("Ana"))
}
Hi, Ana

שני דברים מכשילים אנשים כאן. ה-import הוא נתיב המודול ועוד התיקייה, לא נתיב יחסי: "./greet" לא עובד במצב מודולים. ו-prefix נשאר מוסתר מ-main כי הוא באות קטנה; רק Hello הוא חלק מה-API של החבילה.

חבילות internal

תיקייה בשם internal מגבילה מי יכול לייבא את מה שבתוכה. קוד ב-shop/internal/store יכול להיות מיובא על ידי כל חבילה שהשורש שלה ב-shop/, ועל ידי שום דבר מחוץ לו. מודול אחר שמנסה מקבל:

use of internal package example.com/shop/internal/store not allowed

השתמשו ב-internal/ לקוד שאתם רוצים לשתף בין החבילות שלכם בלי להבטיח אותו לשאר העולם. אפשר לשנות את ה-API שלו בחופשיות, כי שום קוד חיצוני לא יכול להיות תלוי בו.

כינויים ל-import

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

שתי החבילות נקראות rand, ולכן לפחות אחת צריכה כינוי. השתמשו בכינויים להתנגשויות ולשמות לא ברורים; אל תשנו שמות של חבילות רק כדי לקצר אותם, כי הקוראים מכירים את strconv ולא את ה-sc שלכם.

Blank imports ו-dot imports

Blank import (_). מייבא חבילה רק בשביל תופעות הלוואי שלה: משתני החבילה ופונקציות ה-init שלה רצים, אבל אתם לא משתמשים באף אחד מהשמות שלה. drivers של מסדי נתונים ו-decoders של תמונות רושמים את עצמם בדרך הזאת:

import (
	"database/sql"

	_ "github.com/lib/pq" // registers the "postgres" driver with database/sql
)

בלי ה-_, ה-import היה שגיאה של import שלא בשימוש.

Dot import (.). מכניס את השמות המיוצאים של החבילה ישירות ל-scope של הקובץ שלכם, כך שכותבים Println במקום fmt.Println. זה מסתיר מאיפה שמות מגיעים, ולא מומלץ מחוץ לכמה תבניות של פונקציות עזר לבדיקות.

מעגלי import

Go לא מאפשרת לשתי חבילות לייבא זו את זו, ישירות או דרך שרשרת:

package example.com/shop
	imports example.com/shop/greet from main.go
	imports example.com/shop/other from c.go
	imports example.com/shop/greet from o.go: import cycle not allowed

השגיאה מפרטת את השרשרת. שני תיקונים מכסים כמעט כל מקרה:

  1. העבירו את הקוד ששתי החבילות צריכות לחבילה שלישית, ברמה נמוכה יותר, ששתיהן מייבאות.
  2. אם חבילה A רק צריכה לקרוא למשהו ב-B, הגדירו interface קטן ב-A ותנו לטיפוס של B לממש אותו. A כבר לא מייבאת את B.

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

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

  • הרצה של קובץ בודד. go run main.go בחבילה עם כמה קבצים: השתמשו ב-go run ..
  • imports יחסיים. import "./utils" נכשל במצב מודולים. השתמשו ב-"example.com/yourmodule/utils".
  • שמות באות קטנה שהתכוונתם לייצא. פונקציה, שדה או מתודה שחבילות אחרות צריכות חייבים להתחיל באות גדולה.
  • שם חבילה שלא תואם לתיקייה. חוקי, אבל מבלבל: נתיב ה-import אומר utils והקוד אומר helpers.X. שמרו אותם זהים.
  • שמות חבילה כלליים מדי. util, common ו-misc לא אומרים כלום על התוכן שלהם ונוטים להפוך למגנטים של מעגלי import. תנו לחבילות שמות לפי מה שהן מספקות: money, auth, slug.

שאלות נפוצות

למה Go אומרת שפונקציה באותה חבילה היא undefined?

כנראה הרצתם go run main.go. ציון של קובץ מקמפל רק את הקובץ הזה, ולכן פונקציות שמוגדרות בקבצים אחרים של אותה חבילה חסרות: undefined: total. הריצו במקום זה go run ., שמקמפל כל קובץ .go בתיקייה. בדקו גם שבכל הקבצים יש אותה שורת package.

איך מוסיפים את החבילה fmt ב-Go?

fmt היא חלק מהספרייה הסטנדרטית ומגיעה עם Go, כך שאין מה להתקין. הוסיפו import "fmt" אחרי שורת ה-package וקראו לפונקציות שלה עם הקידומת fmt., למשל fmt.Println("hi"). אותו דבר חל על strings, os, time, net/http וכל חבילה סטנדרטית אחרת.

איך מייבאים חבילה משלי ב-Go?

מייבאים אותה לפי נתיב המודול ועוד התיקייה. במודול בשם example.com/shop, חבילה בתיקייה greet מיובאת כ-import "example.com/shop/greet" ומשמשת כ-greet.Hello(). רק שמות שמתחילים באות גדולה נראים לקוד שמייבא.

מה ההבדל בין exported ל-unexported ב-Go?

שם שמתחיל באות גדולה (Hello, Total, User.Name) הוא מיוצא ואפשר להשתמש בו מחבילות אחרות. שם באות קטנה (hello, total, user.name) לא מיוצא ונראה רק בתוך החבילה שלו. אין מילות מפתח public או private.

איך מתקנים את "import cycle not allowed" ב-Go?

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

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

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

להתחיל