Tata Letak Proyek

Konvensi cmd/, internal/, dan api/.

Daftar isi

Tata letak proyek Go sebagian besar merupakan konvensi, bukan aturan sintaks, tetapi satu bagiannya ditegakkan langsung oleh perintah go. Topik ini membahas konvensi yang dirangkum dalam dokumentasi resmi modul Go, yaitu peran direktori cmd dan internal, ditambah direktori api sebagai konvensi komunitas untuk definisi API publik. Contoh pada topik ini menyusun proyek kecil multi-paket dari nol sampai binernya berhasil dibangun dan dijalankan. Setelah membacanya, struktur direktori sebuah proyek Go dapat dirancang dengan alasan yang jelas di balik setiap folder.

Pohon direktori proyek contoh#

Proyek contoh pada topik ini adalah program baris perintah penghitung kata yang tersusun dari tiga folder di bawah berkas go.mod. Pohon direktorinya digambarkan sebagaimana berikut.

text
penghitung-kata/
├── go.mod
├── api/
│   └── penghitung-kata.yaml
├── cmd/
│   └── penghitung-kata/
│       └── main.go
└── internal/
    └── teks/
        └── teks.go

Setiap entri pohon tersebut memiliki peran yang berbeda. Berkas go.mod berada di akar karena setiap proyek Go dimulai dari satu modul yang dideklarasikan di sana. Folder cmd menyimpan paket main untuk setiap biner yang dihasilkan proyek. Folder internal menyimpan paket pustaka yang sengaja dibuat privat, sedangkan folder api menyimpan definisi API publik dalam berkas YAML yang tidak pernah dikompilasi oleh perangkat go. Struktur ini sengaja diambil dalam ukuran kecil agar setiap folder dapat dijelaskan, dan prinsip yang sama tetap berlaku ketika proyeknya tumbuh menjadi puluhan paket.

Direktori cmd untuk biner#

Direktori cmd adalah tempat paket main dikumpulkan, dengan satu subfolder untuk setiap program yang dihasilkan repositori. Dokumentasi resmi menuliskan bahwa menempatkan seluruh perintah ke dalam cmd bukanlah keharusan untuk repositori yang hanya berisi program, tetapi sangat berguna pada repositori campuran yang memuat program dan paket yang dapat diimpor sekaligus. Pengguna yang ingin memasang salah satu programnya cukup menjalankan go install dengan jalur menuju subfolder tersebut, misalnya go install contoh/penghitung-kata/cmd/penghitung-kata@latest untuk proyek yang telah diterbitkan.

Berkas main.go di dalam folder tersebut sengaja dibuat tipis. Isinya hanya membaca argumen baris perintah, memanggil paket pustaka, dan mencetak hasilnya:

go cmd/penghitung-kata/main.go
// tidak-dijalankan: bagian dari proyek multi-paket

// Command penghitung-kata menghitung banyaknya kata pada argumen baris perintah.
package main

import (
	"fmt"
	"os"

	"contoh/penghitung-kata/internal/teks"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Fprintln(os.Stderr, "gunakan: penghitung-kata <teks>")
		os.Exit(1)
	}
	fmt.Printf("%d kata\n", teks.HitungKata(os.Args[1]))
}
```go {file="internal/teks/teks.go"}
// tidak-dijalankan: bagian dari proyek multi-paket

// Package teks menyediakan pengolahan teks dasar untuk proyek penghitung kata.
package teks

import "strings"

// HitungKata mengembalikan banyaknya kata yang dipisahkan spasi putih.
func HitungKata(teks string) int {
	return len(strings.Fields(teks))
}
```yaml {file="api/penghitung-kata.yaml"}
# Definisi API publik yang ringkas untuk layanan penghitung kata.
layanan: penghitung-kata
versi: 1
endpoint:
  - nama: hitung
    metode: POST
    jalur: /hitung
    masukan:
      teks: string
    keluaran:
      jumlah_kata: integer

Membangun proyeknya sampai biner#

Ketiga berkas proyek sudah lengkap, sehingga tinggal mengurutkan langkah pembangunannya. Folder kerja disiapkan di /tmp, lalu go.mod dibuat di sana dengan go mod init. Selanjutnya seluruh folder proyek disalin ke sana dan paket biner dibangun dengan go build. Perintah-perintah berikut dijalankan dari folder topik ini sehingga penyalinannya menunjuk berkas proyek di dalamnya:

bash
# siapkan folder kerja dan salin pohon proyek dari folder topik ini
rm -rf /tmp/tata-letak
mkdir -p /tmp/tata-letak

# inisialisasi modul di dalam folder kerja tanpa pindah folder
(cd /tmp/tata-letak && go mod init contoh/penghitung-kata)

# salin folder proyek, kecuali go.mod yang baru dibuat
cp -r cmd internal api /tmp/tata-letak/
cd /tmp/tata-letak

# bangun paket biner lalu jalankan hasilnya
go build ./cmd/penghitung-kata
./penghitung-kata "go mengompilasi cepat dan menghasilkan satu biner"
text Keluaran
go: creating new go.mod: module contoh/penghitung-kata
7 kata

Dua hal layak diperhatikan dari urutan di atas. go mod init dijalankan sebelum berkas program disalin agar keluarannya bersih. go build diberi jalur paket ./cmd/penghitung-kata, bukan ./…, karena membangun beberapa paket sekaligus hanya memeriksa kemampuan kompilasinya tanpa meninggalkan biner. Argumen pada pemanggilan biner menunjukkan paket internal ikut terkompilasi secara otomatis, karena main.go mengimpornya dan go build menyelesaikan seluruh graf dependensinya.

Batas internal ditegakkan saat build#

Kekuatan aturan internal baru terlihat ketika dilanggar. Proyek kedua dibuat pada modul terpisah, lalu mencoba mengimpor internal/teks milik modul pertama. Kedua modul dihubungkan secara lokal melalui go mod edit, satu kali untuk mencatat dependensinya dan sekali lagi untuk mengganti sumbernya dengan folder /tmp/tata-letak agar tidak ada unduhan yang terjadi:

bash
# buat proyek lain pada modul yang terpisah
rm -rf /tmp/proyek-lain
mkdir -p /tmp/proyek-lain
cd /tmp/proyek-lain
go mod init contoh/lain

# berkas ini mencoba mengimpor paket internal milik modul lain
cat > lain.go <<'EOF'
// tidak-dijalankan: gagal build dengan sengaja
package main

import (
	"fmt"

	"contoh/penghitung-kata/internal/teks"
)

func main() {
	fmt.Println(teks.HitungKata("satu dua tiga"))
}
EOF

# catat dependensi lokal lalu bangun
go mod edit -require=contoh/[email protected] -replace=contoh/penghitung-kata=/tmp/tata-letak
go build ./...
text Keluaran
go: creating new go.mod: module contoh/lain
package contoh/lain
	lain.go:7:2: use of internal package contoh/penghitung-kata/internal/teks not allowed

Pesan error muncul pada saat build, bukan pada saat program berjalan, dan menyebut secara eksplisit bahwa paket internal tidak boleh dipakai. Kode yang melanggar tidak pernah berhasil dikompilasi, sehingga batas privasi paket dijaga oleh alat bantu dan bukan oleh kepatuhan pembaca kode. Bila paket teks suatu hari dipindahkan keluar dari internal karena memang ditujukan untuk dipakai modul lain, jalur impornya ikut berubah menjadi jalur paket biasa dan seluruh pemakai lama harus menyesuaikan impornya.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul ketika tata letak proyek dirancang.

  1. Menumpuk logika pada main.go. Kode yang tumbuh di paket main tidak dapat diimpor dan tidak dapat diuji, sehingga setiap pembenahan menuntut penyalinan manual. Perbaikannya, pindahkan logika ke paket di internal sejak awal dan jadikan main.go lapisan tipis.
  2. Membuat folder pkg tanpa kebutuhan. Panduan resmi tata letak modul tidak memuat folder pkg, sehingga paket yang memang ditujukan untuk diimpor proyek lain cukup diletakkan langsung di akar modul atau di subfoldernya, sedangkan kode yang privat masuk ke internal.
  3. Menganggap folder internal sebagai penanda dokumentatif semata. Karena batasnya ditegakkan perintah go, pemindahan paket dari folder biasa ke internal langsung memutus build seluruh pemakai dari modul lain. Pemindahan tersebut hanya dilakukan untuk kode yang memang tidak dimaksudkan untuk diimpor dari luar.

Ringkasan#

  • cmd menyimpan satu subfolder paket main untuk setiap biner, dan nama subfolder terakhirnya menjadi nama biner hasil build.
  • internal menyimpan paket privat yang batas impornya ditegakkan perintah go, sehingga kode di luar pohon induk folder internal tidak dapat memakainya.
  • api menyimpan definisi API publik seperti spesifikasi OpenAPI menurut konvensi komunitas, dan tidak disentuh perangkat go.
  • main.go yang tipis menjaga logika tetap berada pada paket yang dapat diuji, sedangkan go.mod tetap menjadi deklarasi tunggal di akar modul.

Referensi#