unisbadri.com » Python Java Golang Typescript Kotlin Ruby Rust Dart PHP

Map #

Map di Go adalah implementasi hash table — struktur data yang memberikan akses O(1) rata-rata untuk operasi get, set, dan delete. Berbeda dari slice yang menyimpan elemen berurutan dengan index numerik, map menyimpan pasangan key-value di mana key bisa berupa hampir semua tipe comparable. Map sangat berguna untuk lookup cepat, pengelompokan data, dan counter — tapi ada beberapa perilaku yang perlu dipahami: iterasinya tidak deterministik, tidak thread-safe, dan ada gotcha penting saat memodifikasi struct yang tersimpan di dalam map.

Cara Membuat Map #

make — Cara yang Direkomendasikan #

// make menghasilkan map yang siap digunakan
m := make(map[string]int)
m["satu"] = 1
m["dua"] = 2

// make dengan kapasitas hint — tidak membatasi, hanya optimasi awal
// berguna jika tahu berapa entry yang akan dimasukkan
m2 := make(map[string]int, 100)

Map Literal #

// Inisialisasi dengan nilai sekaligus
harga := map[string]float64{
    "apel":   5000,
    "mangga": 8000,
    "jeruk":  4500,
    "durian": 35000,
}

// Map kosong dengan literal — berbeda dari nil map!
kosong := map[string]int{}

Nil Map — Jangan Langsung Ditulis #

// var menghasilkan nil map
var m map[string]int

fmt.Println(m == nil)  // true
fmt.Println(len(m))    // 0 — nil map punya len 0

// MEMBACA dari nil map aman — mengembalikan zero value
v := m["kunci"]
fmt.Println(v)  // 0 — tidak panic

// MENULIS ke nil map → PANIC!
m["kunci"] = 1  // panic: assignment to entry in nil map

// Selalu inisialisasi sebelum menulis
m = make(map[string]int)
m["kunci"] = 1  // ✓ aman
Menulis ke nil map menyebabkan panic. Berbeda dari nil slice yang aman untuk di-append, nil map akan crash saat kamu mencoba menulis ke dalamnya. Selalu gunakan make() atau literal {} sebelum operasi write. Ini adalah salah satu runtime panic paling umum di kode Go pemula.

Operasi CRUD #

m := map[string]int{
    "apel":  5000,
    "mangga": 8000,
}

// CREATE / UPDATE — sintaks sama
m["jeruk"] = 4500       // tambah key baru
m["apel"] = 6000        // update key yang ada

// READ
hargaApel := m["apel"]  // 6000
hargaTeh := m["teh"]    // 0 — key tidak ada, kembalikan zero value (bukan error/panic)

// DELETE
delete(m, "mangga")     // hapus key "mangga"
delete(m, "tidakada")   // aman, tidak panic meski key tidak ada

// LENGTH
fmt.Println(len(m))     // jumlah entry saat ini

Two-Value Form — Wajib untuk Membedakan “Tidak Ada” vs Zero Value #

Ini adalah pola yang sangat penting dan wajib dipahami. Membaca map yang keynya tidak ada mengembalikan zero value dari tipe value-nya — sehingga tidak bisa dibedakan dari key yang memang ada dengan nilai zero:

stok := map[string]int{
    "apel":    50,
    "mangga":  0,   // stok 0, beda dengan "tidak terdaftar"!
}

// ANTI-PATTERN: tidak bisa membedakan "tidak ada" vs "stok 0"
s := stok["jeruk"]
if s == 0 {
    // Ini ambigu: apakah jeruk tidak terdaftar, atau stoknya memang 0?
}

// BENAR: selalu gunakan two-value form
s, ok := stok["mangga"]
fmt.Println(s, ok)  // 0 true — mangga ADA, stoknya memang 0

s, ok = stok["jeruk"]
fmt.Println(s, ok)  // 0 false — jeruk TIDAK ADA

s, ok = stok["apel"]
fmt.Println(s, ok)  // 50 true — apel ada dengan stok 50

// Pola idiomatik untuk cek dan akses
if harga, ok := catalog["laptop"]; ok {
    fmt.Printf("Harga laptop: Rp%.0f\n", harga)
} else {
    fmt.Println("Laptop tidak ada dalam katalog")
}

Iterasi — Non-Deterministik #

Iterasi map dengan for range mengembalikan semua key-value, tapi urutan tidak dijamin dan berbeda setiap kali program dijalankan. Ini adalah keputusan desain Go yang disengaja untuk mencegah developer bergantung pada urutan tertentu.

