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:
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))
}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:
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.
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))
}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:
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()
}
}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.
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])
}
}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:
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"])
}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:
strings.FieldsFunc, verdiğimiz fonksiyontruedö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.- Döngüde her kelimeyi küçük harfe çevirip
freq[...]++ile sayıyoruz. Anahtar ilk kez görüldüğünde sıfır değer0okunur ve1yazılır; virgül-ok kontrolüne gerek yoktur. - Küçültmede
strings.ToLoweryerinestrings.ToLowerSpecial(unicode.TurkishCase, w)kullandık. StandartToLower, "Işık" kelimesini "işık" yapardı, çünkü Unicode'un varsayılan kuralındaIharfinin küçüğüi'dir. Türkçede iseI'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. - Map sırasız olduğu için anahtarları
slices.Collect(maps.Keys(freq))ile bir dilime topladık veslices.SortFuncile iki kurala göre sıraladık: Önce sıklığa göre azalan (freq[b]ilefreq[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.
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")
}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.
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)))
}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.
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:
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)
}
}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:
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)
}./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:
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"])
}{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 │
└───────────────────────────┘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)
}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.
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))
}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
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)
}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
Sık yapılan hatalar
- nil map'e yazmak.
var m map[string]intsonrasım["x"] = 1panik oluşturur. Yazacağın map'imakeveya literal ile oluştur; struct alanlarını yapıcı fonksiyonda başlat. - Gezinme sırasına güvenmek. Map'i
rangeile gezerek üretilen çıktılar her çalıştırmada değişebilir. Deterministik çıktı içinslices.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 önemliysev, ok := m[k]kullan. - Map'teki struct'ın alanını yerinde değiştirmeye çalışmak.
m[k].Alan = vderlenmez. Kopyala-değiştir-geri yaz ya damap[K]*Tkullan. Aynı şekildefor _, v := range mile gelenvbir kopyadır. - Map ataması ile kopya aldığını sanmak.
b := aaynı tabloyu paylaşır; bağımsız kopya içinmaps.Clone(a)kullan. appendsonucunu map'e geri yazmamak.m[k] = append(m[k], v)biçiminde yazılmalı; yalnızcaappend(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 writesile sonlanır. Erişimisync.Mutexile koru. - Dilimi anahtar yapmaya çalışmak.
map[[]int]Vderlenmez; sabit boyutlu diziler ([2]int), struct'lar veya dilimden üretilmiş bir string anahtar kullan.
Alıştırmalar
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
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))
}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.
İ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
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]))
}
}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.
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
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))
}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.
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
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))
}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
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]Vnil 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) veyamap[T]boolkullan. - Map elemanları adreslenebilir değildir:
m[k].Alan = vderlenmez. Kopyala-değiştir-geri yaz ya da işaretçi sakla. Dilim değerlerindem[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çinclearkullan. - Map'ler eşzamanlı yazma için güvenli değildir; kilitsiz eşzamanlı erişim programı
fatal errorile sonlandırabilir. Erişimisync.Mutexile koru.