range pada map Go dapat mengunjungi entri yang sama dalam urutan berbeda pada iterasi berturut-turut. Spesifikasi bahasa menyatakan bahwa urutan iterasi map tidak ditentukan dan tidak menjamin bahwa iterasi berikutnya pada map yang tidak berubah akan mengulangi urutan sebelumnya.
Kontrak ini lebih kuat daripada sekadar mengatakan bahwa map tidak terurut. Container yang tidak terurut masih bisa saja menampilkan urutan stabil yang bergantung pada insertion order atau layout storage. Program Go tidak boleh memberikan makna seperti itu pada traversal map.
Urutan berada di luar kontrak map
Untuk map m, loop berikut menghasilkan setiap entri yang memenuhi aturan bahasa, tetapi urutannya bukan bagian dari perilaku program yang portable:
for k, v := range m {
consume(k, v)
}Kode tetap benar ketika consume tidak bergantung pada urutan. Masalah muncul ketika urutan traversal masuk ke hasil yang dapat diamati: teks terserialisasi, konfigurasi yang dihasilkan, snapshot output, tie-breaking, cache key, atau pemilihan entri pertama yang ditemukan.
Perbedaan ini tetap penting meskipun eksekusi berulang terlihat stabil pada satu mesin. Konsistensi yang teramati tidak menciptakan jaminan dari bahasa.
Runtime secara aktif memvariasikan traversal
Kode map pada runtime Go saat ini secara eksplisit mengacak iterasi dengan memilih offset acak ketika iterator diinisialisasi. Perilaku implementasi ini memperkuat kontrak bahasa, tetapi bukan merupakan kontrak itu sendiri.
Implementasi map pada runtime juga telah banyak berubah sepanjang umur Go. Go modern menggunakan desain berbasis Swiss Table dengan data map yang terbagi ke beberapa table ketika kapasitas bertambah. Layout storage, strategi pertumbuhan, dan detail internal iterator merupakan detail implementasi yang dapat berubah tanpa memberikan urutan traversal stabil kepada program.
Karena itu, kode tidak boleh menyimpulkan urutan dari penempatan bucket, nilai hash, riwayat alokasi, atau versi runtime tertentu.
Mutasi selama iterasi memiliki semantik tersendiri
Urutan yang tidak ditentukan bukan berarti seluruh aspek iterasi map tidak ditentukan. Bahasa mendefinisikan perilaku untuk entri yang ditambahkan atau dihapus ketika loop sedang berjalan.
Jika entri yang belum dicapai dihapus, entri tersebut tidak akan dihasilkan. Jika entri baru dimasukkan selama iterasi, entri itu dapat dihasilkan atau dilewati. Keputusannya dapat berbeda untuk setiap entri dan setiap iterasi.
Aturan ini memungkinkan pola mutasi di dalam loop yang berguna tanpa menetapkan urutan. Aturan tersebut juga berarti traversal pada map yang sedang berubah tidak dapat dianggap sebagai snapshot dari kumpulan key awal maupun akhir.
Mutasi concurrent merupakan batas yang berbeda. Map biasa tidak mendukung read dan write concurrent tanpa sinkronisasi. Iterasi yang berjalan bersamaan dengan writer memerlukan sinkronisasi pada map yang dibagi tersebut.
Output stabil memerlukan lapisan pengurutan eksplisit
Ketika urutan output merupakan bagian dari antarmuka, operasi pengurutan harus berada di luar map. Representasi yang umum adalah slice key yang diurutkan:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
emit(k, m[k])
}Sorting membuat urutan yang dimaksud terlihat jelas di dalam kode dan tidak bergantung pada detail internal map. Untuk tipe key yang dapat diurutkan, slices.Sorted(maps.Keys(m)) menyediakan batas yang sama dengan API iterator pada rilis Go terbaru.
Pekerjaan tambahan ini memiliki biaya. Mengumpulkan key membutuhkan ruang yang sebanding dengan jumlah entri, sedangkan comparison sorting membutuhkan O(n log n). Biaya tersebut adalah konsekuensi dari menerapkan urutan yang memang tidak disediakan oleh hash map.
Pengujian memperlihatkan ketergantungan urutan yang tidak disengaja
Pengujian sering menjadi tempat pertama batas ini terlihat. Sebuah fungsi dapat benar secara logis sementara golden file atau exact string assertion gagal karena data berbasis map dikeluarkan secara langsung.
Perbaikan yang tahan lama ditentukan oleh kontrak antarmuka. Jika urutan memiliki makna semantik, kode produksi harus menetapkannya sebelum menghasilkan output. Jika urutan tidak relevan, pengujian seharusnya membandingkan data tanpa memberikan makna pada sequence.
Mengandalkan satu urutan traversal yang teramati membuat pengujian sensitif terhadap detail runtime, state proses, dan perubahan implementasi. Menjalankan ulang loop bukan mekanisme normalisasi yang valid karena iterasi berikutnya memiliki kontrak urutan yang sama-sama tidak ditentukan.
Memilih entri pertama adalah kebijakan nondeterministik
Memilih key pertama yang ditemukan oleh range memang ringkas, tetapi tidak mendefinisikan aturan pemilihan yang portable:
for k := range candidates {
return k
}Hasilnya dapat berubah meskipun candidates berisi entri yang sama. Jika key terpilih memengaruhi routing, pemilihan leader, shard assignment, fallback behavior, atau persistent state, iterasi map diam-diam telah berubah menjadi kebijakan.
Kebijakan deterministik memerlukan kriteria eksplisit seperti urutan leksikal, prioritas numerik, timestamp, hash ranking dengan algoritma tetap, atau sequence terpisah yang dipelihara secara eksplisit. Map tetap dapat menyediakan lookup yang efisien sementara struktur lain membawa semantik urutan.
Map Go menyediakan akses berbasis key tanpa janji urutan iterasi. Memperlakukan urutan traversal sebagai sesuatu yang tidak bermakna menjaga kebebasan implementasi runtime tetap terpisah dari determinisme yang terlihat oleh aplikasi.