להתחיל עם קובץ אחד
תוכנית 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.work | workspace לפיתוח של כמה מודולים יחד במחשב המקומי; בדרך כלל לא עושים לו 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 מתעלם ממנה כשהוא מחפש חבילות.