BAB 19: Public dan Private — Mengontrol Akses

Pelajari konsep exported dan unexported di Go, cara mengontrol akses field dan method antar package, serta fungsi init() yang berjalan otomatis.

Setiap struct dan method yang kamu tulis di bab-bab sebelumnya hidup dalam satu file main.go di satu package bernama main. Ini bekerja dengan baik untuk program kecil. Tapi ketika program tumbuh, memisahkan kode ke package-package yang berbeda adalah keharusan — dan di situlah pertanyaan muncul: field mana yang boleh diakses dari luar, dan mana yang tidak?

Di Go, jawabannya terletak pada satu aturan sederhana yang sudah sempat disinggung di bab struct: huruf kapital.

Exported dan Unexported

Go tidak mengenal kata kunci public atau private. Sebagai gantinya, Go menggunakan konvensi penamaan:

  • Nama yang diawali huruf kapital bersifat exported — bisa diakses dari package lain.
  • Nama yang diawali huruf kecil bersifat unexported — hanya bisa diakses dalam package yang sama.

Aturan ini berlaku untuk semua level: struct, field, method, function, variabel, dan konstanta.

// package model

type Pengguna struct {
    Nama     string  // exported — bisa diakses dari luar
    Email    string  // exported
    password string  // unexported — private untuk package model
}

func (p *Pengguna) GantiPassword(baru string) {  // exported method
    p.password = baru  // akses field unexported dari dalam package sendiri — valid
}

func (p *Pengguna) validasiPassword(pw string) bool {  // unexported method
    return p.password == pw
}

Memisahkan Kode ke Package Terpisah

Mari buat contoh nyata. Kita akan punya dua package: model yang mendefinisikan struct, dan main yang menggunakannya.

Buat struktur folder berikut:

toko/
├── main.go
└── model/
    └── produk.go
// model/produk.go
package model

type Produk struct {
    Nama   string
    Harga  float64
    stok   int     // unexported — tidak terlihat dari luar package
}

func NewProduk(nama string, harga float64, stokAwal int) *Produk {
    return &Produk{
        Nama:  nama,
        Harga: harga,
        stok:  stokAwal,
    }
}

func (p *Produk) Stok() int {
    return p.stok
}

func (p *Produk) TambahStok(jumlah int) {
    p.stok += jumlah
}

func (p *Produk) KurangiStok(jumlah int) bool {
    if p.stok < jumlah {
        return false
    }
    p.stok -= jumlah
    return true
}
// main.go
package main

import (
    "fmt"
    "toko/model"
)

func main() {
    p := model.NewProduk("Kopi Arabika", 85000, 100)

    fmt.Println("Nama:", p.Nama)
    fmt.Println("Harga:", p.Harga)
    fmt.Println("Stok:", p.Stok())

    berhasil := p.KurangiStok(10)
    fmt.Println("Berhasil kurangi stok:", berhasil)
    fmt.Println("Stok sekarang:", p.Stok())

    // p.stok = 999 — ini akan ERROR: p.stok undefined (cannot refer to unexported field)
}
Nama: Kopi Arabika
Harga: 85000
Stok: 100
Berhasil kurangi stok: true
Stok sekarang: 90

stok di-hide dari luar dengan sengaja. Kode di luar package model tidak bisa langsung mengubah p.stok = 999. Satu-satunya cara mengubah stok adalah lewat method TambahStok dan KurangiStok — yang menerapkan validasi. Ini persis inti dari enkapsulasi.

Pola NewProduk(...) yang mengembalikan pointer ke struct adalah constructor function — cara idiomatis Go untuk memastikan struct selalu diinisialisasi dengan benar. Karena stok unexported, tidak ada cara lain untuk mengisi nilainya selain lewat constructor ini.

Kapan Gunakan Unexported?

Tidak semua field harus di-export. Pertimbangkan untuk menjadikan field unexported ketika:

  • Field tersebut adalah detail implementasi yang tidak perlu diketahui pengguna package.
  • Mengubah field secara langsung bisa menyebabkan state yang tidak konsisten.
  • Field hanya bermakna dalam konteks method-method di package itu sendiri.