m := map[string]int{"charlie": 3, "alice": 1, "bob": 2}

// Urutan bisa berbeda setiap run
for k, v := range m {
    fmt.Printf("%s: %d\n", k, v)
}

// Hanya iterasi key
for k := range m {
    fmt.Println(k)
}

// Hanya value (jarang dipakai karena kehilangan konteks key)
for _, v := range m {
    fmt.Println(v)
}

Iterasi dengan Urutan Konsisten #

import "sort"

m := map[string]int{"charlie": 3, "alice": 1, "bob": 2}

// Kumpulkan semua key, sort, iterasi
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
    fmt.Printf("%s: %d\n", k, m[k])
}
// Output selalu konsisten: alice:1, bob:2, charlie:3

Map adalah Reference Type #

Map adalah reference type — ketika dioper ke fungsi atau diassign ke variabel lain, keduanya menunjuk ke underlying hash table yang sama:

func tambahEntry(m map[string]int, key string, val int) {
    m[key] = val  // memodifikasi map ASLI, bukan salinan
}

func main() {
    data := map[string]int{"a": 1}
    tambahEntry(data, "b", 2)
    fmt.Println(data)  // map[a:1 b:2] — berubah!
}

// Assignment pun berbagi referensi yang sama
original := map[string]int{"x": 10}
alias := original
alias["y"] = 20
fmt.Println(original)  // map[x:10 y:20] — original juga berubah!

Ini berbeda dari slice yang punya shared backing array — tapi konsepnya serupa. Untuk membuat salinan yang benar-benar independen:

// Salin map secara manual
func copyMap(src map[string]int) map[string]int {
    dst := make(map[string]int, len(src))
    for k, v := range src {
        dst[k] = v
    }
    return dst
}

Key Constraints — Tipe yang Bisa Jadi Key #

Key map harus bertipe comparable — bisa dibandingkan dengan ==. Ini termasuk: semua tipe dasar (int, string, bool, float), pointer, array, dan struct yang semua field-nya comparable. Slice, map, dan function tidak bisa jadi key:

// ✓ Key yang valid
m1 := map[string]int{}
m2 := map[int]string{}
m3 := map[bool]int{}
m4 := map[[3]int]string{}   // array bisa jadi key

type Point struct{ X, Y int }
m5 := map[Point]string{}    // struct comparable bisa jadi key

// ✗ Key yang tidak valid — compile error
// m6 := map[[]int]string{}  // slice tidak comparable
// m7 := map[map[string]int]string{}  // map tidak comparable

// Penggunaan struct sebagai key
grid := map[Point]string{
    {0, 0}: "start",
    {10, 5}: "waypoint",
    {20, 0}: "end",
}
fmt.Println(grid[Point{10, 5}])  // "waypoint"

Map sebagai Set #

Go tidak punya tipe Set bawaan. Cara idiomatik adalah menggunakan map[T]struct{}struct{} adalah tipe tanpa field yang tidak memakan memori (zero size):

// Set of strings
seen := make(map[string]struct{})

words := []string{"go", "python", "go", "rust", "python", "go"}
var unique []string

for _, w := range words {
    if _, ok := seen[w]; !ok {
        seen[w] = struct{}{}     // tandai sudah dilihat
        unique = append(unique, w)
    }
}
fmt.Println(unique)  // [go python rust]

// Helper functions untuk set operations
func contains(set map[string]struct{}, item string) bool {
    _, ok := set[item]
    return ok
}

func addToSet(set map[string]struct{}, item string) {
    set[item] = struct{}{}
}

func removeFromSet(set map[string]struct{}, item string) {
    delete(set, item)
}

// Intersection — elemen yang ada di kedua set
func intersection(a, b map[string]struct{}) map[string]struct{} {
    result := make(map[string]struct{})
    for k := range a {
        if _, ok := b[k]; ok {
            result[k] = struct{}{}
        }
    }
    return result
}

// Union — gabungan semua elemen
func union(a, b map[string]struct{}) map[string]struct{} {
    result := make(map[string]struct{}, len(a)+len(b))
    for k := range a { result[k] = struct{}{} }
    for k := range b { result[k] = struct{}{} }
    return result
}

Nested Map dan Gotcha Modifikasi Struct #

Map bisa menyimpan nilai bertipe map lain atau slice:

// Map of slices — kelompokkan data
byCategory := make(map[string][]string)
byCategory["buah"] = append(byCategory["buah"], "apel")
byCategory["buah"] = append(byCategory["buah"], "mangga")
byCategory["sayur"] = append(byCategory["sayur"], "bayam")

