go
Go dersleri
Go/Temeller

Haritalar (Maps)

Anahtar-değer yapıları, virgül-ok deyimi, kümeler ve maps paketi.

Ders 6 / 2120 dkBaşlangıç
Bu derste öğreneceklerin
  • Map oluşturma: make ve literal
  • Okuma, yazma, silme ve virgül-ok (comma ok)
  • Rastgele gezinme sırası
  • nil map tuzağı
  • map[T]struct{} ile küme (set)
  • Değer olarak struct ve slice tutma
  • maps paketi ve clear
  • Eşzamanlı erişim uyarısı

Bir sözlükte "gönül" kelimesinin anlamını ararken ilk sayfadan başlayıp her kelimeyi tek tek okumazsın. Kelimeyi bildiğin için doğrudan ilgili sayfaya gidersin. Programlarda da sık sık aynı ihtiyaç doğar: Bir kullanıcı adına karşılık gelen hesabı, bir ürün koduna karşılık gelen fiyatı veya bir kelimenin metinde kaç kez geçtiğini hızlıca bulmak isteriz. Bu bilgileri bir dilimde tutarsan her aramada baştan sona tarama yapman gerekir, bu da O(n) demektir. Harita (map) ise anahtarı verdiğinde değere ortalama O(1) sürede ulaşır.

Go'da map dilin yerleşik bir parçasıdır: Ayrı bir paket ya da kütüphane gerekmez, kendine özgü bir sözdizimi vardır ve çok yaygın kullanılır. Önbellekler, sayaçlar, kümeler, indeksler, yapılandırma tabloları... Hepsinin arkasında çoğu zaman bir map durur. Ama map'lerin de dilimler gibi bilinmesi gereken birkaç tuzağı vardır: nil map'e yazmak programı çökertir, gezinme sırası her çalıştırmada değişir ve eşzamanlı yazma programı anında sonlandırır.

Bu dersin sonunda map oluşturmayı, okuma, yazma ve silme işlemlerini, eksik anahtarı virgül-ok deyimiyle ayırt etmeyi, map'i sıralı ve deterministik biçimde gezmeyi bileceksin. Ayrıca map[T]struct{} ile küme kuracak, değer olarak dilim ve struct saklarken karşılaşılan derleme hatasını çözecek, maps paketini ve clear fonksiyonunu kullanacak ve map'lerin eşzamanlı erişimde neden tehlikeli olduğunu öğreneceksin.

Map nedir?

Map, benzersiz anahtarları değerlere eşleyen bir veri yapısıdır. Tipi map[K]V biçiminde yazılır: K anahtarın, V değerin tipidir. map[string]int "string'den int'e bir map" diye okunur. Aynı anahtar bir map'te en fazla bir kez bulunur; var olan bir anahtara yeniden yazmak eski değeri günceller.

Arka planda map bir hash tablosudur. Anahtar bir hash fonksiyonundan geçirilir, çıkan sayı verinin tablonun hangi bölümünde duracağını belirler. Okurken aynı hesap tekrarlanır ve doğrudan o bölüme gidilir:

m["elma"] = 12

   "elma" ──► hash fonksiyonu ──► 0x7c91...e4 ──► tablo içindeki yer

          ┌──────────┬──────────┬──────────┬──────────┐ │
          │ bölüm 0  │ bölüm 1  │ bölüm 2  │ bölüm 3  │◄┘
          │          │ "kiraz"  │ "elma"   │          │
          │          │   → 30   │   → 12   │          │
          └──────────┴──────────┴──────────┴──────────┘

m["elma"] okunurken: aynı hash → aynı bölüm → tüm tabloyu taramaya gerek yok

İki farklı anahtarın aynı bölüme düşmesi (çakışma) mümkündür ve Go'nun uygulaması bunu kendisi yönetir. Go 1.24'ten itibaren yerleşik map, Swiss table adı verilen modern bir tasarımla uygulanıyor. Hash fonksiyonlarını, çakışma çözümünü ve sıfırdan bir hash tablosu yazmayı Hash Tabloları dersinde ayrıntılı olarak göreceksin. Bu derste map'i bir kullanıcı gözüyle ele alıyoruz.

Anahtar tipi karşılaştırılabilir olmalı

Map'in aynı anahtarı tekrar bulabilmesi için anahtarları == ile karşılaştırabilmesi gerekir. Bu yüzden anahtar tipi karşılaştırılabilir (comparable) olmalıdır: string, tüm sayı tipleri, bool, işaretçiler, diziler ve yalnızca karşılaştırılabilir alanlardan oluşan struct'lar anahtar olabilir. Dilimler, map'ler ve fonksiyonlar == ile karşılaştırılamadığı için anahtar olamaz; map[[]int]string yazmak invalid map key type []int derleme hatası verir. Değer tipi için ise hiçbir kısıt yoktur; değer bir dilim, başka bir map veya bir fonksiyon olabilir.

Birden fazla parçadan oluşan bir anahtara ihtiyacın varsa dizi veya struct kullanabilirsin: Izgara koordinatları için map[[2]int]bool gayet yaygın bir desendir.

Map oluşturma: make ve literal

Map oluşturmanın üç yolu vardır ve aralarındaki fark önemlidir:

main.go
package main

import "fmt"

func main() {
	// 1) Literal: başlangıç çiftleriyle birlikte
	plates := map[string]int{
		"Adana":    1,
		"Ankara":   6,
		"İstanbul": 34,
		"İzmir":    35, // son satırdaki virgül zorunlu
	}

	// 2) make: boş, kullanıma hazır map
	stock := make(map[string]int)
	stock["kalem"] = 120

	// make ile boyut ipucu: yaklaşık 1000 girdi bekliyoruz
	cache := make(map[int]string, 1000)

	// 3) Sıfır değer: nil map
	var empty map[string]int

	fmt.Println(plates, len(plates))
	fmt.Println(stock, len(stock))
	fmt.Println(len(cache), empty == nil, len(empty))
}
Çıktı
map[Adana:1 Ankara:6 İstanbul:34 İzmir:35] 4
map[kalem:120] 1
0 true 0

