Menu

Struktur Proyek Golang: cmd, internal, dan Tata Letak Package

Cara menyusun proyek Go: mulai dengan struktur datar, pecah menjadi package saat ada alasannya, pakai cmd/ untuk banyak binary dan internal/ untuk kode yang tidak boleh diimpor pihak lain, beri nama package dengan baik, dan simpan test di sebelah kodenya.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Mulai dengan satu file

Program Go bisa sepenuhnya berada di satu main.go, dan tool kecil memang sebaiknya begitu. Program lengkap ini mem-parsing input, mengerjakan tugasnya, dan mencetak laporan, dalam satu package:

Saat program bertambah besar, pecah menjadi beberapa file di direktori yang sama dan package yang sama (parse.go, report.go, main.go). File-file dalam satu package berbagi setiap identifier, jadi tidak ada yang perlu di-export atau diimpor. Sering kali hanya struktur itulah yang dibutuhkan proyek berukuran beberapa ribu baris.

Ingat bahwa package main yang terdiri dari banyak file harus dijalankan sebagai package: go run ., bukan go run main.go. Kalau tidak, file-file itu dikompilasi sendiri-sendiri dan kamu mendapat error undefined untuk nama yang didefinisikan di file lain.

Tidak ada tata letak yang diwajibkan

Go tidak mewajibkan struktur direktori apa pun selain "satu package per direktori". Panduan resminya adalah halaman Organizing a Go module di go.dev, yang menjelaskan beberapa bentuk, bukan aturan.

Repository GitHub golang-standards/project-layout banyak disalin dan sering dikira sebagai standar. Repository itu adalah kumpulan konvensi komunitas dari proyek-proyek besar, dan Russ Cox, yang saat itu menjadi tech lead Go, membuka issue di sana yang menyatakan bahwa itu bukan standar Go. Kebanyakan direktorinya (pkg/, api/, build/, deployments/) hanya masuk akal untuk codebase yang besar. Menyalin seluruh kerangkanya ke proyek baru hanya menghasilkan folder kosong dan import path yang dalam tanpa manfaat apa pun.

Aturan yang benar-benar ditegakkan oleh tool-nya cukup singkat:

  • Satu direktori adalah satu package. Semua file .go di dalamnya (kecuali file _test.go yang memakai akhiran _test) berbagi nama package yang sama.
  • Import path adalah path module ditambah path direktori.
  • Direktori bernama internal membatasi siapa yang boleh mengimpor isinya.
  • Direktori bernama testdata, dan yang diawali . atau _, diabaikan oleh tool go.

Bentuk-bentuk yang umum

Satu command

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

Datar, semuanya di package main. go install github.com/you/todo@latest langsung bisa dipakai dan binary-nya bernama todo.

Sebuah library

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

Package-nya berada di root module, jadi pengguna mengimpor github.com/you/slug dan memanggil slug.Make(...). Apa pun yang tidak ingin kamu dukung sebagai API publik masuk ke internal/.

Sebuah service atau beberapa binary

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 berisi satu direktori per binary, dan nama direktorinya menjadi nama binary (go build ./cmd/shop-api menghasilkan shop-api). Setiap main dibuat tipis: membaca konfigurasi, menyusun dependensi, menjalankan server. Kode yang sebenarnya berada di package-package di bawah internal/, tempat kedua binary bisa memakainya dan module lain tidak bisa.

Inilah tata letak yang akhirnya dipakai kebanyakan service Go. Pakai saat kamu punya binary kedua atau alasan nyata untuk memecah package, bukan sejak hari pertama.

internal/ ditegakkan oleh perintah go

Package di bawah internal/ hanya bisa diimpor oleh kode di pohon direktori yang berakar di induk internal. Dari module lain, build-nya gagal:

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

Karena itu internal/ adalah alat untuk menjaga permukaan API-mu tetap kecil. Kode di sana bebas diubah, karena kamu tahu setiap pemanggilnya ada di repository-mu sendiri. Untuk sebuah aplikasi, menaruh hampir semuanya di internal/ itu wajar. Untuk sebuah library, direktori ini memisahkan apa yang kamu janjikan tetap stabil dari apa yang tidak.

internal bekerja di kedalaman berapa pun: shop/internal/order terlihat oleh seluruh shop/, sedangkan shop/internal/order/internal/pricing hanya terlihat di dalam shop/internal/order/.

Memberi nama package