// Map of maps — nested
config := map[string]map[string]string{
    "database": {
        "host": "localhost",
        "port": "5432",
        "name": "myapp",
    },
    "cache": {
        "host": "localhost",
        "port": "6379",
    },
}
fmt.Println(config["database"]["host"])  // localhost

Gotcha: Tidak Bisa Memodifikasi Field Struct dalam Map Langsung #

Ini adalah limitasi yang sering mengejutkan:

type Counter struct {
    Count int
    Name  string
}

counters := map[string]Counter{
    "hits": {Count: 0, Name: "Page Hits"},
}

// ANTI-PATTERN: compile error!
// counters["hits"].Count++
// ← cannot assign to struct field in map

// Solusi 1: ambil, modifikasi, simpan kembali
c := counters["hits"]
c.Count++
counters["hits"] = c

// Solusi 2: simpan pointer ke struct
pCounters := map[string]*Counter{
    "hits": {Count: 0, Name: "Page Hits"},
}
pCounters["hits"].Count++  // ✓ pointer receiver bisa dimodifikasi langsung

Hapus Sambil Iterasi — Aman di Go #

Berbeda dari beberapa bahasa lain, di Go kamu bisa menghapus entry map selama iterasi for range — aman dan tidak menyebabkan undefined behavior:

m := map[string]int{
    "a": 1, "b": -2, "c": 3, "d": -4, "e": 5,
}

// Hapus semua entry dengan nilai negatif
for k, v := range m {
    if v < 0 {
        delete(m, k)  // ✓ aman dilakukan selama range
    }
}
fmt.Println(m)  // map[a:1 c:3 e:5]

Package maps (Go 1.21+) #

Sejak Go 1.21, package maps di standard library menyediakan fungsi utilitas yang berguna:

import "maps"

m := map[string]int{"a": 1, "b": 2, "c": 3}

// Clone — salinan independen
clone := maps.Clone(m)
clone["d"] = 4
fmt.Println(m)     // map[a:1 b:2 c:3] — tidak berubah
fmt.Println(clone) // map[a:1 b:2 c:3 d:4]

// Keys dan Values (sebagai iterator, Go 1.23+)
// Atau collect ke slice:
keys := make([]string, 0, len(m))
for k := range m { keys = append(keys, k) }

// Equal — bandingkan dua map
m2 := map[string]int{"a": 1, "b": 2, "c": 3}
fmt.Println(maps.Equal(m, m2))  // true

// DeleteFunc — hapus entry yang memenuhi kondisi
maps.DeleteFunc(m, func(k string, v int) bool {
    return v > 2  // hapus entry dengan value > 2
})
fmt.Println(m)  // map[a:1 b:2]

Concurrency — Map Tidak Thread-Safe #

Map Go tidak aman untuk akses concurrent dari beberapa goroutine. Membaca dan menulis map dari goroutine berbeda secara bersamaan adalah race condition yang menyebabkan panic di runtime:

// ANTI-PATTERN: race condition!
var counter = make(map[string]int)

func incrementUnsafe(key string) {
    counter[key]++  // ← tidak aman jika dipanggil dari banyak goroutine!
}

// Solusi 1: sync.RWMutex — fleksibel, read banyak tulis satu
type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func NewSafeMap() *SafeMap {
    return &SafeMap{m: make(map[string]int)}
}

func (sm *SafeMap) Set(key string, val int) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[key] = val
}

func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    v, ok := sm.m[key]
    return v, ok
}

func (sm *SafeMap) Increment(key string) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[key]++
}

// Solusi 2: sync.Map — optimized untuk read-heavy atau key yang jarang berubah
var safeCounter sync.Map

safeCounter.Store("hits", 0)

val, _ := safeCounter.Load("hits")
fmt.Println(val)

safeCounter.Store("hits", val.(int)+1)

// LoadOrStore — load jika ada, store jika belum
actual, loaded := safeCounter.LoadOrStore("new_key", 42)
fmt.Println(actual, loaded)  // 42, false (key baru)

// Range — iterasi sync.Map
safeCounter.Range(func(key, value any) bool {
    fmt.Printf("%v: %v\n", key, value)
    return true  // return false untuk stop iterasi
})

Pola Idiomatik #

Frequency Counter #

teks := "the quick brown fox jumps over the lazy dog the fox"
words := strings.Fields(teks)

freq := make(map[string]int)
for _, w := range words {
    freq[w]++  // increment — zero value int adalah 0, jadi langsung bisa ++
}