Literal, başlangıç verisi belli olduğunda en okunaklı yoldur. Her satırın sonundaki virgül, son satırda da zorunludur. Böylece yeni bir satır eklemek diğer satırlara dokunmayı gerektirmez. Boş bir map'e ihtiyacın varsa make(map[string]int) veya kısa literal map[string]int{} aynı sonucu verir.

make'e verilen ikinci argüman bir boyut ipucudur. Dilimlerdeki kapasiteden farklı olarak map'in uzunluğunu etkilemez ve map'ler için cap fonksiyonu yoktur. Yalnızca çalışma zamanına "yaklaşık bu kadar girdi gelecek, belleği baştan buna göre ayır" der. Map dolmaya başladığında zaten otomatik olarak büyür, ama girdi sayısını önceden biliyorsan ipucu vermek büyüme sırasında yapılan yeniden düzenlemeleri azaltır.

Üçüncü yol olan var empty map[string]int bir nil map üretir. Onu okuyabilirsin ama yazamazsın; bu tuzağa aşağıda ayrı bir bölümde döneceğiz.

Okuma, yazma ve silme

Map ile yapılan temel işlemler şunlardır:

İşlemSözdizimiNot
Ekleme veya güncellemem[k] = vAnahtar yoksa eklenir, varsa değer değişir
Okumav := m[k]Anahtar yoksa V tipinin sıfır değeri döner
Varlık kontrolüv, ok := m[k]ok, anahtarın map'te olup olmadığını söyler
Silmedelete(m, k)Anahtar yoksa hiçbir şey yapmaz
Girdi sayısılen(m)O(1)

Okuma, ekleme ve silme ortalama O(1) sürer. En çok dikkat edilmesi gereken satır okuma işlemidir: Olmayan bir anahtarı okumak hata veya panik üretmez, sessizce sıfır değer döndürür.

Eksik anahtar ve sıfır değer

Sıfır değer davranışı çoğu zaman işine yarar. Örneğin bir sayaç yazarken anahtarın daha önce eklenip eklenmediğini kontrol etmene gerek kalmaz: counts[word]++ ilk seferde 0 + 1 yapar. Ama bazen belirsizlik yaratır. Bir ürünün stok değeri 0 döndüğünde bu, "ürün kayıtlı ve stokta yok" mu, yoksa "böyle bir ürün hiç kayıtlı değil" mi demektir?

Virgül-ok deyimi

Bu belirsizliği çözmek için okuma ifadesi iki değer döndürebilir: v, ok := m[k]. İkinci değer olan ok, anahtar map'te varsa true, yoksa false olur. Bu kalıba virgül-ok (comma ok) deyimi denir ve Go'da tip iddialarında ve kanallarda da karşına çıkacak.

main.go
package main

import "fmt"

func main() {
	stock := map[string]int{
		"elma":  12,
		"armut": 0, // kayıtlı ama tükenmiş
	}

	stock["kiraz"] = 30    // ekleme
	stock["elma"] += 8     // güncelleme: 12 + 8
	delete(stock, "kiraz") // silme
	delete(stock, "muz")   // olmayan anahtarı silmek sorun değil

	fmt.Println("elma:", stock["elma"])
	fmt.Println("armut:", stock["armut"])
	fmt.Println("muz:", stock["muz"]) // kayıtlı değil ama 0 döner

	for _, fruit := range []string{"armut", "muz"} {
		if n, ok := stock[fruit]; ok {
			fmt.Printf("%s kayıtlı, adet: %d\n", fruit, n)
		} else {
			fmt.Printf("%s hiç kayıtlı değil\n", fruit)
		}
	}
	fmt.Println(stock, len(stock))
}
Çıktı
elma: 20
armut: 0
muz: 0
armut kayıtlı, adet: 0
muz hiç kayıtlı değil
map[armut:0 elma:20] 2

stock["armut"] ve stock["muz"] ikisi de 0 döndürdü; aradaki farkı yalnızca ok değeri gösterdi. if n, ok := stock[fruit]; ok yazımı, kontrol akışı dersinde gördüğün kısa ifadeli if kalıbıdır ve n ile ok değişkenlerini yalnızca if-else bloğunun içinde tanımlar. Map'ten değer okurken "anahtar yoksa ne olmalı?" sorusunun cevabı sıfır değerden farklıysa virgül-ok deyimini kullan.

Gezinme sırası rastgeledir

for key, value := range m ile bir map'in tüm girdilerini gezebilirsin. Ama dil, gezinme sırası hakkında hiçbir garanti vermez. Go çalışma zamanı bunu bilinçli olarak kullanır ve her range döngüsünde gezinmeye rastgele bir noktadan başlar. Böylece geliştiriciler yanlışlıkla belirli bir sıraya bağımlı kod yazamaz. Aşağıdaki program aynı map'i üç kez geziyor:

main.go
package main

import "fmt"

func main() {
	colors := map[string]string{
		"kırmızı": "#ff0000",
		"yeşil":   "#00ff00",
		"mavi":    "#0000ff",
		"sarı":    "#ffff00",
		"mor":     "#800080",
	}
	for round := range 3 {
		fmt.Print(round+1, ". tur:")
		for name := range colors {
			fmt.Print(" ", name)
		}
		fmt.Println()
	}
}
Çıktı

Bu programı birkaç kez çalıştırırsan her seferinde farklı sıralar göreceksin; aynı çalıştırmadaki turlar bile birbirinden farklı olabilir. Bu yüzden testlerin, log çıktıların veya kullanıcıya gösterdiğin listeler map gezinme sırasına bağlı olmamalı.

Deterministik gezinme: anahtarları sırala

Sıralı çıktıya ihtiyacın olduğunda anahtarları bir dilime toplayıp sıralarsın. Go 1.23 ile gelen iteratör fonksiyonları bunu tek satıra indirir: maps.Keys(m) anahtarlar üzerinde bir iteratör döndürür, slices.Sorted bu iteratördeki değerleri toplayıp sıralanmış bir dilim üretir.

main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	colors := map[string]string{
		"kırmızı": "#ff0000",
		"yeşil":   "#00ff00",
		"mavi":    "#0000ff",
		"sarı":    "#ffff00",
		"mor":     "#800080",
	}

	for _, name := range slices.Sorted(maps.Keys(colors)) {
		fmt.Printf("%-8s %s\n", name, colors[name])
	}
}
Çıktı
kırmızı  #ff0000
mavi     #0000ff
mor      #800080
sarı     #ffff00
yeşil    #00ff00

Bu çözümün maliyeti anahtarları sıralamaktır: k anahtar için O(k log k). Çoğu durumda önemsizdir. Ama bir map'i sürekli sıralı gezmen gerekiyorsa, belki de ihtiyacın olan veri yapısı map değil, sıralı bir dilim veya ikili arama ağacı gibi sıralı bir yapıdır. maps.Keys ve slices.Sorted gibi iteratör alan fonksiyonların nasıl çalıştığını İteratörler dersinde göreceksin.

Gezinirken map'i değiştirmekle ilgili kurallar da şöyledir: Döngü sırasında henüz ziyaret edilmemiş bir girdiyi delete ile silmek güvenlidir ve o girdi döngüde görünmez. Döngü sırasında eklenen bir girdi ise döngüde görünebilir de, görünmeyebilir de. Bu yüzden gezinirken ekleme yapmaktan kaçın.

Örnek: kelime frekansı sayacı

Şimdi öğrendiklerimizi bir araya getirelim ve bir Türkçe metinde en sık geçen kelimeleri bulalım. Bu, map'lerin en klasik kullanım alanlarından biridir:

main.go
package main

import (
	"cmp"
	"fmt"
	"maps"
	"slices"
	"strings"
	"unicode"
)

const text = `Bir varmış bir yokmuş. Evvel zaman içinde, kalbur saman içinde,
bir küçük kız varmış. Kız her gün ormana gidermiş. Orman büyükmüş,
kız küçükmüş. Işık azmış ama kız hiç korkmazmış.`

func main() {
	// Harf olmayan her karakterde böl
	words := strings.FieldsFunc(text, func(r rune) bool {
		return !unicode.IsLetter(r)
	})

	freq := make(map[string]int)
	for _, w := range words {
		// Türkçe kurallarıyla küçült: "Işık" -> "ışık"
		freq[strings.ToLowerSpecial(unicode.TurkishCase, w)]++
	}

	// Sıklığa göre azalan, eşitlikte alfabetik sırala
	keys := slices.Collect(maps.Keys(freq))
	slices.SortFunc(keys, func(a, b string) int {
		if c := cmp.Compare(freq[b], freq[a]); c != 0 {
			return c
		}
		return strings.Compare(a, b)
	})

	fmt.Println("toplam kelime:", len(words), "farklı kelime:", len(freq))
	for _, w := range keys[:5] {
		fmt.Printf("%-7s %d\n", w, freq[w])
	}
	fmt.Println("ışık:", freq["ışık"], "işık:", freq["işık"])
}
Çıktı
toplam kelime: 29 farklı kelime: 22
kız     4
bir     3
içinde  2
varmış  2
ama     1
ışık: 1 işık: 0

Programı adım adım inceleyelim:

  1. strings.FieldsFunc, verdiğimiz fonksiyon true döndürdüğü her karakterde metni böler. Harf olmayan her şeyi (boşluk, virgül, nokta, satır sonu) ayırıcı saydığımız için noktalama işaretleri kelimelere yapışmaz.
  2. Döngüde her kelimeyi küçük harfe çevirip freq[...]++ ile sayıyoruz. Anahtar ilk kez görüldüğünde sıfır değer 0 okunur ve 1 yazılır; virgül-ok kontrolüne gerek yoktur.
  3. Küçültmede strings.ToLower yerine strings.ToLowerSpecial(unicode.TurkishCase, w) kullandık. Standart ToLower, "Işık" kelimesini "işık" yapardı, çünkü Unicode'un varsayılan kuralında I harfinin küçüğü i'dir. Türkçede ise I'nın küçüğü ı'dır. Son satırdaki çıktı doğru anahtarın oluştuğunu doğruluyor. Bu konuyu String, Byte ve Rune dersinde ayrıntılı işleyeceğiz.
  4. Map sırasız olduğu için anahtarları slices.Collect(maps.Keys(freq)) ile bir dilime topladık ve slices.SortFunc ile iki kurala göre sıraladık: Önce sıklığa göre azalan (freq[b] ile freq[a] karşılaştırılıyor, sıra ters), eşitlik varsa alfabetik. İkinci kural olmasaydı aynı sıklıktaki kelimelerin sırası her çalıştırmada değişebilirdi.

Toplam maliyet, n kelimeyi saymak için O(n) ve k farklı kelimeyi sıralamak için O(k log k)'dir.

nil map tuzağı

Bir map değişkeninin sıfır değeri nil'dir. nil map, okuma işlemlerinde boş bir map gibi davranır: Her anahtar için sıfır değer döner, len 0'dır, range hiç dönmez, delete hiçbir şey yapmaz. Ama nil map'e yazmaya çalışmak paniğe yol açar, çünkü girdilerin yazılacağı bir tablo henüz ayrılmamıştır.

main.go
package main

import "fmt"

func main() {
	var scores map[string]int // nil map

	fmt.Println(scores["ali"], len(scores)) // okumak güvenli
	for name := range scores {
		fmt.Println(name) // hiç çalışmaz
	}
	delete(scores, "ali") // güvenli, etkisiz

	scores["ali"] = 90 // panik!
	fmt.Println("buraya ulaşılmaz")
}
Çıktı

Bu hata en çok, map alanı olan bir struct'ın sıfır değeriyle kullanıldığı durumlarda karşına çıkar. Struct'ın diğer alanları kullanıma hazırdır, ama map alanı nil'dir ve ilk yazmada program çöker:

type Inventory struct {
	items map[string]int // sıfır değeri nil
}

var inv Inventory
inv.items["kalem"] = 10 // panic: assignment to entry in nil map

Çözüm, map'i yazmadan önce make veya literal ile oluşturmaktır. Struct'larda bunu genellikle bir yapıcı fonksiyonda (NewInventory) yaparsın; bu deseni Struct'lar ve Metotlar dersinde göreceksin. Bir fonksiyonun hem nil hem dolu map ile çalışması gerekiyorsa ilk yazmadan önce kontrol edebilirsin: if m == nil { m = make(map[string]int) }.

