Setiap penugasan di Go menyalin nilai yang disimpan: pernyataan b := a menghasilkan salinan independen sehingga perubahan pada b tidak pernah menyentuh a. Perilaku salin ini sederhana dan aman, tetapi menimbulkan dua pertanyaan praktis: bagaimana bila struct berukuran besar harus berpindah-pindah antar fungsi, dan bagaimana bila sebuah fungsi memang harus mengubah data milik pemanggilnya? Chapter ini membandingkan semantik salinan (value) dengan semantik referensi lewat pointer, lalu menyusun panduan memilih di antara keduanya.
Semantik Salinan pada Nilai #
Ketika kita menuliskan b := a untuk dua variabel bertipe struct, runtime menyalin seluruh isi struct field demi field. Fungsi yang menerima parameter bertipe struct juga menerima salinan: perubahan apa pun pada parameter tersebut berakhir ketika fungsi kembali. Sebaliknya, parameter bertipe pointer hanya menyalin satu alamat, sehingga fungsi bekerja langsung pada objek milik pemanggil. Perbedaan dua semantik ini menentukan seluruh keputusan desain API di Go, dari signature fungsi hingga bentuk receiver pada method.
Contoh Utuh: Salinan yang Terpisah dan Pointer yang Berbagi #
Program berikut membandingkan kedua semantik pada struct Pengaturan. Fungsi ubahSalinan menerima struct dan mengembalikan salinan yang telah dinaikkan Level-nya, sedangkan fungsi ubahAsli menerima pointer dan menaikkan field Level langsung pada struct pemanggil:
package main
import "fmt"
type Pengaturan struct {
Nama string
Level int
}
// ubahSalinan menerima salinan struct dan mengembalikan salinan yang diubah.
func ubahSalinan(p Pengaturan) Pengaturan {
p.Level = p.Level + 1
return p
}
// ubahAsli menerima pointer dan mengubah struct milik pemanggil.
func ubahAsli(p *Pengaturan) {
p.Level = p.Level + 1
}
func main() {
asli := Pengaturan{Nama: "Budi", Level: 1}
salinan := ubahSalinan(asli)
fmt.Println("asli.Level :", asli.Level) // tetap 1
fmt.Println("salin.Level :", salinan.Level) // menjadi 2
ubahAsli(&asli)
fmt.Println("asli.Level setelah ubahAsli:", asli.Level) // menjadi 2
// operator penugasan juga menyalin seluruh struct
a := asli
a.Level = 99
fmt.Println("asli.Level setelah a diubah :", asli.Level) // tetap 2
}Jalankan program tersebut dari direktori chapter dengan perintah berikut:
go run pointer-vs-value-demo.goOutput yang Dihasilkan:
asli.Level : 1
salin.Level : 2
asli.Level setelah ubahAsli: 2
asli.Level setelah a diubah : 2Mari kita telaah baris demi baris program di atas:
- Baris 11-14 (
ubahSalinan): parameterpadalah salinan structPengaturan; kenaikanLevelhanya mengenai salinan, lalu salinan tersebut dikembalikan sebagai nilai baru. - Baris 17-19 (
ubahAsli): parameterpbertipe*Pengaturan; penulisanp.Levelmengubah field pada struct asli milik pemanggil. - Baris 24-26 (
salinan := ubahSalinan(asli)): pemanggilanubahSalinan(asli)mengirim salinan, sehinggaasli.Leveltetap1dan hanyasalinan.Levelyang menjadi2. - Baris 28-29 (
ubahAsli(&asli)): pemanggilan ini mengirim alamat, sehinggaasli.Levelberubah menjadi2— mutasi terlihat oleh pemanggil. - Baris 32-34 (
a := asli): operator penugasan juga menyalin seluruh struct; penulisana.Level = 99tidak mengubahasliyang tetap bernilai2.
Salinan Dangkal untuk Slice dan Map #
Semantik salinan berlaku pada level terluar struct saja. Ketika struct berisi field bertipe slice, map, atau pointer, salinan yang dihasilkan bersifat dangkal (shallow copy): kedua struct memiliki field sendiri, tetapi field slice pada kedua salinan masih menunjuk backing array yang sama. Perubahan elemen melalui salah satu salinan karenanya terlihat oleh salinan lainnya. Mekanisme header slice dan backing array ini telah kita bahas tuntas pada chapter Backing Array .
Kapan Memilih Pointer atau Nilai #
Pilihan pointer atau nilai sebaiknya ditentukan oleh semantik data, bukan kebiasaan. Tabel berikut merangkum pertimbangan utamanya:
| Aspek | Nilai (value) | Pointer |
|---|---|---|
| Penugasan dan parameter | menyalin seluruh data | menyalin satu alamat |
| Mutasi oleh fungsi | tidak terlihat pemanggil | langsung mengubah data pemanggil |
| Keamanan concurrency | tanpa data yang dibagikan | data dibagikan, perlu sinkronisasi |
| Kondisi ideal | struct kecil, data tidak berubah | struct besar, data harus bisa diubah |
Pointer layak dipilih ketika struct berukuran besar sehingga penyalinan berulang mahal, ketika fungsi atau method memang harus memodifikasi data milik pemanggil, atau ketika satu objek harus dikenali sebagai instance yang sama di banyak tempat. Nilai lebih tepat ketika struct berukuran kecil sehingga biaya salinan dapat diabaikan, ketika data bersifat tetap atau memang dirancang tidak berubah, serta ketika kita ingin tiap pemanggil bekerja pada datanya sendiri tanpa risiko data race. Nilai juga menghapus seluruh kategori galat pointer nil karena salinan selalu berisi data yang valid.
Referensi #
- Effective Go — panduan resmi gaya idiomatik, termasuk pemilihan bentuk data.
- Spesifikasi bahasa Go — aturan resmi penugasan dan penyalinan nilai.
Dengan panduan ini, kita siap memakai pointer pada tempat yang paling sering membutuhkannya: parameter fungsi. Chapter berikutnya mendemonstrasikan bagaimana sebuah fungsi menaikkan nilai variabel milik pemanggilnya hanya dengan menerima alamat variabel tersebut.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
pointer-vs-value-demo.go
Unduh berkaspackage main
import "fmt"
type Pengaturan struct {
Nama string
Level int
}
// ubahSalinan menerima salinan struct dan mengembalikan salinan yang diubah.
func ubahSalinan(p Pengaturan) Pengaturan {
p.Level = p.Level + 1
return p
}
// ubahAsli menerima pointer dan mengubah struct milik pemanggil.
func ubahAsli(p *Pengaturan) {
p.Level = p.Level + 1
}
func main() {
asli := Pengaturan{Nama: "Budi", Level: 1}
salinan := ubahSalinan(asli)
fmt.Println("asli.Level :", asli.Level) // tetap 1
fmt.Println("salin.Level :", salinan.Level) // menjadi 2
ubahAsli(&asli)
fmt.Println("asli.Level setelah ubahAsli:", asli.Level) // menjadi 2
// operator penugasan juga menyalin seluruh struct
a := asli
a.Level = 99
fmt.Println("asli.Level setelah a diubah :", asli.Level) // tetap 2
}