// Tampilkan urutan frekuensi
type WordFreq struct {
    Word  string
    Count int
}
var list []WordFreq
for w, c := range freq {
    list = append(list, WordFreq{w, c})
}
sort.Slice(list, func(i, j int) bool {
    if list[i].Count != list[j].Count {
        return list[i].Count > list[j].Count  // descending by count
    }
    return list[i].Word < list[j].Word  // ascending by word jika count sama
})

for _, wf := range list[:3] {
    fmt.Printf("%-10s: %d\n", wf.Word, wf.Count)
}

Grouping Data #

type Order struct {
    ID       int
    Customer string
    Amount   float64
    Status   string
}

orders := []Order{
    {1, "Budi",  150000, "paid"},
    {2, "Sari",  250000, "pending"},
    {3, "Budi",   75000, "paid"},
    {4, "Ahmad",  300000, "pending"},
    {5, "Sari",  100000, "paid"},
}

// Group by customer
byCustomer := make(map[string][]Order)
for _, o := range orders {
    byCustomer[o.Customer] = append(byCustomer[o.Customer], o)
}

// Hitung total per customer
for customer, customerOrders := range byCustomer {
    total := 0.0
    for _, o := range customerOrders {
        total += o.Amount
    }
    fmt.Printf("%s: %d order, total Rp%.0f\n",
        customer, len(customerOrders), total)
}

In-Memory Cache #

type Cache struct {
    mu    sync.RWMutex
    store map[string]cacheEntry
}

type cacheEntry struct {
    value    interface{}
    expireAt time.Time
}

func NewCache() *Cache {
    return &Cache{store: make(map[string]cacheEntry)}
}

func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.store[key] = cacheEntry{
        value:    value,
        expireAt: time.Now().Add(ttl),
    }
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    entry, ok := c.store[key]
    if !ok || time.Now().After(entry.expireAt) {
        return nil, false
    }
    return entry.value, true
}

func (c *Cache) Cleanup() {
    c.mu.Lock()
    defer c.mu.Unlock()
    now := time.Now()
    for k, v := range c.store {
        if now.After(v.expireAt) {
            delete(c.store, k)  // hapus yang expired
        }
    }
}

Contoh Program Lengkap #

Program berikut menganalisis teks dan menghasilkan berbagai statistik menggunakan map:

package main

import (
    "fmt"
    "sort"
    "strings"
    "unicode"
)

type TextAnalyzer struct {
    wordFreq    map[string]int
    charFreq    map[rune]int
    bigramFreq  map[string]int
    sentences   int
    totalWords  int
    totalChars  int
}

func NewTextAnalyzer() *TextAnalyzer {
    return &TextAnalyzer{
        wordFreq:   make(map[string]int),
        charFreq:   make(map[rune]int),
        bigramFreq: make(map[string]int),
    }
}

func (ta *TextAnalyzer) Analyze(text string) {
    // Hitung karakter
    for _, r := range text {
        if !unicode.IsSpace(r) {
            ta.charFreq[unicode.ToLower(r)]++
            ta.totalChars++
        }
    }

    // Hitung kata
    words := strings.FieldsFunc(text, func(r rune) bool {
        return !unicode.IsLetter(r) && !unicode.IsDigit(r)
    })

    cleanWords := make([]string, 0, len(words))
    for _, w := range words {
        if len(w) > 0 {
            lower := strings.ToLower(w)
            ta.wordFreq[lower]++
            ta.totalWords++
            cleanWords = append(cleanWords, lower)
        }
    }

    // Hitung bigram (pasangan kata berurutan)
    for i := 0; i < len(cleanWords)-1; i++ {
        bigram := cleanWords[i] + " " + cleanWords[i+1]
        ta.bigramFreq[bigram]++
    }

    // Hitung kalimat (kasar)
    for _, r := range text {
        if r == '.' || r == '!' || r == '?' {
            ta.sentences++
        }
    }
}

// Top N entries dari map berdasarkan frekuensi
func topN(freq map[string]int, n int) []struct{ Key string; Count int } {
    type entry struct {
        Key   string
        Count int
    }
    entries := make([]entry, 0, len(freq))
    for k, v := range freq {
        entries = append(entries, entry{k, v})
    }
    sort.Slice(entries, func(i, j int) bool {
        if entries[i].Count != entries[j].Count {
            return entries[i].Count > entries[j].Count
        }
        return entries[i].Key < entries[j].Key
    })

    if n > len(entries) {
        n = len(entries)
    }
    result := make([]struct{ Key string; Count int }, n)
    for i := 0; i < n; i++ {
        result[i] = struct{ Key string; Count int }{entries[i].Key, entries[i].Count}
    }
    return result
}

