Menu

Struktura projektu w Golang: cmd, internal i układ pakietów

Jak ułożyć projekt w Go: zacznij płasko, dziel na pakiety, gdy jest powód, używaj cmd/ dla wielu binarek i internal/ dla kodu, którego nikt inny nie może importować, dobrze nazywaj pakiety i trzymaj testy obok kodu.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Zacznij od jednego pliku

Program w Go może w całości mieścić się w jednym main.go i małe narzędzia powinny tak wyglądać. Ten kompletny program parsuje wejście, wykonuje swoją pracę i wypisuje raport, w jednym pakiecie:

Gdy urośnie, podziel go na więcej plików w tym samym katalogu i tym samym pakiecie (parse.go, report.go, main.go). Pliki jednego pakietu współdzielą każdy identyfikator, więc nic nie trzeba eksportować ani importować. Często to cała struktura, jakiej potrzebuje projekt na kilka tysięcy linii.

Pamiętaj, że wieloplikowy package main trzeba uruchamiać jako pakiet: go run ., a nie go run main.go. W przeciwnym razie pliki kompilują się osobno i dostajesz błędy undefined dla nazw zdefiniowanych w pozostałych plikach.

Nie ma narzuconego układu

Go nie wymaga żadnej struktury katalogów poza zasadą „jeden pakiet na katalog”. Oficjalne wytyczne to strona Organizing a Go module na go.dev, która opisuje kilka kształtów, a nie reguły.

Repozytorium GitHub golang-standards/project-layout jest powszechnie kopiowane i powszechnie mylone ze standardem. To zbiór konwencji z dużych projektów zebrany przez społeczność, a Russ Cox, wówczas lider techniczny Go, otworzył tam zgłoszenie stwierdzające, że nie jest to standard Go. Większość jego katalogów (pkg/, api/, build/, deployments/) ma sens tylko w dużych bazach kodu. Skopiowanie całego szkieletu do nowego projektu tworzy puste foldery i głębokie ścieżki importu bez żadnej korzyści.

Reguły egzekwowane przez narzędzie są krótkie:

  • Jeden katalog to jeden pakiet. Wszystkie pliki .go w nim (poza plikami _test.go z przyrostkiem _test w nazwie pakietu) mają wspólną nazwę pakietu.
  • Ścieżka importu to ścieżka modułu plus ścieżka katalogu.
  • Katalog o nazwie internal ogranicza, kto może importować to, co jest w środku.
  • Katalogi o nazwie testdata oraz te zaczynające się od . albo _ są pomijane przez narzędzie go.

Typowe kształty

Pojedyncze polecenie

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

Płasko, wszystko w package main. go install github.com/you/todo@latest działa, a binarka nazywa się todo.

Biblioteka

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

Pakiet leży w korzeniu modułu, więc użytkownicy importują github.com/you/slug i wywołują slug.Make(...). Wszystko, czego nie chcesz wspierać jako publicznego API, trafia do internal/.

Serwis albo kilka binarek

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 to jeden katalog na binarkę, a nazwa katalogu staje się nazwą binarki (go build ./cmd/shop-api tworzy shop-api). Każde main pozostaje cienkie: wczytuje konfigurację, tworzy zależności, uruchamia serwer. Właściwy kod żyje w pakietach pod internal/, gdzie mogą z niego korzystać obie binarki i żaden inny moduł.

To układ, do którego zbiega się większość serwisów w Go. Sięgnij po niego, gdy pojawi się druga binarka albo prawdziwy powód do podziału pakietów, a nie pierwszego dnia.

internal/ jest egzekwowany przez polecenie go

Pakiet pod internal/ może być importowany tylko przez kod w drzewie zakorzenionym w katalogu nadrzędnym internal. Z innego modułu budowanie się nie powiedzie:

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

Dzięki temu internal/ jest narzędziem do utrzymywania małej powierzchni API. Kod tam może się swobodnie zmieniać, bo wiesz, że każdy wywołujący jest w twoim repozytorium. W aplikacji umieszczenie prawie wszystkiego w internal/ jest rozsądne. W bibliotece oddziela to, co obiecujesz utrzymywać stabilne, od tego, czego nie obiecujesz.

internal działa na dowolnej głębokości: shop/internal/order jest widoczny dla całego shop/, a shop/internal/order/internal/pricing tylko wewnątrz shop/internal/order/.

Nazywanie pakietów

Nazwa pakietu jest częścią każdego miejsca wywołania, więc ma większe znaczenie niż drzewo katalogów.

  • Krótka, małymi literami, jedno słowo: order, store, httpapi. Bez podkreśleń i bez mixedCaps.
  • Nazywaj od tego, co pakiet dostarcza, a nie co zawiera. util, common, helpers, misc i models nic nie mówią i stają się śmietnikami; wytyczne stylu zespołu Go wprost ich odradzają. Umieść funkcję pomocniczą w pakiecie, który jej używa, albo w pakiecie nazwanym od jej celu (slug, retry).
  • Unikaj powtórzeń. Wywołujący piszą order.Order i order.New, więc nie nazywaj rzeczy order.OrderService czy order.NewOrder. Biblioteka standardowa robi to konsekwentnie: http.Server, a nie http.HTTPServer.
  • Nazwa katalogu i nazwa pakietu powinny się zgadzać, poza package main w katalogach poleceń. Gdy się różnią, czytelnik musi otworzyć plik, żeby dowiedzieć się, jaki identyfikator wprowadza import.