Sebaliknya, field sebaiknya di-export ketika struct dimaksudkan sebagai data transfer object (DTO) yang dikirim antar layer, seperti struct yang di-marshal ke JSON atau dibaca dari database.

Import dan Teknik Penamaan Package

Saat menggunakan package, kamu bisa mengontrol bagaimana package itu disebut dalam kode:

// main.go
package main

import (
    "fmt"

    // import dengan alias
    m "toko/model"
)

func main() {
    p := m.NewProduk("Teh Hijau", 45000, 50)
    fmt.Println(p.Nama)
}

Alias berguna ketika nama package terlalu panjang atau berbenturan dengan nama lain. Selain alias, ada juga blank import dengan _ yang mengeksekusi init() package tanpa membuat namanya tersedia:

import _ "database/sql/driver"  // mengeksekusi init() saja

Fungsi init()

Setiap package bisa mendefinisikan satu atau lebih fungsi bernama init(). Fungsi ini tidak perlu dipanggil secara eksplisit — Go memanggilnya secara otomatis saat package di-import, sebelum main() berjalan.

// model/produk.go
package model

import "fmt"

var katalog []*Produk

func init() {
    // dieksekusi otomatis saat package model di-import
    katalog = make([]*Produk, 0, 100)
    fmt.Println("Package model siap digunakan")
}

func TambahKeKatalog(p *Produk) {
    katalog = append(katalog, p)
}

func JumlahKatalog() int {
    return len(katalog)
}
// main.go
package main

import (
    "fmt"
    "toko/model"
)

func main() {
    fmt.Println("main dimulai")

    p1 := model.NewProduk("Kopi", 85000, 100)
    p2 := model.NewProduk("Teh", 45000, 50)

    model.TambahKeKatalog(p1)
    model.TambahKeKatalog(p2)

    fmt.Println("Jumlah produk di katalog:", model.JumlahKatalog())
}
Package model siap digunakan
main dimulai
Jumlah produk di katalog: 2

Perhatikan urutan output: “Package model siap digunakan” muncul sebelum “main dimulai”. init() berjalan dulu karena package model di-import sebelum main() mulai.

init() tidak menerima parameter dan tidak mengembalikan nilai. Kamu tidak bisa memanggil init() secara manual dari kode. Jika butuh inisialisasi yang bisa diulangi, buat function biasa dengan nama lain.

Latihan

Latihan 1 — Package user: Buat package user dengan struct Akun yang punya field exported Username dan Email, serta field unexported passwordHash string. Buat method SetPassword(plain string) yang menyimpan hash sederhana (cukup "hash:" + plain), dan method CekPassword(plain string) bool. Dari main, coba akses passwordHash langsung dan amati error yang muncul.

Latihan 2 — Constructor wajib: Buat package keuangan dengan struct Transaksi yang punya field ID, Jumlah, dan Tipe (debit/kredit). Jadikan semua field unexported, lalu buat constructor NewTransaksi(tipe string, jumlah float64) *Transaksi yang memvalidasi bahwa jumlah harus positif. Tambahkan method getter untuk membaca setiap field.

Latihan 3 — init(): Tambahkan fungsi init() ke package keuangan yang mencetak pesan saat package di-load. Buat variabel package-level counter int yang di-increment setiap kali NewTransaksi dipanggil. Tambahkan function TotalTransaksi() int yang mengembalikan nilainya.

Exported dan unexported menentukan batas antara “detail implementasi” dan “kontrak publik” sebuah package. Dengan memahami ini, kamu sudah siap untuk topik yang lebih powerful: interface. Interface adalah cara Go mendefinisikan kontrak publik tanpa peduli siapa yang mengimplementasikannya.

Referensi

  1. 1Exported identifiers — The Go Programming Language Specification
  2. 2Package names — Effective Go
  3. 3Package initialization — The Go Programming Language Specification