Küme (set): map[T]struct{}

Go'da yerleşik bir küme (set) tipi yoktur; bunun yerine yalnızca anahtarları önemli olan bir map kullanılır. Bir elemanın kümede olup olmadığını sormak, anahtarın map'te olup olmadığını sormakla aynı şeydir ve ortalama O(1) sürer.

Değer tipi için iki yaygın seçenek vardır. map[string]bool okunması kolaydır: if seen[x] yazabilirsin, çünkü eksik anahtar zaten false döner. map[string]struct{} ise değer için sıfır byte kullanır; struct{} hiç alanı olmayan boş struct tipidir ve tek değeri struct{}{}'dir. Bu seçenek hem bellekte biraz tasarruf sağlar hem de okuyana "burada yalnızca anahtarlar önemli" mesajını açıkça verir.

main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

func intersect(a, b map[string]struct{}) map[string]struct{} {
	out := make(map[string]struct{})
	for k := range a {
		if _, ok := b[k]; ok {
			out[k] = struct{}{}
		}
	}
	return out
}

func main() {
	visited := make(map[string]struct{})
	route := []string{"Ankara", "Konya", "Ankara", "Antalya", "Konya", "İzmir"}
	for _, city := range route {
		visited[city] = struct{}{} // tekrar eklemek etkisiz
	}
	fmt.Println("farklı şehir sayısı:", len(visited))

	if _, ok := visited["Konya"]; ok {
		fmt.Println("Konya ziyaret edildi")
	}
	if _, ok := visited["Van"]; !ok {
		fmt.Println("Van ziyaret edilmedi")
	}

	delete(visited, "Ankara")
	fmt.Println("kalanlar:", slices.Sorted(maps.Keys(visited)))

	friend := map[string]struct{}{"İzmir": {}, "Bursa": {}, "Konya": {}}
	common := intersect(visited, friend)
	fmt.Println("ortak şehirler:", slices.Sorted(maps.Keys(common)))
}
Çıktı
farklı şehir sayısı: 4
Konya ziyaret edildi
Van ziyaret edilmedi
kalanlar: [Antalya Konya İzmir]
ortak şehirler: [Konya İzmir]

Kümeye eleman eklemek visited[city] = struct{}{} biçimindedir. İlk süslü parantez çifti tipi (struct{}), ikincisi o tipin değerini oluşturan literali belirtir. Map literalinde ise değer tipi zaten bilindiği için yalnızca {} yazmak yeterlidir. Kesişim fonksiyonu bir kümeyi gezip her elemanı diğerinde arar; boyutları n ve m olan iki küme için O(n) sürer. Daha küçük kümeyi gezmek işi hızlandırır.

map[T]boolmap[T]struct{}
Eklemes[x] = trues[x] = struct{}{}
Üyelik kontrolüif s[x]if _, ok := s[x]; ok
Değer başına bellek1 byte0 byte
Dikkats[x] = false üç durumlu belirsizlik yaratabilirNiyet açık: yalnızca anahtarlar

Her iki yol da idiomatiktir. Küçük programlarda bool okunabilirliği öne çıkarır; büyük kümelerde ve kütüphane kodunda struct{} daha sık tercih edilir. Jenerikleri öğrendikten sonra bu map'i Set[T comparable] gibi kendi tipinin içine saklayabilirsin (Jenerikler).

Değer olarak dilim ve struct tutma

Map'in değer tipi herhangi bir tip olabilir. Pratikte en sık görülen iki durum, bir anahtara birden fazla değer bağlamak için dilim saklamak ve bir anahtara ilişkin birden fazla bilgiyi bir struct içinde tutmaktır. İkisinin de bilmen gereken birer ayrıntısı var.

Map içinde dilim: gruplama

Bir anahtara karşılık bir liste tutmak için map[string][]string kullanılır. Güzel olan şu ki eksik anahtarın sıfır değeri nil bir dilimdir ve nil dilime append yapmak güvenlidir. Bu yüzden gruplama kodu kontrol gerektirmez:

main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	regionOf := map[string]string{
		"İzmir":   "Ege",
		"Ankara":  "İç Anadolu",
		"Manisa":  "Ege",
		"Konya":   "İç Anadolu",
		"Trabzon": "Karadeniz",
		"Aydın":   "Ege",
	}

	// Map'i ters çevir: bölge -> şehirler
	byRegion := make(map[string][]string)
	for city, region := range regionOf {
		byRegion[region] = append(byRegion[region], city)
	}

	for _, region := range slices.Sorted(maps.Keys(byRegion)) {
		cities := byRegion[region]
		slices.Sort(cities) // gezinme sırası rastgele olduğu için sırala
		fmt.Printf("%s (%d): %v\n", region, len(cities), cities)
	}
}
Çıktı
Ege (3): [Aydın Manisa İzmir]
Karadeniz (1): [Trabzon]
İç Anadolu (2): [Ankara Konya]

byRegion[region] = append(byRegion[region], city) satırındaki atama zorunludur. append yeni bir dilim başlığı döndürür (Diziler ve Dilimler dersini hatırla) ve map'te saklanan başlığın güncellenmesi gerekir. Öte yandan slices.Sort(cities) çağrısı map'teki dilimi de sıraladı: cities değişkeni map'teki başlığın bir kopyasıdır ama aynı alttaki diziyi gösterir. Elemanları yerinde değiştiren işlemler için geri atama gerekmez; uzunluğu değiştiren işlemler için gerekir.

Çıktıda "İç Anadolu"nun "Karadeniz"den sonra gelmesi de dikkatini çekmiş olabilir. Bunun nedeni sıralamanın byte değerlerine göre yapılması ve İ harfinin UTF-8 kodlamasının ASCII harflerinden büyük olmasıdır.

Map içindeki struct'ın alanı değiştirilemez

Bir oyuncunun puanını map'te struct olarak sakladığını düşün ve puanı artırmak iste:

main.go
package main

import "fmt"

type Player struct {
	Name  string
	Score int
}