func (ta *TextAnalyzer) Report() {
    fmt.Println("=== Laporan Analisis Teks ===\n")

    fmt.Printf("Statistik Dasar:\n")
    fmt.Printf("  Total karakter (non-spasi): %d\n", ta.totalChars)
    fmt.Printf("  Total kata                : %d\n", ta.totalWords)
    fmt.Printf("  Total kalimat (estimasi)  : %d\n", ta.sentences)
    fmt.Printf("  Kosakata unik             : %d\n", len(ta.wordFreq))
    if ta.sentences > 0 {
        fmt.Printf("  Rata-rata kata/kalimat    : %.1f\n",
            float64(ta.totalWords)/float64(ta.sentences))
    }

    fmt.Printf("\n10 Kata Paling Sering:\n")
    for i, e := range topN(ta.wordFreq, 10) {
        bar := strings.Repeat("█", e.Count)
        fmt.Printf("  %2d. %-15s %2d  %s\n", i+1, e.Key, e.Count, bar)
    }

    fmt.Printf("\n5 Bigram Paling Sering:\n")
    for i, e := range topN(ta.bigramFreq, 5) {
        fmt.Printf("  %d. %-25s %d kali\n", i+1, e.Key, e.Count)
    }

    fmt.Printf("\n10 Karakter Paling Sering:\n")
    charEntries := make(map[string]int)
    for r, c := range ta.charFreq {
        if unicode.IsLetter(r) {
            charEntries[string(r)] = c
        }
    }
    for i, e := range topN(charEntries, 10) {
        fmt.Printf("  %2d. '%s': %d\n", i+1, e.Key, e.Count)
    }

    // Distribusi panjang kata
    lengthDist := make(map[int]int)
    for w, c := range ta.wordFreq {
        lengthDist[len([]rune(w))] += c
    }
    fmt.Printf("\nDistribusi Panjang Kata:\n")
    lengths := make([]int, 0)
    for l := range lengthDist {
        lengths = append(lengths, l)
    }
    sort.Ints(lengths)
    for _, l := range lengths {
        if l <= 10 {
            pct := float64(lengthDist[l]) / float64(ta.totalWords) * 100
            bar := strings.Repeat("▪", int(pct/2))
            fmt.Printf("  %2d huruf: %4d kata (%4.1f%%) %s\n",
                l, lengthDist[l], pct, bar)
        }
    }
}

func main() {
    teks := `Go adalah bahasa pemrograman yang dikembangkan oleh Google.
Go dirancang untuk efisiensi dan kesederhanaan. Bahasa Go memiliki
sintaks yang bersih dan mudah dipelajari. Go mendukung konkurensi
melalui goroutine dan channel. Banyak perusahaan menggunakan Go
untuk membangun sistem yang skalabel dan efisien. Go adalah pilihan
yang tepat untuk backend, microservice, dan alat command line.
Ekosistem Go terus berkembang dengan banyak library yang tersedia.
Go memiliki compiler yang cepat dan menghasilkan binary tunggal.`

    analyzer := NewTextAnalyzer()
    analyzer.Analyze(teks)
    analyzer.Report()
}

Ringkasan #

  • Nil map aman dibaca (mengembalikan zero value) tapi panic jika ditulis — selalu inisialisasi dengan make() atau {} sebelum write.
  • Two-value form (val, ok := m[key]) adalah satu-satunya cara membedakan “key tidak ada” dari “key ada dengan zero value”.
  • Urutan iterasi tidak dijamin — sort key ke slice terlebih dahulu jika butuh output konsisten.
  • Map adalah reference type — pass ke fungsi atau assign ke variabel lain berbagi underlying hash table yang sama.
  • Key harus comparable — string, int, bool, array, struct comparable bisa jadi key; slice, map, func tidak bisa.
  • map[T]struct{} adalah cara idiomatik Set di Go — struct{} tidak memakan memori.
  • Tidak bisa memodifikasi field struct dalam map langsung — ambil, modifikasi, simpan kembali, atau gunakan pointer sebagai value.
  • Hapus sambil iterasi aman di Go — delete(m, k) selama for range tidak menyebabkan undefined behavior.
  • Map tidak thread-safe — gunakan sync.RWMutex untuk read-heavy atau sync.Map untuk concurrent access.
  • maps.Clone, maps.Equal, maps.DeleteFunc tersedia sejak Go 1.21 untuk operasi map yang umum.

← Sebelumnya: Slice   Berikutnya: Date & Time →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact