Menu

מבנה פרויקט ב-Golang: cmd, internal וחלוקה לחבילות

איך בונים את המבנה של פרויקט Go: מתחילים שטוח, מפצלים לחבילות כשיש סיבה, משתמשים ב-cmd/ לכמה קובצי הרצה וב-internal/ לקוד שאף אחד אחר לא אמור לייבא, נותנים לחבילות שמות טובים ומשאירים את הבדיקות ליד הקוד.

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

להתחיל עם קובץ אחד

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

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

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

אין מבנה מחייב

Go לא דורשת שום מבנה תיקיות מעבר ל"חבילה אחת לכל תיקייה". ההנחיה הרשמית היא העמוד Organizing a Go module ב-go.dev, והוא מתאר כמה צורות, לא כללים.

המאגר golang-standards/project-layout ב-GitHub מועתק הרבה, והרבה פעמים נחשב בטעות לתקן. זה אוסף קהילתי של מוסכמות מפרויקטים גדולים, ו-Russ Cox, שהיה אז ה-tech lead של Go, פתח שם issue שקובע שזה לא תקן של Go. רוב התיקיות שלו (pkg/, api/, build/, deployments/) הגיוניות רק לבסיסי קוד גדולים. העתקה של כל השלד לפרויקט חדש יוצרת תיקיות ריקות ונתיבי import עמוקים בלי שום תועלת.

הכללים שהכלי אוכף קצרים:

  • תיקייה אחת היא חבילה אחת. כל קובצי ה-.go שבה (חוץ מקובצי _test.go שמשתמשים בסיומת _test) חולקים שם חבילה.
  • נתיב ה-import הוא נתיב המודול ועוד נתיב התיקייה.
  • תיקייה בשם internal מגבילה מי יכול לייבא את מה שבתוכה.
  • כלי ה-go מתעלם מתיקיות בשם testdata ומתיקיות שמתחילות ב-. או ב-_.

הצורות הנפוצות

פקודה אחת

todo/
├── go.mod        module github.com/you/todo
├── main.go
├── parse.go
├── report.go
└── parse_test.go

שטוח, הכול ב-package main. go install github.com/you/todo@latest עובד, והקובץ הבינארי נקרא todo.

ספרייה

slug/
├── go.mod        module github.com/you/slug
├── slug.go       package slug
├── slug_test.go
├── example_test.go
└── internal/
    └── table/    helpers the public package uses, hidden from users

החבילה נמצאת בשורש המודול, כך שמשתמשים מייבאים את github.com/you/slug וקוראים ל-slug.Make(...). כל מה שאתם לא רוצים לתמוך בו כ-API ציבורי נכנס תחת internal/.

service או כמה קובצי הרצה

shop/
├── go.mod                module github.com/you/shop
├── cmd/
│   ├── shop-api/
│   │   └── main.go       package main: flags, config, wiring
│   └── shop-worker/
│       └── main.go
├── internal/
│   ├── order/            package order: domain types and logic
│   │   ├── order.go
│   │   └── order_test.go
│   ├── store/            package store: database access
│   └── httpapi/          package httpapi: handlers, routing
├── migrations/
└── README.md

cmd/<name>/main.go מחזיק תיקייה אחת לכל קובץ הרצה, ושם התיקייה הופך לשם הקובץ הבינארי (go build ./cmd/shop-api מייצר shop-api). כל main נשאר רזה: קורא הגדרות, בונה תלויות, מפעיל את השרת. הקוד האמיתי חי בחבילות תחת internal/, ששני קובצי ההרצה יכולים להשתמש בהן ושום מודול אחר לא יכול.

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

הפקודה go אוכפת את internal/

חבילה תחת internal/ יכולה להיות מיובאת רק על ידי קוד בעץ שהשורש שלו בתיקיית האב של internal. ממודול אחר, ה-build נכשל:

package example.com/b
	main.go:6:2: use of internal package example.com/a/internal/secret not allowed

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

internal עובד בכל עומק: shop/internal/order נראה לכל shop/, בעוד ש-shop/internal/order/internal/pricing נראה רק בתוך shop/internal/order/.

לתת שמות לחבילות

שם החבילה הוא חלק מכל מקום שקורא לה, ולכן הוא חשוב יותר מעץ התיקיות.

  • קצר, באותיות קטנות, מילה אחת: order, store, httpapi. בלי קווים תחתונים ובלי mixedCaps.
  • תנו שם לפי מה שהחבילה מספקת, לא לפי מה שהיא מכילה. util, common, helpers, misc ו-models לא אומרים כלום והופכים למזבלות; הנחיות הסגנון של צוות Go ממליצות במפורש להימנע מהם. שימו פונקציית עזר בחבילה שמשתמשת בה, או בחבילה שנקראת לפי המטרה שלה (slug, retry).
  • הימנעו מגמגום. קוד קורא כותב order.Order ו-order.New, אז אל תקראו לדברים order.OrderService או order.NewOrder. הספרייה הסטנדרטית עושה את זה בעקביות: http.Server, לא http.HTTPServer.
  • שם התיקייה ושם החבילה צריכים להתאים, חוץ מ-package main בתיקיות של פקודות. כשהם שונים, הקוראים צריכים לפתוח קובץ כדי לגלות איזה מזהה ה-import מכניס.

פצלו חבילות לפי אחריות, לא לפי סוג. חלוקה ל-models/, controllers/, services/ (נפוצה באקוסיסטמים אחרים) מכריחה כל פיצ'ר לגעת בשלוש חבילות ונוטה ליצור מעגלי import, ש-Go אוסרת. חבילות שמאורגנות סביב מושג מהתחום (order, payment, user) מחזיקות כל אחת את הטיפוסים והלוגיקה שלה יחד.

איפה שמים בדיקות

בדיקות חיות ליד הקוד, באותה תיקייה, בקבצים בשם *_test.go. אין עץ tests/.

  • package order ב-order_test.go: בדיקה פנימית, שיכולה להשתמש במזהים לא מיוצאים.
  • package order_test באותה תיקייה: בדיקה חיצונית, שרואה רק את ה-API המיוצא, כמו שקוד קורא היה רואה. שימושי לדוגמאות ולהימנעות ממעגלי import בבדיקות.
  • קובצי fixtures נכנסים לתיקייה testdata/ ליד הבדיקות. כלי ה-go מתעלם ממנה, ובדיקות רצות כשתיקיית החבילה היא תיקיית העבודה, כך שנתיבים יחסיים כמו testdata/big.json עובדים.

קבצים אחרים בשורש

קובץ או תיקייהמטרה
go.mod, go.sumהגדרת המודול ו-checksums של התלויות, תמיד בשורש
README.md, LICENSEכמו בכל פרויקט
Makefile או Taskfile.ymlקיצורי build אופציונליים
Dockerfileב-services בדרך כלל בשורש
migrations/, web/, docs/קבצים שהם לא Go, עם שם לפי מה שהם מכילים
tools.goדרך ישנה לנעול תלויות של כלים; Go 1.24 מחליפה אותה בשורות tool ב-go.mod (go get -tool)
go.workworkspace לפיתוח של כמה מודולים יחד במחשב המקומי; בדרך כלל לא עושים לו commit בפרויקטים עם מודול אחד

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

  • העתקת תבנית גדולה לפרויקט קטן. התחילו שטוח והוסיפו תיקיות כשמופיע קובץ הרצה שני או גבול אמיתי.
  • חבילות בשם utils או common. תנו לחבילות שמות לפי מה שהן עושות.
  • חבילה אחת לכל קובץ או לכל טיפוס. חבילות Go אמורות להיות יחידות גדולות יותר ממחלקות ב-Java. חבילה עם עשרה קבצים היא דבר רגיל.
  • מעגלי import. שתי חבילות לא יכולות לייבא זו את זו. זה בדרך כלל אומר שהן שייכות יחד, שטיפוס משותף צריך לעבור לחבילה ברמה נמוכה יותר, או שצד אחד צריך להיות תלוי ב-interface קטן במקום בחבילה השנייה.
  • pkg/ מתוך הרגל. זה מוסיף רכיב לכל נתיב import בלי להוסיף משמעות.
  • בדיקות בתיקייה נפרדת. הן לא יכולות לראות משם קוד לא מיוצא, והכלים לא מצפים להן שם.

שאלות נפוצות

יש מבנה פרויקט רשמי ל-Go?

לא כזה שהוא חובה. צוות Go מפרסם הנחיות ב-go.dev/doc/modules/layout, שמתארות כמה צורות נפוצות (חבילה אחת, פקודה אחת, כמה פקודות עם internal/). המאגר הפופולרי golang-standards/project-layout הוא פרויקט קהילתי, לא תקן של Go, וצוות Go אמר את זה בפומבי.

מה זו התיקייה internal ב-Go?

חבילה שנתיב ה-import שלה מכיל רכיב internal יכולה להיות מיובאת רק על ידי קוד שהשורש שלו בתיקיית האב של אותה תיקיית internal. את example.com/app/internal/store אפשר לייבא מכל מקום תחת example.com/app/, והפקודה go דוחה imports מכל מודול אחר עם use of internal package ... not allowed.

כדאי להשתמש בתיקייה pkg ב-Go?

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

איפה שמים קובצי בדיקה בפרויקט Go?

באותה תיקייה כמו הקוד שהם בודקים, עם שם בצורה *_test.go. ב-Go אין עץ נפרד לבדיקות. קובצי fixtures נכנסים לתיקייה testdata ליד הבדיקות, שכלי ה-go מתעלם ממנה כשהוא מחפש חבילות.

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

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

להתחיל