func main() {
	players := map[string]Player{
		"ayse": {Name: "Ayşe", Score: 40},
	}
	players["ayse"].Score += 10 // derleme hatası
	fmt.Println(players)
}
Çıktı
./prog.go:14:2: cannot assign to struct field players["ayse"].Score in map

Derleyici bu satırı reddeder, çünkü map elemanları adreslenebilir değildir. Map büyürken çalışma zamanı girdileri tablonun başka bir yerine taşıyabilir. Bir girdinin adresini almana (&players["ayse"]) veya onun bir alanına yerinde yazmana izin verilseydi, taşıma sonrasında bu adres geçersiz belleği gösterirdi. Go bu riski tümden ortadan kaldırmak için m[k].alan = v ve &m[k] ifadelerini yasaklar. players["ayse"] okuması sana struct'ın bir kopyasını verir ve kopyanın alanını değiştirmenin map'e bir etkisi olmazdı. İki yaygın çözüm vardır:

main.go
package main

import "fmt"

type Player struct {
	Name  string
	Score int
}

func main() {
	players := map[string]Player{
		"ayse": {Name: "Ayşe", Score: 40},
		"mert": {Name: "Mert", Score: 25},
	}

	// Çözüm 1: kopyayı al, değiştir, geri yaz
	p := players["ayse"]
	p.Score += 10
	players["ayse"] = p
	fmt.Println(players["ayse"])

	// Dikkat: range ile gelen değer de bir kopyadır
	for _, p := range players {
		p.Score = 0 // map etkilenmez
	}
	fmt.Println(players["mert"])

	// Çözüm 2: değer olarak işaretçi sakla
	ptrs := map[string]*Player{
		"ayse": {Name: "Ayşe", Score: 40},
	}
	ptrs["ayse"].Score += 10 // işaretçinin gösterdiği struct değişir
	fmt.Println(*ptrs["ayse"])
}
Çıktı
{Ayşe 50}
{Mert 25}
{Ayşe 50}

İlk çözüm struct küçükse ve güncelleme seyrekse en temiz yoldur; map'teki veri her zaman bağımsız kopyalardan oluşur. İkinci çözümde map'te struct'ın kendisi değil, struct'a işaretçi durur. ptrs["ayse"] bir işaretçi döndürür ve bu işaretçi üzerinden yapılan değişiklik heap'teki asıl struct'ı günceller; taşınan şey yalnızca işaretçinin kendisidir. Bu yol büyük struct'larda kopyalamayı önler, ama aynı struct'a birden fazla yerden erişilebildiği için kimin neyi değiştirdiğini takip etmek zorlaşır. İşaretçileri İşaretçiler dersinde ayrıntılı göreceksin.

Map'ler fonksiyonlara nasıl geçer?

Go'da her şey değer ile geçirilir; map de istisna değildir. Ancak bir map değişkeninin içinde tablonun kendisi değil, çalışma zamanındaki tablo yapısını gösteren bir işaretçi bulunur. Fonksiyona map geçirdiğinde bu işaretçi kopyalanır ve iki değişken aynı tabloyu gösterir. Bu yüzden map'ler "referans gibi" davranır:

main içindeki scores ──┐
                       ├──►  ┌───────────────────────────┐
addBonus içindeki m ───┘     │ çalışma zamanı tablosu    │
                             │ "ayşe" → 75, "mert" → 60  │
                             └───────────────────────────┘
main.go
package main

import (
	"fmt"
	"maps"
)

func addBonus(scores map[string]int, bonus int) {
	for name := range scores {
		scores[name] += bonus // çağıranın map'i değişir
	}
}

func replace(scores map[string]int) {
	scores = map[string]int{"yeni": 1} // yalnızca yerel değişken değişir
	_ = scores
}

func main() {
	scores := map[string]int{"ayşe": 70, "mert": 55}

	addBonus(scores, 5)
	fmt.Println("addBonus sonrası:", scores)

	replace(scores)
	fmt.Println("replace sonrası:", scores)

	alias := scores // aynı tabloyu gösterir
	alias["zeynep"] = 90
	fmt.Println("alias sonrası:", scores)

	copied := maps.Clone(scores) // bağımsız kopya
	copied["mert"] = 0
	fmt.Println("scores:", scores)
	fmt.Println("copied:", copied)
}
Çıktı
addBonus sonrası: map[ayşe:75 mert:60]
replace sonrası: map[ayşe:75 mert:60]
alias sonrası: map[ayşe:75 mert:60 zeynep:90]
scores: map[ayşe:75 mert:60 zeynep:90]
copied: map[ayşe:75 mert:0 zeynep:90]

addBonus'un yaptığı değişiklikler main'e yansıdı, çünkü girdiler ortak tabloya yazıldı. replace ise yerel scores değişkenine yeni bir map atadı. Bu atama yalnızca kopyalanan işaretçiyi değiştirdi, çağıranınkini değil. Aynı mantıkla alias := scores yeni bir map oluşturmaz; iki isim tek bir tabloyu paylaşır. Gerçekten bağımsız bir kopya istiyorsan maps.Clone kullanmalısın. Dilimlerden farklı olarak burada "uzunluk değişikliği çağırana yansımaz" gibi bir istisna yoktur: Fonksiyon içinde eklenen ve silinen girdiler de çağıran tarafından görülür.

maps paketi ve clear

Go 1.21 ile gelen maps paketi ve yerleşik clear fonksiyonu, map'lerle ilgili tekrar eden döngüleri ortadan kaldırır. Go 1.23'te pakete iteratör döndüren Keys, Values ve All gibi fonksiyonlar eklendi.

