Peta dalam Praktik

Keacakan urutan iterasi, penghapusan entri, dan pola set.

Daftar isi

Peta menyimpan entrinya secara internal dengan urutan yang tidak ditetapkan, dan runtime Go bahkan mengacak urutan itu antar eksekusi. Topik ini membahas konsekuensi keacakan tersebut pada perulangan beserta pola yang benar ketika urutan cetak penting, penghapusan entri dengan delete, dan penerapan peta sebagai set, yaitu kumpulan keanggotaan yang unik. Ketiganya melengkapi pemakaian peta sehari-hari.

Urutan iterasi yang acak#

Perulangan range atas peta mengunjungi entri dalam urutan yang tidak dijamin spesifikasi, dan runtime Go sengaja mengacaknya pada setiap eksekusi. Pengacakan ini disengaja agar program tidak tergoda bergantung pada urutan yang tidak pernah dijamin. Alasannya terletak pada implementasi, yaitu urutan internal bergantung pada algoritma hash, posisi awal kunjungan yang ditetapkan secara acak, beserta susunan memori yang tidak pernah dijanjikan. Konsekuensi praktisnya satu, yaitu perulangan peta hanya boleh dipakai ketika urutan kunjungan tidak berpengaruh pada hasil. Program yang sama dapat mencetak entri dengan urutan berbeda pada dua eksekusi berurutan:

go Perulangan peta mentah yang urutannya berubah-ubah
// tidak-dijalankan
for nama, jumlah := range frekuensi {
	fmt.Println(nama, jumlah) // urutan cetak berbeda pada setiap eksekusi
}

Menampilkan keluaran perulangan peta mentah sebagai contoh memang tidak bermakna karena keluarannya tidak dapat direproduksi. Bentuk for nama := range frekuensi dengan satu variabel mengambil kuncinya saja, tepat untuk keperluan pengumpulan kunci, sedangkan bentuk for range tanpa variabel cukup ketika isinya tidak diperlukan. Ketika urutan cetak penting, misalnya untuk laporan, pola yang benar adalah mengumpulkan kunci ke sebuah slice, mengurutkannya dengan sort.Strings, lalu menjelajah slice yang sudah terurut:

go terurut.go
package main

import (
	"fmt"
	"sort"
)

func main() {
	frekuensi := map[string]int{
		"Ali":   3,
		"Budi":  1,
		"Citra": 2,
	}

	kunci := make([]string, 0, len(frekuensi))
	for nama := range frekuensi {
		kunci = append(kunci, nama)
	}
	sort.Strings(kunci)

	for _, nama := range kunci {
		fmt.Println(nama, ":", frekuensi[nama])
	}
}
text Keluaran
Ali : 3
Budi : 1
Citra : 2

Biaya pengurutan sebesar O(nlog⁡n)O(n \log n) hanya dibayar ketika urutan memang dibutuhkan, sedangkan penelusuran tanpa urutan tetap lebih murah. Peta yang kuncinya bertipe bilangan dapat diurutkan dengan sort.Ints mengikuti pola yang sama.

Program dijalankan dari folder kerja tetap:

bash
# siapkan folder kerja untuk topik ini
mkdir -p /tmp/gotipe/peta-praktik
cp terurut.go hapus.go set-piket.go /tmp/gotipe/peta-praktik/
cd /tmp/gotipe/peta-praktik

# periksa format lalu jalankan programnya
gofmt -l .
go run terurut.go
text Keluaran
Ali : 3
Budi : 1
Citra : 2

Menghapus entri#

Fungsi delete(m, k) menghapus entri dengan kunci k dari peta m. Fungsi ini satu-satunya cara menghapus entri, karena penugasan dengan nilai nol hanya mengosongkan nilai dan tetap meninggalkan kuncinya. Pemanggilan pada kunci yang tidak ada tidak menimbulkan error, sedangkan len menyusut setiap kali entri benar-benar terhapus:

go hapus.go
package main

import "fmt"

func main() {
	stok := map[string]int{
		"kertas-a4":   120,
		"tinta-hitam": 45,
		"penghapus":   80,
	}

	delete(stok, "tinta-hitam")
	delete(stok, "tidak-ada")

	fmt.Println("Isi peta :", stok)
	fmt.Println("Banyaknya:", len(stok))
}
text Keluaran
Isi peta : map[kertas-a4:120 penghapus:80]
Banyaknya: 2

