Belajar Golang dari Nol #7: Pointer, Alamat Memori Tanpa Pusing
Di Belajar Golang dari Nol #6 kita sudah belajar mengendalikan alur program dengan for dan percabangan. Sekarang kita masuk ke materi yang paling sering bikin pemula gentar sebelum mencoba: pointer. Padahal konsepnya sederhana. Anda hanya perlu satu analogi yang pas, dan sisanya akan terasa masuk akal.
Masih ingat di bagian 4 kita menyinggung pointer receiver saat membahas method, lalu saya janji akan membahasnya tuntas? Ini bagian yang menepati janji itu.
Analogi: Fotokopi Dokumen vs Alamat Rumah
Bayangkan Anda punya dokumen penting di rumah, misalnya sertifikat tanah. Teman Anda butuh dokumen itu. Ada dua cara Anda memberikannya.
Cara pertama, Anda fotokopi dokumennya lalu kirim fotokopiannya. Teman Anda boleh mencoret-coret fotokopian itu sesuka hati. Dokumen asli di rumah Anda tetap aman, tidak berubah sedikit pun.
Cara kedua, Anda kasih alamat rumah Anda. Teman Anda datang ke alamat itu, membuka lemari, dan mengubah dokumen aslinya langsung. Apa pun yang dia tulis di sana, itu terjadi pada dokumen asli.
Di Go, cara pertama adalah perilaku default: setiap nilai yang dioper ke function akan difotokopi. Cara kedua adalah pointer: Anda tidak mengoper nilainya, tapi mengoper alamat tempat nilai itu disimpan di memori. Pegang analogi ini terus. Semua materi di bawah hanyalah variasi dari dua cara itu.
Go Selalu Mengoper Salinan
Mari buktikan dulu perilaku defaultnya. Coba jalankan program ini.
package main
import "fmt"
func tambahBonus(gaji int) {
gaji = gaji + 500000
fmt.Println("Di dalam function:", gaji)
}
func main() {
gaji := 5000000
tambahBonus(gaji)
fmt.Println("Di main:", gaji)
}
Outputnya:
Di dalam function: 5500000
Di main: 5000000
Perhatikan baik-baik. Di dalam tambahBonus, nilai gaji memang bertambah jadi 5500000. Tapi begitu kembali ke main, nilainya tetap 5000000. Kenapa? Karena yang diterima function adalah fotokopian. Function mencoret-coret fotokopian itu, lalu fotokopiannya dibuang saat function selesai. Dokumen asli di main tidak pernah tersentuh.
Ini bukan bug. Ini desain Go yang disengaja, namanya pass by value. Tapi kadang kita memang ingin function lain mengubah data asli. Di situlah pointer masuk.
Dua Operator Kunci: & dan *
Untuk bermain dengan alamat memori, Go memberi kita dua operator.
&artinya “ambil alamat dari variabel ini”. Seperti bertanya, rumah si Budi di mana ya.*artinya “buka isi dari alamat ini”. Seperti datang ke alamat itu dan melihat apa yang ada di dalamnya.
Contoh kecilnya begini.
package main
import "fmt"
func main() {
umur := 25
alamat := &umur
fmt.Println("Nilai umur:", umur)
fmt.Println("Alamat umur:", alamat)
fmt.Println("Isi dari alamat:", *alamat)
}
Outputnya kira-kira seperti ini:
Nilai umur: 25
Alamat umur: 0xc000012028
Isi dari alamat: 25
Angka 0xc000012028 itu alamat memori. Di komputer Anda angkanya hampir pasti beda, dan tiap kali program dijalankan ulang angkanya juga bisa berubah. Itu normal. Sistem operasi bebas menaruh data di slot memori mana pun yang kosong, sama seperti mobil yang tidak selalu parkir di slot yang sama tiap ke mal.
Variabel alamat di atas bertipe *int, dibaca “pointer ke int”. Artinya dia tidak menyimpan angka 25, dia menyimpan alamat tempat angka 25 berada.
Function yang Benar-Benar Mengubah Nilai
Sekarang gabungkan keduanya. Kalau parameter function bertipe pointer, function itu menerima alamat, bukan fotokopian. Dia bisa datang ke alamat itu dan mengubah dokumen aslinya.
package main
import "fmt"
func ubahNilai(x *int) {
*x = 100
}
func ubahNilaiBiasa(x int) {
x = 100
}
func main() {
angka := 5
ubahNilaiBiasa(angka)
fmt.Println("Setelah versi biasa:", angka)
ubahNilai(&angka)
fmt.Println("Setelah versi pointer:", angka)
}
Outputnya:
Setelah versi biasa: 5
Setelah versi pointer: 100
Baca ubahNilai pelan-pelan. Parameter x *int artinya x adalah alamat. Baris *x = 100 artinya datang ke alamat itu, lalu tulis 100 di sana. Karena yang diubah adalah isi alamat asli, perubahan itu terlihat dari main. Dan perhatikan cara memanggilnya: ubahNilai(&angka). Kita tidak mengirim angkanya, kita mengirim alamatnya.
Pointer dan Struct: Rahasia di Balik Pointer Receiver
Sekarang kita bayar utang dari bagian 4. Waktu itu Anda sudah lihat method dengan receiver (r *Rekening) dan saya bilang pakai saja dulu, nanti dijelaskan. Ini penjelasannya.
package main
import "fmt"
type Pengguna struct {
Nama string
}
func (p Pengguna) GantiNamaValue(baru string) {
p.Nama = baru
}
func (p *Pengguna) GantiNamaPointer(baru string) {
p.Nama = baru
}
func main() {
u := Pengguna{Nama: "Budi"}
u.GantiNamaValue("Andi")
fmt.Println("Setelah value receiver:", u.Nama)
u.GantiNamaPointer("Andi")
fmt.Println("Setelah pointer receiver:", u.Nama)
}
Outputnya:
Setelah value receiver: Budi
Setelah pointer receiver: Andi
Sekarang mekanismenya jelas. Value receiver (p Pengguna) menerima fotokopian struct. Method mengganti nama di fotokopian, fotokopiannya dibuang, struct asli tetap Budi. Persis seperti kasus tambahBonus tadi. Pointer receiver (p *Pengguna) menerima alamat struct asli, jadi perubahannya nyata.
Ada satu kemudahan dari Go yang perlu Anda tahu. Secara teori, untuk mengakses field lewat pointer Anda harus menulis (*p).Nama: buka dulu isi alamatnya, baru ambil fieldnya. Tapi Go tahu itu merepotkan, jadi p.Nama saja sudah cukup. Go otomatis mengerti maksud Anda. Itulah kenapa di dalam GantiNamaPointer kita bisa menulis p.Nama = baru tanpa tanda bintang. Hal yang sama berlaku saat memanggil: u.GantiNamaPointer("Andi") otomatis diterjemahkan Go menjadi (&u).GantiNamaPointer("Andi").
Hati-Hati dengan Nil Pointer
Masih ingat konsep zero value dari bagian 2? Setiap tipe punya nilai awal. Zero value untuk pointer adalah nil, artinya pointer itu belum menunjuk ke alamat mana pun. Seperti secarik kertas alamat yang masih kosong.
Masalah muncul kalau Anda mencoba membuka isi dari alamat kosong.
package main
import "fmt"
func main() {
var p *int
fmt.Println(*p)
}
Program ini langsung crash dengan pesan:
panic: runtime error: invalid memory address or nil pointer dereference
Wajar saja. Anda menyuruh Go datang ke sebuah alamat, tapi kertas alamatnya kosong. Mau ke mana? Ini salah satu error paling umum di dunia Go, jadi biasakan mengecek dulu sebelum membuka isi pointer.
package main
import "fmt"
func main() {
var p *int
if p != nil {
fmt.Println("Isi p:", *p)
} else {
fmt.Println("p masih nil, belum menunjuk ke mana-mana")
}
}
Outputnya:
p masih nil, belum menunjuk ke mana-mana
Pola if p != nil ini akan sangat sering Anda temui di kode Go sungguhan, terutama saat menerima pointer dari function lain.
Kapan Pakai Pointer, Kapan Tidak
Supaya tidak bingung di lapangan, pegang aturan praktis ini.
Pakai pointer kalau:
- Anda ingin function atau method mengubah data aslinya, seperti semua contoh di atas.
- Struct Anda besar dan sering dioper ke banyak function. Mengoper alamat jauh lebih ringan daripada memfotokopi seluruh isi struct berulang kali.
- Menjaga konsistensi method. Kalau satu method di sebuah struct sudah pakai pointer receiver, sebaiknya semua method di struct itu pakai pointer receiver juga, biar tidak membingungkan pembaca kode.
Tidak perlu pointer kalau:
- Tipenya kecil seperti int, bool, atau string, dan Anda tidak berniat mengubahnya. Fotokopi tipe kecil itu murah.
- Datanya slice atau map. Ingat pelajaran bagian 5: slice dan map secara internal sudah menunjuk ke data yang sama, jadi perubahan isi di dalam function memang terlihat dari luar tanpa pointer.
Satu hal lagi yang layak disebut singkat: function bawaan new(). Menulis p := new(int) artinya Go membuatkan sebuah int dengan zero value, lalu langsung memberi Anda pointernya. Praktiknya, gaya p := &variabel jauh lebih sering dipakai, jadi cukup kenali saja bahwa new() ada.
Latihan: Sistem Saldo Rekening
Sekarang rangkai semuanya jadi satu program utuh. Kita buat sistem saldo sederhana: struct Rekening, method Setor dan Tarik dengan pointer receiver, plus function TransferDana yang menerima dua pointer sekaligus. Method Tarik mengembalikan error kalau saldo kurang, persis pola error yang Anda pelajari di bagian 3.
package main
import (
"errors"
"fmt"
)
type Rekening struct {
Pemilik string
Saldo int
}
func (r *Rekening) Setor(jumlah int) {
r.Saldo = r.Saldo + jumlah
}
func (r *Rekening) Tarik(jumlah int) error {
if jumlah > r.Saldo {
return errors.New("saldo tidak cukup")
}
r.Saldo = r.Saldo - jumlah
return nil
}
func TransferDana(dari *Rekening, ke *Rekening, jumlah int) error {
err := dari.Tarik(jumlah)
if err != nil {
return err
}
ke.Setor(jumlah)
return nil
}
func main() {
rekeningBudi := Rekening{Pemilik: "Budi", Saldo: 500000}
rekeningSari := Rekening{Pemilik: "Sari", Saldo: 200000}
rekeningBudi.Setor(100000)
fmt.Println("Saldo Budi setelah setor:", rekeningBudi.Saldo)
err := TransferDana(&rekeningBudi, &rekeningSari, 250000)
if err != nil {
fmt.Println("Transfer gagal:", err)
} else {
fmt.Println("Transfer 250000 dari Budi ke Sari berhasil")
}
fmt.Println("Saldo Budi:", rekeningBudi.Saldo)
fmt.Println("Saldo Sari:", rekeningSari.Saldo)
err = TransferDana(&rekeningBudi, &rekeningSari, 1000000)
if err != nil {
fmt.Println("Transfer gagal:", err)
}
fmt.Println("Saldo Budi:", rekeningBudi.Saldo)
fmt.Println("Saldo Sari:", rekeningSari.Saldo)
}
Outputnya:
Saldo Budi setelah setor: 600000
Transfer 250000 dari Budi ke Sari berhasil
Saldo Budi: 350000
Saldo Sari: 450000
Transfer gagal: saldo tidak cukup
Saldo Budi: 350000
Saldo Sari: 450000
Telusuri alurnya. Setor menambah saldo Budi jadi 600000, dan perubahannya nyata karena receivernya pointer. TransferDana menerima alamat dua rekening sekaligus, jadi dia bisa mengurangi saldo Budi dan menambah saldo Sari dalam satu function. Transfer kedua gagal karena Budi hanya punya 350000, dan lihat saldonya: tidak ada satu rupiah pun yang berubah saat transfer gagal. Method Tarik menolak duluan sebelum sempat menyentuh apa pun.
Coba modifikasi sendiri sebagai latihan. Misalnya, tambahkan validasi agar jumlah setor tidak boleh negatif, atau buat method Info yang mencetak pemilik dan saldo dengan rapi. Kalau kode Anda berperilaku sesuai dugaan, berarti pointer sudah masuk ke kepala.
Rangkuman dan Lanjut ke Bagian 8
Hari ini Anda belajar bahwa Go mengoper nilai secara fotokopi, pointer adalah cara mengoper alamat aslinya, & mengambil alamat, * membuka isinya, pointer receiver bekerja karena menerima alamat struct asli, dan nil pointer wajib dicek sebelum dibuka. Materi yang katanya menakutkan ternyata cuma soal fotokopi dan alamat rumah.
Sejauh ini semua kode kita masih menumpuk di satu file main.go. Untuk program kecil tidak masalah, tapi program sungguhan butuh struktur. Di bagian 8 kita akan membahas Package dan Go Module: Merapikan Kode ke Banyak File. Sampai ketemu di sana.
Kalau bisnis Anda butuh aplikasi yang dibangun dengan fondasi backend yang rapi seperti ini, tim Arrazy siap bantu lewat layanan pengembangan sistem aplikasi.
Artikel Lainnya di Kategori Golang
Golang 28 Juli 2026
Belajar Golang dari Nol #2: Variabel, Tipe Data, dan Zero Value
Lanjutan seri Go pemula: tiga cara deklarasi variabel, tipe data dasar, zero value, konstanta, konversi tipe, plus latihan program hitung belanja.
Baca Artikel
Golang 1 Agustus 2026
Belajar Golang dari Nol #6: Perulangan for dan Percabangan
Lanjutan seri Go pemula: tiga bentuk for, for range tuntas, break dan continue, if dengan statement pendek, switch, plus latihan rekap penjualan.
Baca Artikel
Golang 31 Juli 2026
Belajar Golang dari Nol #5: Slice dan Map, Kumpulan Data di Go
Lanjutan seri Go pemula: slice, append, map, comma ok idiom, jebakan nil map, plus latihan keranjang belanja mini dengan pengecekan stok.
Baca ArtikelIngin Membaca Artikel Lainnya?
Temukan lebih banyak insight dan tips tentang teknologi dan bisnis digital.
Lihat Semua Artikel