Dziel pakiety według odpowiedzialności, a nie rodzaju. Podział na models/, controllers/, services/ (częsty w innych ekosystemach) zmusza każdą funkcjonalność do dotykania trzech pakietów i zwykle tworzy cykle importów, których Go zabrania. Pakiety zorganizowane wokół pojęcia z domeny (order, payment, user) trzymają razem swoje typy i logikę.

Gdzie umieszczać testy

Testy żyją obok kodu w tym samym katalogu, w plikach nazwanych *_test.go. Nie ma drzewa tests/.

  • package order w order_test.go: test wewnętrzny, który może używać nieeksportowanych identyfikatorów.
  • package order_test w tym samym katalogu: test zewnętrzny, który widzi tylko eksportowane API, tak jak wywołujący. Przydaje się w przykładach i do unikania cykli importów w testach.
  • Pliki z danymi testowymi trafiają do katalogu testdata/ obok testów. Narzędzie go go pomija, a testy działają z katalogiem pakietu jako katalogiem roboczym, więc ścieżki względne, takie jak testdata/big.json, działają.

Inne pliki w korzeniu

Plik lub katalogPrzeznaczenie
go.mod, go.sumdefinicja modułu i sumy kontrolne zależności, zawsze w korzeniu
README.md, LICENSEjak w każdym projekcie
Makefile albo Taskfile.ymlopcjonalne skróty do budowania
Dockerfilew serwisach zwykle w korzeniu
migrations/, web/, docs/zasoby spoza Go, nazwane od zawartości
tools.gostarszy sposób przypinania zależności narzędziowych; Go 1.24 zastępuje go liniami tool w go.mod (go get -tool)
go.workprzestrzeń robocza do lokalnej pracy nad kilkoma modułami naraz; w projektach z jednym modułem zwykle nie trafia do repozytorium

Częste błędy

  • Kopiowanie dużego szablonu do małego projektu. Zacznij płasko i dodawaj katalogi, gdy pojawi się druga binarka albo prawdziwa granica.
  • Pakiety o nazwach utils czy common. Nazywaj pakiety od tego, co robią.
  • Jeden pakiet na plik albo na typ. Pakiety w Go mają być większymi jednostkami niż klasy w Javie. Pakiet z dziesięcioma plikami to norma.
  • Cykle importów. Dwa pakiety nie mogą importować się nawzajem. Zwykle oznacza to, że należą do siebie, że wspólny typ powinien przejść do pakietu niższego poziomu albo że jedna strona powinna zależeć od małego interfejsu zamiast od drugiego pakietu.
  • pkg/ z odruchu. Dodaje segment do każdej ścieżki importu, nie dodając znaczenia.
  • Testy w osobnym katalogu. Nie widzą tam nieeksportowanego kodu, a narzędzia się ich tam nie spodziewają.

Najczęściej zadawane pytania

Czy istnieje oficjalny układ projektu w Go?

Nie ma obowiązkowego. Zespół Go publikuje wytyczne na go.dev/doc/modules/layout, które opisują kilka typowych kształtów (pojedynczy pakiet, polecenie, kilka poleceń z internal/). Popularne repozytorium golang-standards/project-layout to projekt społeczności, a nie standard Go, i zespół Go powiedział to publicznie.

Czym jest katalog internal w Go?

Pakiet, którego ścieżka importu zawiera element internal, może być importowany tylko przez kod zakorzeniony w katalogu nadrzędnym tego internal. example.com/app/internal/store można zaimportować w dowolnym miejscu pod example.com/app/, a polecenie go odrzuca importy z każdego innego modułu komunikatem use of internal package ... not allowed.

Czy w Go używać katalogu pkg?

To opcjonalne i dodaje segment ścieżki, nie dodając znaczenia. Niektóre duże projekty używają pkg/, żeby oddzielić publiczny kod biblioteczny od reszty, ale biblioteka standardowa Go i większość nowoczesnych projektów tego nie robi. Pakiety do importowania umieszczaj w korzeniu modułu albo w nazwanych katalogach, a wszystko prywatne w internal/.

Gdzie w projekcie Go umieszczać pliki testów?

W tym samym katalogu co testowany kod, w plikach nazwanych *_test.go. Go nie ma osobnego drzewa testów. Pliki z danymi testowymi trafiają do katalogu testdata obok testów, który narzędzie go pomija przy wyszukiwaniu pakietów.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