delete aman dipanggil selama perulangan range atas peta yang sama berlangsung, baik untuk entri yang sedang dikunjungi maupun yang sudah dilewati, sedangkan entri yang belum tercapai cukup dilewati tanpa dikunjungi. Berbeda dengan penghapusan, penambahan entri baru di tengah perulangan membuat kunjungannya tidak dapat diprediksi, sehingga penambahan sebaiknya ditunda hingga perulangan selesai. Penghapusan massal mengikuti pola yang sama, yaitu menghapus kunci saat perulangan berlangsung tanpa menyimpan kumpulan kunci terlebih dahulu. Apabila seluruh isi harus dibuang sekaligus, cukup tetapkan ulang variabelnya dengan make baru, karena tidak ada fungsi khusus untuk mengosongkan peta.

Pola set#

Set adalah kumpulan anggota yang unik, dan Go tidak menyediakan tipenya secara bawaan. Peta menjadi penggantinya yang lazim, yaitu map[string]bool dengan kunci sebagai anggota dan nilai sebagai penanda keanggotaan. Pendaftaran pada kunci yang sama dapat diulang tanpa mengubah isi set, sehingga pemasukan anggota tidak perlu pemeriksaan lebih dahulu:

go set-piket.go
package main

import "fmt"

func main() {
	hadir := map[string]bool{}

	for _, nama := range []string{"Ali", "Citra", "Ali"} {
		hadir[nama] = true
	}

	fmt.Println("Ali hadir  :", hadir["Ali"])
	fmt.Println("Budi hadir :", hadir["Budi"])

	hadir["Budi"] = true
	hadir["Ali"] = false
	fmt.Println("Set terkini:", hadir)
}
text Keluaran
Ali hadir  : true
Budi hadir : false
Set terkini: map[Ali:false Budi:true Citra:true]

Pembacaan kunci yang tidak ada menghasilkan false, sehingga pemeriksaan keanggotaan ditulis dalam satu ekspresi tanpa comma-ok. Pemeriksaan set kosong memakai len(hadir) == 0, dan banyaknya anggota dihitung dengan len tanpa perlu perulangan. Set beranggota tipe lain mengikuti pola yang sama, misalnya map[int]bool untuk nomor antrean yang sudah dipanggil.

Menggabungkan dua set dilakukan dengan perulangan atas set sumber beserta penugasan ke set tujuan, sedangkan irisan dan selisih dihitung lewat pemeriksaan keanggotaan pada set lainnya. Pola map[string]bool menyimpan satu nilai logika untuk setiap anggota. Untuk set yang besar dan hanya menampung keanggotaan, map[string]struct{} lebih hemat karena tidak menyimpan nilai sama sekali. Perlu dicatat, pemeriksaan keanggotaannya memakai comma-ok karena pembacaannya menghasilkan struct kosong, bukan nilai bool.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul ketika peta mulai masuk ke program yang lebih besar.

  1. Mengandalkan urutan iterasi. Program yang membandingkan catatan (log) antar eksekusi atau yang menyusun laporan langsung dari range peta menghasilkan keluaran yang berubah-ubah. Perbaikannya, kumpulkan kunci, urutkan dengan sort.Strings, lalu jelajah slice yang terurut.
  2. Menambah entri baru saat perulangan. Entri yang ditambahkan di tengah range mungkin dikunjungi dan mungkin tidak, sehingga hasilnya tidak dapat diprediksi. Perbaikannya, kumpulkan data yang mau ditambahkan terlebih dahulu, lalu tugas semuanya setelah perulangan selesai.
  3. Memperlakukan map[string]struct{} seperti map[string]bool. Pembacaan langsung pada set jenis ini menghasilkan struct kosong, sehingga ekspresi if set[k] ditolak kompiler. Perbaikannya, gunakan if _, ok := set[k]; ok untuk memeriksa keanggotaannya.

Ringkasan#

  • Urutan range atas peta tidak dijamin spesifikasi dan sengaja diacak runtime antar eksekusi.
  • Kunci dikumpulkan ke slice, diurutkan dengan sort.Strings, lalu dijelajah bila urutan penting.
  • delete menghapus entri tanpa error pada kunci yang tidak ada, dan aman dipanggil selama perulangan berlangsung.
  • Pola set memakai map[string]bool untuk keanggotaan sederhana, atau map[string]struct{} dengan comma-ok bila memori lebih penting.

Referensi#