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:

go
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:

bash
go run pointer-vs-value-demo.go

Output yang Dihasilkan:

text
asli.Level   : 1
salin.Level  : 2
asli.Level setelah ubahAsli: 2
asli.Level setelah a diubah : 2

Mari kita telaah baris demi baris program di atas:

  1. Baris 11-14 (ubahSalinan): parameter p adalah salinan struct Pengaturan; kenaikan Level hanya mengenai salinan, lalu salinan tersebut dikembalikan sebagai nilai baru.
  2. Baris 17-19 (ubahAsli): parameter p bertipe *Pengaturan; penulisan p.Level mengubah field pada struct asli milik pemanggil.
  3. Baris 24-26 (salinan := ubahSalinan(asli)): pemanggilan ubahSalinan(asli) mengirim salinan, sehingga asli.Level tetap 1 dan hanya salinan.Level yang menjadi 2.
  4. Baris 28-29 (ubahAsli(&asli)): pemanggilan ini mengirim alamat, sehingga asli.Level berubah menjadi 2 — mutasi terlihat oleh pemanggil.
  5. Baris 32-34 (a := asli): operator penugasan juga menyalin seluruh struct; penulisan a.Level = 99 tidak mengubah asli yang tetap bernilai 2.

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:

AspekNilai (value)Pointer
Penugasan dan parametermenyalin seluruh datamenyalin satu alamat
Mutasi oleh fungsitidak terlihat pemanggillangsung mengubah data pemanggil
Keamanan concurrencytanpa data yang dibagikandata dibagikan, perlu sinkronisasi
Kondisi idealstruct kecil, data tidak berubahstruct 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 #

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 berkas
go
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
}