Nama package menjadi bagian dari setiap tempat pemanggilan, jadi nama itu lebih penting daripada pohon direktorinya.

  • Pendek, huruf kecil, satu kata: order, store, httpapi. Tanpa garis bawah dan tanpa mixedCaps.
  • Namai sesuai apa yang disediakannya, bukan apa yang ada di dalamnya. util, common, helpers, misc, dan models tidak mengatakan apa-apa dan berubah menjadi tempat pembuangan; panduan gaya dari tim Go secara eksplisit tidak menyarankannya. Taruh helper di package yang memakainya, atau di package yang dinamai sesuai tujuannya (slug, retry).
  • Hindari pengulangan nama (stutter). Pemanggil menulis order.Order dan order.New, jadi jangan beri nama seperti order.OrderService atau order.NewOrder. Standard library melakukannya secara konsisten: http.Server, bukan http.HTTPServer.
  • Nama direktori dan nama package sebaiknya sama, kecuali package main di direktori command. Kalau berbeda, pembaca harus membuka file dulu untuk tahu identifier apa yang dihadirkan oleh import itu.

Pecah package berdasarkan tanggung jawab, bukan berdasarkan jenisnya. Pembagian models/, controllers/, services/ (umum di ekosistem lain) memaksa setiap fitur menyentuh tiga package dan cenderung menciptakan import cycle, yang dilarang Go. Package yang disusun di sekitar konsep domain (order, payment, user) masing-masing menyimpan tipe dan logikanya bersama-sama.

Tempat menyimpan test

Test berada di sebelah kodenya di direktori yang sama, dalam file bernama *_test.go. Tidak ada pohon direktori tests/.

  • package order di order_test.go: test internal, yang bisa memakai identifier yang tidak di-export.
  • package order_test di direktori yang sama: test eksternal, yang hanya melihat API yang di-export, seperti yang dilihat pemanggil. Berguna untuk contoh dan untuk menghindari import cycle di test.
  • File fixture disimpan di direktori testdata/ di sebelah test-nya. Tool go mengabaikannya, dan test berjalan dengan direktori package sebagai working directory, jadi path relatif seperti testdata/big.json bisa dipakai.

File lain di root

File atau direktoriKegunaan
go.mod, go.sumdefinisi module dan checksum dependensi, selalu di root
README.md, LICENSEsama seperti di proyek mana pun
Makefile atau Taskfile.ymljalan pintas build yang opsional
Dockerfilebiasanya di root untuk service
migrations/, web/, docs/aset non-Go, dinamai sesuai isinya
tools.gocara lama untuk mengunci dependensi tool; Go 1.24 menggantinya dengan baris tool di go.mod (go get -tool)
go.workworkspace untuk mengembangkan beberapa module bersamaan secara lokal; biasanya tidak di-commit untuk proyek dengan satu module

Kesalahan umum

  • Menyalin template besar untuk proyek kecil. Mulai dengan struktur datar dan tambahkan direktori saat binary kedua atau batas yang nyata muncul.
  • Package bernama utils atau common. Namai package sesuai apa yang dikerjakannya.
  • Satu package per file atau per tipe. Package Go dimaksudkan sebagai unit yang lebih besar daripada class di Java. Package dengan sepuluh file itu normal.
  • Import cycle. Dua package tidak bisa saling mengimpor. Biasanya ini berarti keduanya memang satu kesatuan, bahwa sebuah tipe bersama sebaiknya dipindah ke package yang levelnya lebih rendah, atau bahwa salah satu sisi sebaiknya bergantung pada interface kecil alih-alih pada package lainnya.
  • pkg/ karena kebiasaan. Direktori ini menambah satu segmen ke setiap import path tanpa menambah makna.
  • Test di direktori terpisah. Di sana test tidak bisa melihat kode yang tidak di-export, dan tooling-nya tidak mengharapkannya.

Pertanyaan yang sering diajukan

Apakah ada tata letak proyek Go yang resmi?

Tidak ada yang wajib. Tim Go menerbitkan panduan di go.dev/doc/modules/layout, yang menjelaskan beberapa bentuk umum (satu package, satu command, beberapa command dengan internal/). Repository golang-standards/project-layout yang populer adalah proyek komunitas, bukan standar Go, dan tim Go sudah menyatakannya secara terbuka.

Apa itu direktori internal di Go?

Package yang import path-nya mengandung elemen internal hanya bisa diimpor oleh kode yang berakar di induk direktori internal tersebut. example.com/app/internal/store bisa diimpor dari mana pun di bawah example.com/app/, dan perintah go menolak import dari module lain dengan use of internal package ... not allowed.

Apakah saya perlu direktori pkg di Go?

Opsional, dan hanya menambah satu segmen path tanpa menambah makna. Beberapa proyek besar memakai pkg/ untuk memisahkan kode library publik dari sisanya, tapi standard library Go dan kebanyakan proyek modern tidak melakukannya. Taruh package yang bisa diimpor di root module atau di direktori yang diberi nama, dan apa pun yang privat di internal/.

Di mana file test disimpan di proyek Go?

Di direktori yang sama dengan kode yang diujinya, dengan nama *_test.go. Go tidak punya pohon direktori test yang terpisah. File fixture disimpan di direktori testdata di sebelah test-nya, yang diabaikan oleh tool go saat mencari package.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai