Ketika module dipublikasikan, semua package di dalamnya secara default dapat diimpor siapa saja — termasuk package yang sebenarnya hanya menjadi detail implementasi. Mengubah detail semacam itu lalu berpotensi merusak proyek orang lain yang tidak kita ketahui. Go menjawab kebutuhan ini dengan aturan direktori internal/ yang ditegakkan langsung oleh compiler: package di dalamnya hanya boleh diimpor oleh kode pada pohon direktori induknya. Pada chapter ini kita akan membedah aturannya, membuktikan dua galat pelanggaran secara nyata, lalu membandingkannya dengan mekanisme unexported dari chapter sebelumnya.

Aturan Pohon internal #

Aturan lengkapnya ringkas: import path yang memuat segmen internal hanya sah bila kode pengimpor berada di dalam subtree direktori induk dari internal tersebut. Ambil struktur berikut dari bundle chapter ini (module contoh.com/gudang):

text
src/
├── go.mod                      ← module contoh.com/gudang
├── main.go
├── internal/
│   └── stok/                   ← boleh diimpor oleh SEMUA package di gudang
└── layanan/
    ├── layanan.go
    └── internal/
        └── pajak/              ← hanya boleh diimpor oleh package di bawah layanan/

Direktori internal/ pertama berada langsung di bawah akar module, jadi induknya adalah seluruh module — semua package contoh.com/gudang/... boleh mengimpornya. Direktori internal/ kedua berada di bawah layanan/, jadi induknya hanya contoh.com/gudang/layanan/... — kode lain, termasuk main.go di akar, ditolak. Semakin dalam internal diletakkan, semakin sempit lingkup pemuji yang diizinkan.

Demo: Import yang Sah #

Program utama module demo memanggil kedua batas tersebut dengan benar — internal/stok diimpor dari akar, sementara layanan/internal/pajak hanya diimpor dari dalam layanan:

go
package main

import (
	"fmt"

	"contoh.com/gudang/internal/stok"
	"contoh.com/gudang/layanan"
)

func main() {
	nilai := stok.Total(25000, 10)
	fmt.Println("stok    :", nilai)
	layanan.Tagihan(nilai)
}

Package layanan menjadi jembatan satu-satunya menuju pajak:

go
package layanan

import (
	"fmt"

	"contoh.com/gudang/layanan/internal/pajak"
)

// Tagihan mencetak nilai dasar beserta pajaknya.
func Tagihan(dasar int) {
	fmt.Println("tagihan:", dasar, "pajak:", pajak.Hitung(dasar))
}

Jalankan dari dalam direktori src/:

bash
cd src && go run .

Output yang Dihasilkan:

text
stok    : 250000
tagihan: 250000 pajak: 25000

Mari kita telaah alur kedua berkas di atas:

  1. main.go baris 3-8 (blok import): mengimpor internal/stok — sah karena main.go berada dalam pohon induk internal/ tersebut (akar module).
  2. main.go baris 11 (stok.Total): memanggil fungsi internal module lewat pintu yang memang dibuka untuk seluruh module.
  3. main.go baris 13 (layanan.Tagihan): berinteraksi dengan pajak secara tidak langsung — main.go sama sekali tidak menyebut pajak, sebab akses langsung akan gagal dikompilasi.
  4. layanan.go baris 3-7: layanan berada di bawah layanan/ sendiri, sehingga mengimpor layanan/internal/pajak diperbolehkan — inilah posisi induk yang dimaksud aturan.

Galat Nyata: Melanggar Batas internal #

Kedua pelanggaran berikut dijalankan sungguhan pada salinan module demo. Pertama, main.go dipindah ke akar mencoba mengimpor pajak yang bukan wilayahnya:

go
import "contoh.com/gudang/layanan/internal/pajak"

Output yang Dihasilkan:

text
package contoh.com/gudang
	main.go:7:2: use of internal package contoh.com/gudang/layanan/internal/pajak not allowed

Kedua, module lain (dengan replace ke direktori demo) mencoba mengimpor contoh.com/gudang/internal/stok dari luar module gudang:

Output yang Dihasilkan:

text
package contoh.com/konsumen
	main.go:6:2: use of internal package contoh.com/gudang/internal/stok not allowed

Bentuk pesannya sama: use of internal package ... not allowed, diikuti posisi berkas pengimpor. Perhatikan kasus kedua — meskipun internal/ pertama tadi “dibuka untuk seluruh module”, pintu itu tertutup rapat untuk module lain: contoh.com/konsumen berada di luar pohon contoh.com/gudang sama sekali.

internal atau unexported? #

MekanismeTingkatMelindungi
internal/Antar packageSeluruh package dari pemanggil yang tidak berhak
Unexported (huruf kecil)Antar package jugaIdentifier individual di dalam satu package

Keduanya saling melengkapi: internal/ menyembunyikan package utuh — API privat, helper domain, kode hasil generate — sedangkan unexported menyembunyikan anggota di dalam package yang memang dibuka. Kombinasi umumnya: taruh detail di internal/, lalu di package pintu masuk hanya ekspor identifier yang benar-benar stabil.

Dengan internal/, arsitektur kita kini punya pagar antar wilayah. Persoalan berikutnya lebih ringan namun keseharian: import path yang panjang dan bentrok nama package. Solusinya — alias — kita bahas pada chapter Package Alias .

Referensi #

Berkas Sumber

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

src/internal/stok/stok.go

Unduh berkas
go
package stok

// Total menghitung nilai stok berdasarkan harga satuan.
func Total(hargaSatuan, jumlah int) int {
	return hargaSatuan * jumlah
}

src/layanan/internal/pajak/pajak.go

Unduh berkas
go
package pajak

// Hitung menghitung pajak atas nilai dasar.
func Hitung(dasar int) int {
	return dasar / 10
}

src/layanan/layanan.go

Unduh berkas
go
package layanan

import (
	"fmt"

	"contoh.com/gudang/layanan/internal/pajak"
)

// Tagihan mencetak nilai dasar beserta pajaknya.
func Tagihan(dasar int) {
	fmt.Println("tagihan:", dasar, "pajak:", pajak.Hitung(dasar))
}

src/main.go

Unduh berkas
go
package main

import (
	"fmt"

	"contoh.com/gudang/internal/stok"
	"contoh.com/gudang/layanan"
)

func main() {
	nilai := stok.Total(25000, 10)
	fmt.Println("stok    :", nilai)
	layanan.Tagihan(nilai)
}