main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	defaults := map[string]string{"dil": "tr", "tema": "açık", "yazı": "orta"}
	user := map[string]string{"tema": "koyu"}

	settings := maps.Clone(defaults) // sığ kopya
	maps.Copy(settings, user)        // user'daki değerler üzerine yazılır
	fmt.Println("ayarlar:", settings)
	fmt.Println("varsayılandan farklı mı?", !maps.Equal(settings, defaults))

	stock := map[string]int{"kalem": 0, "silgi": 7, "defter": 0, "cetvel": 3}
	maps.DeleteFunc(stock, func(name string, n int) bool {
		return n == 0 // tükenenleri sil
	})
	fmt.Println("stokta olanlar:", stock)

	fmt.Println("anahtarlar:", slices.Sorted(maps.Keys(settings)))
	fmt.Println("değerler:", slices.Sorted(maps.Values(settings)))

	clear(settings) // tüm girdileri sil
	fmt.Println("clear sonrası:", settings, len(settings), settings == nil)

	nums := []int{1, 2, 3}
	clear(nums) // dilimde elemanları sıfırlar, uzunluk aynı kalır
	fmt.Println(nums, len(nums))
}
Çıktı
ayarlar: map[dil:tr tema:koyu yazı:orta]
varsayılandan farklı mı? true
stokta olanlar: map[cetvel:3 silgi:7]
anahtarlar: [dil tema yazı]
değerler: [koyu orta tr]
clear sonrası: map[] 0 false
[0 0 0] 3
FonksiyonNe yapar?
maps.Keys(m), maps.Values(m)Anahtarlar veya değerler üzerinde iteratör (sırasız)
maps.All(m)Anahtar-değer çiftleri üzerinde iteratör
maps.Clone(m)Sığ kopya; nil map için nil döner
maps.Copy(dst, src)src'deki tüm çiftleri dst'ye yazar, aynı anahtarlarda üzerine yazar
maps.Equal(m1, m2)Aynı anahtar-değer çiftlerini mi içeriyorlar?
maps.DeleteFunc(m, f)f(k, v) true dönen girdileri siler
maps.Collect(seq)Bir anahtar-değer iteratöründen yeni map oluşturur
clear(m)Map'teki tüm girdileri siler (map nil olmaz)

clear fonksiyonu map ve dilimde farklı davranır: Map'te tüm girdileri siler ve len 0 olur. Dilimde ise elemanları sıfır değere ayarlar, uzunluk değişmez. clear sonrası map hâlâ aynı tablodur; bu map'i paylaşan tüm değişkenler de boş map görür. Oysa m = make(map[string]string) yazmak yalnızca m değişkenine yeni bir map bağlar ve eski map'i gösteren diğer değişkenleri etkilemez.

maps.Clone ve maps.Copy sığ kopyalar: Değerler dilim, map veya işaretçi ise kopyalanan şey bu referanslardır ve gösterdikleri veri paylaşılmaya devam eder.

Eşzamanlı erişim uyarısı

Map'ler eşzamanlı kullanım için güvenli değildir. Birden fazla goroutine aynı map'i yalnızca okuyorsa sorun yoktur. Ama en az biri yazıyorsa, aynı anda başka bir goroutine'in okuması veya yazması tablonun iç yapısını bozabilir. Goroutine'leri henüz görmedik; şimdilik onları aynı anda çalışan işler olarak düşün. Aşağıdaki programda dört goroutine aynı map'e kilitsiz yazıyor:

package main

import (
	"fmt"
	"sync"
)

func main() {
	counts := make(map[int]int)
	var wg sync.WaitGroup
	for g := range 4 {
		wg.Go(func() {
			for i := range 100_000 {
				counts[i%10] += g // kilitsiz eşzamanlı yazma
			}
		})
	}
	wg.Wait()
	fmt.Println(counts)
}
Çıktı

Go çalışma zamanı, map'e yazılırken aynı anda başka bir erişim olduğunu fark ettiğinde veri bozulmasına izin vermek yerine programı anında sonlandırır. Bu bir panic değil fatal error'dır, yani recover ile yakalanamaz. Tespit her zaman garanti değildir: Zamanlama şansına bağlı olarak program bazen hatasız bitip yanlış sonuç da üretebilir. Bu yüzden eşzamanlı kodu yarış dedektörüyle (go run -race) test etmek önemlidir.

Çözüm, map'e erişimi senkronize etmektir. En yaygın yol, map'i bir sync.Mutex ile korumaktır:

type SafeCounter struct {
	mu     sync.Mutex
	counts map[string]int
}

func (c *SafeCounter) Inc(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.counts[key]++
}

Okumaların yazmalardan çok daha fazla olduğu durumlarda sync.RWMutex, belirli erişim desenleri için ise sync.Map seçenekleri de vardır. Bunların hepsini Senkronizasyon: sync ve atomic dersinde, goroutine'leri ve yarış dedektörünü ise Goroutine'ler dersinde öğreneceksin.

İşlemlerin maliyeti

