Module yang baru diinisialisasi hanya berisi go.mod dan satu berkas main.go — cukup untuk program kecil. Namun begitu jumlah package bertambah dari tiga menjadi sepuluh, penempatan berkas mulai menentukan kesehatan proyek: program mana yang bisa dieksekusi, kode mana yang boleh diimpor pihak luar, dan kode mana yang benar-benar privat. Komunitas Go menyepakati sebuah pola layout yang sederhana namun elastis, dibangun dari tiga direktori: cmd/, internal/, dan pkg/. Pada chapter ini kita akan menyusun layout tersebut dalam module demo dan mengujinya sampai tuntas.

Proyek Kecil: Cukup main.go di Akar #

Jangan menerapkan struktur berlapis untuk skrip atau program mainan. Module di bawah ini sudah sah dan idiomatik:

text
catatan/
├── go.mod          ← module contoh.com/catatan
└── main.go         ← package main, satu-satunya berkas

Aturan praktisnya: seluruh kode masih muat dibaca dalam satu tarikan napas dan tidak ada package lain yang mengimpor kode Anda, satu main.go di akar sudah memadai. Struktur bertingkat baru memberi nilai ketika module memiliki lebih dari satu program atau lebih dari satu package.

Layout Standar: cmd, internal, pkg #

Bundle chapter ini berisi module demo contoh.com/toko dengan tiga direktori kunci:

text
src/
├── go.mod                  ← module contoh.com/toko
├── cmd/
│   └── kasir/
│       └── main.go         ← package main (program kasir)
├── internal/
│   └── harga/
│       └── harga.go        ← logika bisnis, khusus module ini
└── pkg/
    └── format/
        └── format.go       ← utilitas format, boleh dipakai pihak luar

Ketiga direktori tersebut punya peran yang saling melengkapi:

DirektoriIsiPeran
cmd/namaapp/Package mainSatu subdirektori untuk setiap program yang dieksekusi
internal/Package apa punKode privat; import dari module lain ditolak compiler
pkg/Package apa punKode yang secara sengaja dibuka untuk pemakai luar

Program cmd/kasir/main.go merangkai kedua package pendukung:

go
package main

import (
	"fmt"

	"contoh.com/toko/internal/harga"
	"contoh.com/toko/pkg/format"
)

func main() {
	items := []int{15000, 27500, 8000}
	total := harga.Total(items)
	fmt.Println("Jumlah item :", len(items))
	fmt.Println("Total       :", format.Rupiah(total))
}

Sementara internal/harga/harga.go menjumlahkan daftar harga, dan pkg/format/format.go memformat angka menjadi teks rupiah:

go
package harga

// Total menjumlahkan seluruh harga item.
func Total(items []int) int {
	total := 0
	for _, h := range items {
		total += h
	}
	return total
}
go
package format

import "strconv"

// Rupiah memformat bilangan bulat menjadi teks rupiah berpemisah titik.
func Rupiah(n int) string {
	s := strconv.Itoa(n)
	if len(s) <= 3 {
		return "Rp" + s
	}
	hasil := ""
	for len(s) > 3 {
		hasil = "." + s[len(s)-3:] + hasil
		s = s[:len(s)-3]
	}
	return "Rp" + s + hasil
}

Karena main.go tidak lagi berada di akar module, kita menjalankannya dengan path package, bukan titik:

bash
cd src && go run ./cmd/kasir

Output yang Dihasilkan:

text
Jumlah item : 3
Total       : Rp50.500

Mari kita telaah baris demi baris berkas utama:

  1. Baris 1 (package main): berkas ini adalah program yang dapat dieksekusi, berada di subdirektori cmd/kasir/.
  2. Baris 3-8 (blok import): mengimpor package internal/harga untuk logika bisnis dan pkg/format untuk format tampilan; import path lengkap dimulai dari module path contoh.com/toko.
  3. Baris 11 (items := ...): mendeklarasikan potongan harga tiga item sebagai data masukan.
  4. Baris 12 (harga.Total(items)): memanggil fungsi dari direktori internal/ — kode privat yang tak terlihat dari luar module.
  5. Baris 13-14 (fmt.Println): mencetak ringkasan; format.Rupiah mengubah angka 50500 menjadi teks Rp50.500.

Direktori cmd: Satu Subdirektori per Program #

Direktori cmd/ menampung seluruh titik masuk program. Setiap subdirektori di dalamnya memuat satu package main dengan nama yang mencerminkan biner yang dihasilkan: cmd/kasir menghasilkan biner kasir, cmd/migrasi menghasilkan migrasi. Nama subdirektori inilah yang menjadi nama berkas biner setelah go build ./cmd/kasir, sehingga penamaan yang deskriptif sekaligus singkat sangat berpengaruh pada pengalaman pengguna CLI Anda.

Direktori internal dan pkg #

Direktori internal/ menikmati perlindungan khusus dari compiler: package di dalamnya hanya boleh diimpor oleh kode pada pohon direktori induknya. Aturan lengkap dan demo galatnya akan kita bedah pada chapter Package internal . Direktori pkg/ tidak punya perlakuan khusus dari compiler — fungsinya murni konvensi untuk memberi sinyal “kode di sini aman diimpor pihak luar”.

Memilih Layout Berdasarkan Ukuran Proyek #

Ukuran proyekBentuk yang cocok
Skrip / latihanmain.go di akar module
Satu aplikasi, beberapa packagecmd/app/ + internal/
Aplikasi + library publikcmd/app/ + internal/ + pkg/ (atau package publik di akar)
Banyak aplikasi / monorepoBanyak subdirektori cmd/, internal/ per domain, pertimbangkan go.work

Pola cmd/ + internal/ menutup mayoritas kebutuhan proyek aplikasi; tambahkan pkg/ hanya ketika memang ada package yang sengaja dipublikasikan. Setelah kerangka direktori berdiri, pertanyaan berikutnya adalah bagaimana mengontrol kode mana di dalam package yang boleh diakses dari luar — jawabannya ada pada kapitalisasi huruf pertama identifier, yang kita bahas pada chapter Export dan Unexported .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

src/cmd/kasir/main.go

Unduh berkas
go
package main

import (
	"fmt"

	"contoh.com/toko/internal/harga"
	"contoh.com/toko/pkg/format"
)

func main() {
	items := []int{15000, 27500, 8000}
	total := harga.Total(items)
	fmt.Println("Jumlah item :", len(items))
	fmt.Println("Total       :", format.Rupiah(total))
}

src/internal/harga/harga.go

Unduh berkas
go
package harga

// Total menjumlahkan seluruh harga item.
func Total(items []int) int {
	total := 0
	for _, h := range items {
		total += h
	}
	return total
}

src/pkg/format/format.go

Unduh berkas
go
package format

import "strconv"

// Rupiah memformat bilangan bulat menjadi teks rupiah berpemisah titik.
func Rupiah(n int) string {
	s := strconv.Itoa(n)
	if len(s) <= 3 {
		return "Rp" + s
	}
	hasil := ""
	for len(s) > 3 {
		hasil = "." + s[len(s)-3:] + hasil
		s = s[:len(s)-3]
	}
	return "Rp" + s + hasil
}