BAB 16: Pointer — Mengoper Alamat, Bukan Salinan
Pelajari konsep pointer di Go: cara kerja referencing dan dereferencing, perbedaan pass-by-value dan pass-by-reference, serta kapan harus menggunakan pointer agar program lebih efisien dan benar.
Di bab sebelumnya, kamu mencoba membuat function yang mengubah nilai variabel di luarnya. Tapi ada masalah: ketika kamu mengoper variabel ke function, Go membuat salinan nilainya. Modifikasi di dalam function hanya mengubah salinan itu — variabel aslinya tidak tersentuh.
Coba perhatikan ini:
// main.go
package main
import "fmt"
func gandakan(n int) {
n = n * 2
fmt.Println("di dalam function:", n)
}
func main() {
angka := 5
gandakan(angka)
fmt.Println("di luar function:", angka) // tetap 5!
}
di dalam function: 10
di luar function: 5
angka tidak berubah karena function gandakan bekerja dengan salinannya, bukan dengan angka itu sendiri. Untuk membuat function yang benar-benar mengubah variabel asli, kamu perlu pointer.
Apa Itu Pointer?
Setiap variabel disimpan di suatu lokasi di memori komputer, dan lokasi itu punya alamat — sebuah angka heksadesimal seperti 0xc0000b4008. Pointer adalah variabel yang menyimpan alamat tersebut, bukan nilainya langsung.
Analogi sederhananya: bayangkan variabel biasa seperti sebuah kotak berisi angka. Pointer adalah selembar kertas yang berisi alamat kotak itu. Ketika kamu memberikan pointer ke function, function bisa pergi ke alamat itu dan mengubah isi kotak langsung.
Di Go, pointer bekerja dengan dua operator:
| Operator | Nama | Fungsi |
|---|---|---|
& | referencing | Mengambil alamat memori sebuah variabel |
* | dereferencing | Mengambil nilai di alamat yang disimpan pointer |
Deklarasi dan Penggunaan Dasar
// main.go
package main
import "fmt"
func main() {
angka := 42
// & mengambil alamat memori angka
pAngka := &angka
fmt.Println("nilai angka :", angka)
fmt.Println("alamat angka :", &angka)
fmt.Println("isi pAngka :", pAngka) // sama dengan &angka
fmt.Println("nilai via ptr :", *pAngka) // * mengambil nilai di alamat tsb
}
nilai angka : 42
alamat angka : 0xc0000b4008
isi pAngka : 0xc0000b4008
nilai via ptr : 42
pAngka dan &angka menunjuk ke lokasi memori yang sama. Ketika kamu mengubah nilai via pointer, variabel aslinya ikut berubah:
// main.go
package main
import "fmt"
func main() {
angka := 42
pAngka := &angka
*pAngka = 100 // ubah nilai via pointer
fmt.Println("angka sekarang:", angka) // 100 — bukan 42 lagi
}
angka sekarang: 100
Parameter Pointer pada Function
Ini adalah penerapan pointer yang paling sering kamu temui. Dengan mengoper pointer sebagai parameter, function bisa memodifikasi variabel asli di pemanggil.
// main.go
package main
import "fmt"
func gandakan(n *int) {
*n = *n * 2 // modifikasi nilai di alamat yang ditunjuk
}
func tukar(a, b *int) {
*a, *b = *b, *a
}
func main() {
angka := 5
gandakan(&angka)
fmt.Println("setelah gandakan:", angka) // 10
x, y := 3, 7
tukar(&x, &y)
fmt.Printf("setelah tukar: x=%d, y=%d\n", x, y) // x=7, y=3
}
setelah gandakan: 10
setelah tukar: x=7, y=3
Perhatikan: saat memanggil function, kamu mengoper &angka (alamatnya), bukan angka (nilainya). Di dalam function, *n digunakan untuk mengakses dan memodifikasi nilai di alamat tersebut.
Pass-by-Value vs Pass-by-Pointer
Go selalu pass by value — setiap argumen yang kamu oper ke function akan disalin. Tapi ketika nilai yang disalin itu adalah sebuah alamat (pointer), maka function punya akses ke data aslinya.
// main.go
package main
import "fmt"
// pass by value — tidak mengubah aslinya
func tambahSatu(n int) {
n++
}
// pass by pointer — mengubah aslinya
func tambahSatuPtr(n *int) {
*n++
}
func main() {
a := 10
tambahSatu(a)
fmt.Println("setelah tambahSatu:", a) // 10, tidak berubah
b := 10
tambahSatuPtr(&b)
fmt.Println("setelah tambahSatuPtr:", b) // 11, berubah
}
setelah tambahSatu: 10
setelah tambahSatuPtr: 11
Ini juga relevan untuk efisiensi. Kalau kamu mengoper struct berukuran besar ke function, Go menyalin seluruh struct itu. Dengan pointer, yang disalin hanya alamatnya — jauh lebih kecil dan cepat.
Pointer dengan new
Selain &, kamu bisa membuat pointer ke nilai baru menggunakan fungsi bawaan new:
// main.go
package main
import "fmt"
func main() {
// new(int) mengalokasikan int baru dengan zero value, lalu mengembalikan pointernya
p := new(int)
fmt.Println("nilai awal:", *p) // 0
*p = 55
fmt.Println("setelah diisi:", *p) // 55
}
nilai awal: 0
setelah diisi: 55
new(T) selalu mengalokasikan nilai baru dengan zero value dan mengembalikan pointernya. Ini berguna ketika kamu butuh pointer ke nilai sementara tanpa perlu mendeklarasikan variabel terlebih dahulu.
Kapan Pakai Pointer, Kapan Tidak
Pointer bukan solusi untuk semua masalah. Berikut panduan praktisnya:
Gunakan pointer ketika:
- Function perlu memodifikasi variabel di pemanggil
- Mengoper struct berukuran besar untuk menghindari salinan yang mahal
- Ingin membedakan antara “nilai nol” dan “tidak ada nilai” (pointer
nilvs*Tdengan nilai)
Hindari pointer ketika:
- Data kecil dan sederhana (int, bool, string pendek) — salinan lebih cepat dari dereferencing
- Kode menjadi lebih sulit dibaca tanpa manfaat nyata
- Kamu belum yakin apakah pointer benar-benar dibutuhkan
Hati-hati dengan nil pointer dereference — error paling umum saat bekerja dengan pointer. Jika pointer bernilai nil dan kamu mencoba mengakses nilainya dengan *p, program akan panic. Selalu pastikan pointer sudah menunjuk ke sesuatu sebelum di-dereference.
Latihan
Latihan 1 — Tukar tiga nilai:
Tulis function rotasi(a, b, c *int) yang merotasi tiga nilai: nilai a pindah ke b, b ke c, dan c ke a. Uji dengan x, y, z := 1, 2, 3 dan tampilkan hasilnya sebelum dan sesudah rotasi.
Latihan 2 — Modifikasi slice via pointer:
Buat function isiNol(data *[]int) yang mengubah semua elemen slice menjadi 0 menggunakan pointer ke slice. Bandingkan hasilnya jika kamu mengoper slice langsung (bukan pointer ke slice) — apakah perbedaannya?
Latihan 3 — Pointer dalam map:
Buat map[string]*int yang menyimpan pointer ke nilai int. Tambahkan key “stok” dengan nilai 100, lalu buat function kurangi(m map[string]*int, key string, jumlah int) yang mengurangi nilai di map tanpa mengembalikan map baru. Mengapa ini berbeda dari mengoper map[string]int biasa?
Pointer membuka cara berpikir baru tentang bagaimana data bergerak di dalam program. Di bab-bab selanjutnya, kamu akan bertemu pointer lagi — terutama saat bekerja dengan struct dan method, di mana pointer receiver sangat sering dipakai.