İşlemOrtalamaNot
m[k] okuma, v, ok := m[k]O(1)Hash hesabı anahtar boyutuna bağlıdır (uzun string'ler daha pahalı)
m[k] = v ekleme/güncellemeO(1)Büyüme anında o işlem daha uzun sürer
delete(m, k)O(1)Bellek geri küçülmez
len(m)O(1)Sayaç tutulur
for range mO(n)Sıra belirsiz
Sıralı gezinmeO(n log n)Anahtarları sıralama maliyeti
maps.Clone(m)O(n)Sığ kopya

Sık yapılan hatalar

  • nil map'e yazmak. var m map[string]int sonrası m["x"] = 1 panik oluşturur. Yazacağın map'i make veya literal ile oluştur; struct alanlarını yapıcı fonksiyonda başlat.
  • Gezinme sırasına güvenmek. Map'i range ile gezerek üretilen çıktılar her çalıştırmada değişebilir. Deterministik çıktı için slices.Sorted(maps.Keys(m)) ile anahtarları sırala.
  • Eksik anahtarla sıfır değeri karıştırmak. m[k] 0 döndürdüğünde anahtarın var olduğunu varsayma; ayrım önemliyse v, ok := m[k] kullan.
  • Map'teki struct'ın alanını yerinde değiştirmeye çalışmak. m[k].Alan = v derlenmez. Kopyala-değiştir-geri yaz ya da map[K]*T kullan. Aynı şekilde for _, v := range m ile gelen v bir kopyadır.
  • Map ataması ile kopya aldığını sanmak. b := a aynı tabloyu paylaşır; bağımsız kopya için maps.Clone(a) kullan.
  • append sonucunu map'e geri yazmamak. m[k] = append(m[k], v) biçiminde yazılmalı; yalnızca append(m[k], v) sonucunu bir yerel değişkende bırakmak map'i güncellemez.
  • Birden fazla goroutine'den kilitsiz yazmak. Program fatal error: concurrent map writes ile sonlanır. Erişimi sync.Mutex ile koru.
  • Dilimi anahtar yapmaya çalışmak. map[[]int]V derlenmez; sabit boyutlu diziler ([2]int), struct'lar veya dilimden üretilmiş bir string anahtar kullan.

Alıştırmalar

Alıştırma·İki toplam
Kolay

Bir []int dilimi ve bir target değeri alan twoSum(nums []int, target int) (int, int, bool) fonksiyonunu yaz. Toplamı target olan iki farklı elemanın indekslerini döndürsün; böyle bir çift yoksa false dönsün. Çözüm O(n) zamanda çalışmalı. Örnek: [2 7 11 15] ve 9 için 0 1 true.

İpucu

Dilimi bir kez gez. Her x elemanı için ihtiyacın olan eş target - x'tir. Daha önce gördüğün değerleri indeksleriyle birlikte bir map[int]int içinde tutarsan, eşin daha önce görülüp görülmediğini virgül-ok deyimiyle O(1) sürede sorabilirsin.

Çözümü göster
main.go
package main

import "fmt"

func twoSum(nums []int, target int) (int, int, bool) {
	seen := make(map[int]int) // değer -> indeks
	for i, x := range nums {
		if j, ok := seen[target-x]; ok {
			return j, i, true
		}
		seen[x] = i
	}
	return 0, 0, false
}

func main() {
	fmt.Println(twoSum([]int{2, 7, 11, 15}, 9))
	fmt.Println(twoSum([]int{3, 2, 4}, 6))
	fmt.Println(twoSum([]int{3, 3}, 6))
	fmt.Println(twoSum([]int{1, 2, 3}, 100))
}
Çıktı
0 1 true
1 2 true
0 1 true
0 0 false

Kaba kuvvet çözümü her çifti dener ve O(n²) sürer. Map ile her eleman için eşini O(1) ortalama sürede sorduğumuz için toplam süre O(n), ek alan O(n) olur. Eşi kontrol etmeyi seen[x] = i atamasından önce yapmak önemlidir: [3 2 4] ve 6 örneğinde 3'ün kendisiyle eşleşmesini önler. [3 3] örneğinde ise ikinci 3 geldiğinde ilki zaten map'te olduğu için doğru çift bulunur.

Alıştırma·Türkçe anagram kontrolü
Kolay

İki kelimenin birbirinin anagramı olup olmadığını (aynı harflerden aynı sayıda içerip içermediğini) döndüren isAnagram(a, b string) bool fonksiyonunu yaz. Türkçe karakterlerle doğru çalışmalı: "kalem" ile "kelam" anagramdır, "ağaç" ile "agac" değildir.

İpucu

Bir map[rune]int ile ilk kelimedeki her harfi say, ikinci kelimedeki her harf için sayacı azalt. range bir string'i gezerken byte'ları değil harfleri (rune) verir, bu yüzden ğ veya ç gibi çok byte'lı karakterler sorun çıkarmaz. Sonda sıfır olmayan bir sayaç kaldıysa kelimeler anagram değildir.

Çözümü göster
main.go
package main

import "fmt"

func isAnagram(a, b string) bool {
	counts := make(map[rune]int)
	for _, r := range a {
		counts[r]++
	}
	for _, r := range b {
		counts[r]--
		if counts[r] < 0 {
			return false // b'de bu harf a'dakinden fazla
		}
	}
	for _, c := range counts {
		if c != 0 {
			return false // a'da fazladan harf kaldı
		}
	}
	return true
}

func main() {
	pairs := [][2]string{
		{"kalem", "kelam"},
		{"ağaç", "çağa"},
		{"ağaç", "agac"},
		{"elma", "armut"},
		{"kitap", "kitaplık"},
	}
	for _, p := range pairs {
		fmt.Printf("%s / %s: %t\n", p[0], p[1], isAnagram(p[0], p[1]))
	}
}
Çıktı
kalem / kelam: true
ağaç / çağa: true
ağaç / agac: false
elma / armut: false
kitap / kitaplık: false

range ile string gezmek her adımda bir rune verdiği için ğ ve g farklı anahtarlar olur ve "ağaç" ile "agac" doğru biçimde ayrılır. İkinci döngüdeki erken çıkış, b'nin daha uzun olduğu durumları hızlıca yakalar; son döngü ise a'da artan harfleri yakalar. Map'i gezme sırası burada önemsizdir, çünkü yalnızca "sıfır olmayan var mı?" diye soruyoruz. İki kelimenin toplam uzunluğu n ise zaman O(n), farklı harf sayısı k ise alan O(k) olur.

Alıştırma·Ters indeks
Orta

Arama motorlarının temelinde ters indeks (inverted index) vardır: Her kelimeyi, geçtiği belgelerin numaralarına eşler. Belge metinlerinden oluşan bir []string alan ve map[string][]int döndüren buildIndex fonksiyonunu yaz. Kelimeleri Türkçe kurallarla küçük harfe çevir. Bir kelime aynı belgede birden çok kez geçse bile belge numarası listede bir kez yer almalı. Sonra "kedi", "köpek" ve "balık" kelimelerinin geçtiği belgeleri yazdır.

İpucu

Belgeleri sırayla gez ve her belge için kelimeleri strings.FieldsFunc ile ayır. Belge numaraları artan sırada işlendiği için, bir kelimenin listesinin son elemanı mevcut belge numarasıysa o belgeyi zaten eklemişsindir. Eksik anahtarda nil dilime append yapmanın güvenli olduğunu hatırla.

Çözümü göster
main.go
package main

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

func buildIndex(docs []string) map[string][]int {
	index := make(map[string][]int)
	for id, doc := range docs {
		words := strings.FieldsFunc(doc, func(r rune) bool {
			return !unicode.IsLetter(r)
		})
		for _, w := range words {
			w = strings.ToLowerSpecial(unicode.TurkishCase, w)
			ids := index[w]
			if len(ids) > 0 && ids[len(ids)-1] == id {
				continue // bu belge zaten eklendi
			}
			index[w] = append(ids, id)
		}
	}
	return index
}

func main() {
	docs := []string{
		"Kedi bahçede uyuyor. Kedi çok tembel.",
		"Köpek kediyi kovaladı.",
		"Balık akvaryumda, kedi balığa bakıyor.",
		"Köpek ve kedi arkadaş oldu.",
	}
	index := buildIndex(docs)

	for _, q := range []string{"kedi", "köpek", "balık", "kuş"} {
		ids, ok := index[q]
		if !ok {
			fmt.Printf("%-6s bulunamadı\n", q)
			continue
		}
		fmt.Printf("%-6s belgeler: %v\n", q, ids)
	}
	fmt.Println("farklı kelime sayısı:", len(index))
}
Çıktı
kedi   belgeler: [0 2 3]
köpek  belgeler: [1 3]
balık  belgeler: [2]
kuş    bulunamadı
farklı kelime sayısı: 15

Her kelime için index[w] dilimini okuyup yalnızca son elemanına bakıyoruz. Belgeler artan numara sırasıyla işlendiğinden, aynı belge bir listeye ancak son eleman olarak eklenmiş olabilir. Bu da tekrar kontrolünü O(1) yapar. "kediyi" ve "balığa" gibi ekli kelimelerin ayrı anahtarlar olduğuna dikkat et; gerçek arama motorları bu yüzden kök bulma (stemming) gibi dile özgü işlemler kullanır. Toplam kelime sayısı n ise indeks O(n) sürede kurulur ve bir sorgu ortalama O(1) sürede cevaplanır.

Alıştırma·En uzun ardışık sayı dizisi
Zor

Sırasız bir []int dilimindeki en uzun ardışık tamsayı dizisinin uzunluğunu bulan longestConsecutive(nums []int) int fonksiyonunu yaz. Örneğin [100 4 200 1 3 2] için cevap 4'tür (1 2 3 4). Çözümün sıralama kullanmamalı ve ortalama O(n) sürede çalışmalı. Tekrar eden sayılar olabilir.

İpucu

Önce tüm sayıları bir map[int]struct{} kümesine koy. Bir x sayısı, ancak x-1 kümede yoksa bir dizinin başlangıcıdır. Yalnızca başlangıç noktalarından ileriye doğru (x+1, x+2, ...) sayarsan her sayı en fazla bir kez "ileriye sayma" adımında ziyaret edilir.

Çözümü göster
main.go
package main

import "fmt"

func longestConsecutive(nums []int) int {
	set := make(map[int]struct{}, len(nums))
	for _, x := range nums {
		set[x] = struct{}{}
	}

	best := 0
	for x := range set {
		if _, hasPrev := set[x-1]; hasPrev {
			continue // x bir dizinin ortasında, başlangıcı değil
		}
		length := 1
		for {
			if _, ok := set[x+length]; !ok {
				break
			}
			length++
		}
		best = max(best, length)
	}
	return best
}

func main() {
	fmt.Println(longestConsecutive([]int{100, 4, 200, 1, 3, 2}))
	fmt.Println(longestConsecutive([]int{0, 3, 7, 2, 5, 8, 4, 6, 0, 1}))
	fmt.Println(longestConsecutive([]int{5, 5, 5}))
	fmt.Println(longestConsecutive(nil))
}
Çıktı
4
9
1
0

İlk bakışta iç içe döngü O(n²) gibi görünür. Ama iç döngü yalnızca bir dizinin başlangıcı olan sayılar için çalışır ve her dizinin elemanlarını bir kez sayar. Dizilerin toplam uzunluğu en fazla n olduğu için iç döngünün tüm adımlarının toplamı da en fazla n'dir; toplam süre ortalama O(n), küme için alan O(n) olur. Kümeyi range ile gezme sırası rastgele olsa da sonuç değişmez, çünkü her başlangıç noktası mutlaka bir kez işlenir. Tekrarlar kümede tek anahtara indirgendiği için [5 5 5] doğru biçimde 1 verir; nil dilimde döngü hiç çalışmaz ve 0 döner.

Kısa sınav

Kısa sınav

m := map[string]int{"a": 0}; v, ok := m["b"]; fmt.Println(v, ok) ne yazdırır?

var m map[string]int tanımından sonra m["x"] = 1 satırı ne olur?

Bir map'i for k, v := range m ile gezdiğinde sıra için hangisi doğrudur?

players map[string]Player için players["ayse"].Score++ satırı neden derlenmez?

func f(m map[string]int) { m["a"] = 1; m = nil } fonksiyonu scores := map[string]int{} ile çağrıldıktan sonra len(scores) kaçtır?

İki goroutine aynı map'e kilit kullanmadan aynı anda yazarsa ne olabilir?

Özet

  • Map, benzersiz anahtarları değerlere eşleyen yerleşik bir hash tablosudur; okuma, yazma ve silme ortalama O(1) sürer. Anahtar tipi == ile karşılaştırılabilir olmalıdır.
  • Map'i make(map[K]V) veya literal ile oluştur. var m map[K]V nil map üretir: Okunabilir ama yazmak panik oluşturur.
  • Eksik anahtar sıfır değer döndürür. Anahtarın var olup olmadığını ayırt etmek için virgül-ok deyimini kullan: v, ok := m[k].
  • Gezinme sırası belirsizdir ve bilinçli olarak rastgeledir. Deterministik çıktı için slices.Sorted(maps.Keys(m)) ile anahtarları sırala.
  • Küme için map[T]struct{} (sıfır byte değer) veya map[T]bool kullan.
  • Map elemanları adreslenebilir değildir: m[k].Alan = v derlenmez. Kopyala-değiştir-geri yaz ya da işaretçi sakla. Dilim değerlerinde m[k] = append(m[k], v) ile geri atamayı unutma.
  • Map'ler fonksiyonlara geçirildiğinde tablo paylaşılır; bağımsız kopya için maps.Clone, tüm girdileri silmek için clear kullan.
  • Map'ler eşzamanlı yazma için güvenli değildir; kilitsiz eşzamanlı erişim programı fatal error ile sonlandırabilir. Erişimi sync.Mutex ile koru.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.