Struktur proyek & paket
Go tidak punya struktur folder resmi selain dua konvensi yang benar-benar berarti: cmd/ untuk titik masuk dan internal/ untuk kode yang dijaga compiler agar tidak bisa diimpor dari luar.
Intisari
internal/bukan sekadar kesepakatan: compiler menolak import dari luar modul. Ini satu-satunya batas modul yang benar-benar ditegakkan.- Satu folder = satu paket. Nama paket mengikuti nama folder, dan sebaiknya satu kata, huruf kecil.
cmd/<nama>/main.gountuk tiap binari yang dihasilkan proyek โ server, worker, alat migrasi.- Impor melingkar adalah error kompilasi. Itu memaksa arah ketergantungan dipikirkan sejak awal.
- Repo
golang-standards/project-layoutbukan standar resmi dan kelewat rumit untuk sebagian besar proyek.
Struktur yang dipakai roadmap ini
toko/
โโโ go.mod
โโโ cmd/
โ โโโ server/main.go # binari HTTP
โ โโโ worker/main.go # binari pemroses antrean
โ โโโ migrate/main.go # alat migrasi database
โโโ internal/ # โ tidak bisa diimpor modul lain. Dipaksa compiler.
โ โโโ http/ # handler, middleware, routing
โ โ โโโ handler_produk.go
โ โ โโโ middleware.go
โ โโโ produk/ # satu domain: model + aturan + penyimpanan
โ โ โโโ produk.go
โ โ โโโ layanan.go
โ โ โโโ postgres.go
โ โโโ pesanan/
โ โโโ auth/
โ โโโ config/
โ โโโ platform/ # pembungkus tipis: db, redis, logger, otel
โโโ migrations/ # berkas .sql, berurutan
โโโ web/ # template, aset statis
โโโ deploy/ # Dockerfile, task definition, workflow
Yang bukan standar: repo populer golang-standards/project-layout sering dikira
resmi karena namanya. Ia bukan โ tidak berasal dari tim Go, dan strukturnya (pkg/,
api/, build/, third_party/โฆ) kelewat berat untuk hampir semua
proyek. Panduan resminya adalah halaman sumber materi ini, dan isinya jauh lebih sederhana:
cmd/, internal/, selesai.
internal/: satu-satunya batas yang ditegakkan
Aturannya dari compiler, bukan dari kesepakatan tim: paket di dalam internal/ hanya bisa
diimpor oleh kode yang berbagi folder induk dengan internal/ itu.
| Paket | Bisa diimpor oleh |
|---|---|
contoh.com/toko/internal/produk | apa pun di dalam contoh.com/toko/ |
contoh.com/toko/internal/produk | bukan contoh.com/lain โ gagal kompilasi |
contoh.com/toko/produk (tanpa internal) | siapa pun di internet, selamanya |
Konsekuensi yang sering tidak disadari: apa pun yang tidak kamu taruh di
internal/ otomatis jadi API publik modulmu. Orang bisa mengimpornya, dan mengubahnya nanti
jadi breaking change. Aturan praktis untuk aplikasi (bukan pustaka): taruh semuanya di
internal/ sampai ada alasan konkret untuk mengeluarkannya.
Paket: satu folder, satu paket
// internal/produk/produk.go
package produk
type Produk struct { ... }
// Dipanggil dari luar sebagai produk.Cari(...)
func Cari(...) { ... }
| Nama paket | Nilai |
|---|---|
produk, auth, billing | โ satu kata, huruf kecil, tanpa garis bawah |
util, common, helper, base | โ tidak memberi tahu apa pun; jadi tempat sampah |
produkService, produk_service | โ camelCase dan garis bawah tidak dipakai untuk paket |
produk.ProdukService | โ nama paket sudah jadi awalan โ cukup produk.Service |
Nama paket ikut terbaca di tempat pemakaian, jadi rancanglah untuk dibaca dari sana:
http.Client, bytes.Buffer, produk.Layanan. Pengulangan seperti
produk.ProdukLayanan adalah gejala paling umum bahwa penamaannya belum dipikir.
Impor melingkar tidak diizinkan
internal/pesanan โ internal/produk โ
internal/produk โ internal/pesanan โ import cycle: gagal kompilasi
Ini terasa mengganggu di minggu pertama dan menyelamatkanmu di tahun kedua. Kalau dua paket saling membutuhkan, biasanya salah satu dari tiga hal ini yang benar:
- Keduanya sebenarnya satu paket โ gabungkan.
- Ada konsep ketiga yang belum punya nama โ keluarkan ke paket sendiri.
- Salah satu arah seharusnya lewat interface yang dideklarasikan di sisi pemakai (Fase 1 dan 5).
Berkas khusus yang dikenali toolchain
| Pola nama | Arti |
|---|---|
*_test.go | Hanya ikut saat go test, tidak pernah masuk binari |
*_linux.go, *_windows.go | Hanya dikompilasi di OS tersebut |
doc.go | Konvensi tempat komentar dokumentasi paket |
//go:embed | Menyisipkan berkas ke dalam binari โ template dan migrasi ikut terbawa |
import "embed"
//go:embed migrations/*.sql
var migrasiFS embed.FS
//go:embed web/template/*
var templateFS embed.FS
//go:embed adalah pasangan alami dari binari statis. Template, berkas migrasi, dan
aset kecil ikut masuk ke dalam satu berkas hasil build. Di Fase 10 hal ini berarti image
scratch-mu benar-benar cukup berisi satu berkas โ tidak ada folder yang harus disalin dan
tidak ada berkas yang bisa hilang di antara build dan deploy.
Kapan memecah paket
| Sinyal | Tindakan |
|---|---|
| Satu berkas > ~800 baris | Pecah berkas, belum tentu pecah paket |
| Paket punya dua kelompok tipe yang tidak saling menyentuh | Pecah jadi dua paket |
Terasa perlu util baru | Berhenti. Taruh fungsinya di paket yang memakainya |
| Satu tipe dipakai tiga paket berbeda | Naikkan ke paket domain sendiri |
Go lebih memilih paket sedikit yang berisi daripada banyak paket berisi satu berkas.
Berbeda dari Java atau C#, jumlah berkas per paket tidak dibatasi apa pun โ net/http di
pustaka standar berisi puluhan berkas dalam satu paket.
Latihan: ubah proyek halo-mu jadi cmd/server/main.go plus
internal/halo/halo.go, lalu panggil fungsi dari paket halo. Setelah jalan, buat
modul kedua di folder lain dan coba impor contoh.com/halo/internal/halo dari sana โ baca
pesan errornya. Itu satu-satunya batas arsitektur di Go yang tidak bisa dilanggar siapa pun.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.