Veri yarışı (data race), iki goroutine'in aynı değişkene eşzamanlı eriştiği ve bu erişimlerden en az birinin yazma olduğu durumdur; erişimlerin tamamı sync/atomic ile yapılmıyorsa program hatalıdır. Go'da veri yarışını önlemenin yolu erişimi sıraya koymaktır: kanal, sync.Mutex ya da sync/atomic. Tek bir sayacı, bayrağı ya da işaretçiyi kilitsiz güncellemek için Go 1.19 ile gelen atomic.Int64, atomic.Bool, atomic.Pointer[T] gibi tipler kullanılır. Yarışları bulmak için de testleri go test -race ile çalıştırırsınız.
Bu yazıda Go Memory Model belgesine göre veri yarışının tanımını ve neden "zararsız yarış" olmadığını, "önce olur" (happens before) ilişkisini, sync/atomic tiplerini ve karşılaştır-değiştir (compare-and-swap) döngüsünü, atomic.Value ile atomic.Pointer arasındaki farkı ve race detector'ün nasıl kullanıldığını anlatıyoruz. Örnekler Go 1.27.3 ile çalıştırıldı. Race detector cgo ve bir C derleyicisi gerektirdiği için onu bu makinede çalıştırmadık; ilgili bölüm resmî belgeye dayanıyor. Seri için Go öğrenme rehberi sayfasına bakabilirsiniz.
Veri yarışı nedir?
Bellek modeli belgesi veri yarışını, aynı bellek konumuna yapılan bir yazmanın aynı konuma yapılan başka bir okuma ya da yazmayla eşzamanlı gerçekleşmesi olarak tanımlar; ilgili bütün erişimler sync/atomic'in sağladığı atomik erişimlerse bu bir yarış sayılmaz. Belgenin tavsiye bölümü kısadır: birden çok goroutine'in aynı anda eriştiği veriyi değiştiren programlar bu erişimi sıraya koymalıdır; bunun için veriyi kanal işlemleriyle ya da sync ve sync/atomic paketlerindeki araçlarla koruyun. Belge, programınızın davranışını anlamak için bellek modelinin geri kalanını okumanız gerekiyorsa fazla zekice kod yazdığınızı da ekler.
En bilinen örnek korumasız bir sayaçtır. sayac++ tek bir işlem gibi görünse de okuma, artırma ve yazma adımlarından oluşur; iki goroutine aynı değeri okuyup aynı sonucu yazınca artışlardan biri kaybolur. Aşağıdaki program bu yüzden her çalıştırmada farklı bir sayı yazdırabilir; bizim denemelerimizde 1000 yerine 986 ile 997 arasında değerler çıktı:
package main
import (
"fmt"
"sync"
)
func main() {
sayac := 0
var wg sync.WaitGroup
for range 1000 {
wg.Go(func() {
sayac++ // VERİ YARIŞI: oku-artır-yaz atomik değil
})
}
wg.Wait()
fmt.Println(sayac)
}
// Çıktı (her çalıştırmada değişebilir):
// 991Race detector belgesi tipik yarışları da sıralar: Go 1.22 öncesinde döngü sayacını kapanışta paylaşmak, iki goroutine'in yanlışlıkla aynı err değişkenine yazması (çözüm goroutine içinde := ile yeni değişken tanımlamak), korumasız global bir map'e eşzamanlı okuma ve yazma, int64 gibi basit tiplerdeki korumasız erişimler ve aynı kanaldaki eşitlenmemiş gönderme ile kapatma. Map'lerin eşzamanlı okuma ve yazmaya karşı güvenli olmadığını belge açıkça belirtir; korumalı map için sync.Mutex yazısına bakabilirsiniz.
Neden "zararsız" veri yarışı yoktur?
Bellek modeli, yarış içermeyen programlara güçlü bir garanti verir: bu programlar, tüm goroutine'ler tek bir işlemciye çoklanmış gibi, sıralı tutarlı (sequentially consistent) davranır. Belge bu özelliği DRF-SC olarak adlandırır. Yarış içeren programlarda ise bu garanti kalkar. Belgedeki örnekte bir goroutine önce a = 1, sonra b = 2 yazar; başka bir goroutine önce b'yi, sonra a'yı okur ve 2 ile 0 görebilir. Yani bir yazmanın etkisini görmeniz, ondan önce yapılan yazmaların etkisini de göreceğiniz anlamına gelmez.
Bu durum yaygın bazı kalıpları geçersiz kılar. Bir bayrağı senkronizasyonsuz okuyup true görünce verinin hazır olduğunu varsaymak (double-checked locking) yanlıştır. Bir bayrak değişene kadar boş döngüde beklemek de yanlıştır; belgeye göre yazmanın hiç görülmeyeceği ve döngünün hiç bitmeyeceği bile mümkündür. Daha da ciddisi, birden fazla makine kelimesi büyüklüğündeki değerlerde yarış tutarsız değerler üretebilir. Çoğu Go uygulamasında arayüz değerleri, map'ler, dilimler ve string'ler iç (işaretçi, uzunluk) ya da (işaretçi, tip) çiftlerine dayandığı için, bu değerlerdeki yarışlar belgeye göre keyfi bellek bozulmasına yol açabilir.
Go yine de C ve C++'tan farklı bir yol izler: yarışlı programlarda bile kelime büyüklüğündeki bir okumanın gerçekten yazılmış bir değeri görmesi gerekir ve "havadan" değerler ortaya çıkamaz. Belge bu kısıtların Go'yu bu açıdan Java ve JavaScript'e yaklaştırdığını, ancak yarışların yine de hata olduğunu ve araçlarla teşhis edilmesi gerektiğini vurgular. Bir uygulama, yarışı tespit ettiğinde raporlayıp programı durdurmakta da serbesttir.
"Önce olur" ilişkisi ve senkronizasyon noktaları

Bir goroutine'deki yazmanın başka bir goroutine'den güvenle okunabilmesi için ikisi arasında "önce olur" (happens before) ilişkisi kurulmalıdır. Bu ilişki, aynı goroutine içindeki sıralama ile goroutine'ler arasındaki eşitleme (synchronized before) olaylarının birleşiminden oluşur. Bellek modeli belgesi bu eşitleme olaylarını tek tek sayar:
| Olay | Neyden önce eşitlenir |
|---|---|
| go deyimi | Başlattığı goroutine'in çalışmaya başlaması |
| Kanala gönderme | Karşılık gelen almanın tamamlanması |
| Kanalın kapatılması | Kapalı olduğu için sıfır değer döndüren alma |
| Tamponsuz kanaldan alma | Karşılık gelen göndermenin tamamlanması |
| Kapasitesi C olan kanalda k'ıncı alma | k+C'inci göndermenin tamamlanması |
| n'inci Unlock | m'inci Lock'un dönüşü (n < m) |
| once.Do(f) içinde f'nin bitişi | Herhangi bir once.Do(f) çağrısının dönüşü |
| Atomik işlem A'nın etkisini gören B | A, B'den önce eşitlenir |
Goroutine'in sona ermesi ise hiçbir olaydan önce eşitlenmez; bu yüzden bir goroutine'in sonucunu görmek için mutlaka bu tablodaki mekanizmalardan birini kullanmalısınız. Aşağıdaki örnekte yazma, kanalın kapatılmasından önce sıralanmıştır; kapatma, onu gören almadan önce eşitlenir; alma da yazdırmadan önce sıralanmıştır. Bu zincir, yazdırmanın yazmayı görmesini garanti eder:
package main
import "fmt"
var mesaj string
func hazirla(bitti chan<- struct{}) {
mesaj = "merhaba, dünya" // 1) yazma
close(bitti) // 2) kapatma, alma işleminden önce eşitlenir
}
func main() {
bitti := make(chan struct{})
go hazirla(bitti)
<-bitti // 3) kapanışı gören alma
fmt.Println(mesaj) // 4) yazmanın etkisini görmesi garanti
}
// Çıktı:
// merhaba, dünyasync/atomic tipleri

sync/atomic, senkronizasyon algoritmaları yazmak için düşük seviyeli atomik bellek işlemleri sağlar. Paket belgesi bunların doğru kullanımının büyük dikkat gerektirdiğini ve özel, düşük seviyeli uygulamalar dışında senkronizasyonun kanallar ya da sync paketiyle yapılmasının daha iyi olduğunu söyler. Bir sayaç, bir durum bayrağı ya da bir yapılandırma işaretçisi gibi tek değerlik paylaşımlarda ise atomik tipler en sade çözümdür.
Go 1.19 sürüm notlarına göre pakete Bool, Int32, Int64, Uint32, Uint64, Uintptr ve Pointer tipleri eklendi. Bu tipler atomik API'yi kullanmayı kolaylaştırır ve değişkene yanlışlıkla atomik olmayan biçimde erişmenizi engeller. Hepsinin sıfır değeri kullanıma hazırdır ve hiçbiri ilk kullanımdan sonra kopyalanmamalıdır. Temel metotlar Load, Store, Swap ve CompareAndSwap'tır; tamsayı tiplerinde Add da vardır. Go 1.23 ile tamsayı tiplerine bit düzeyinde And ve Or eklendi.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var sayac atomic.Int64 // sıfır değeri 0, kullanıma hazır
var wg sync.WaitGroup
for range 1000 {
wg.Go(func() {
sayac.Add(1)
})
}
wg.Wait()
fmt.Println(sayac.Load())
fmt.Println(sayac.Swap(5)) // eski değeri döndürür
fmt.Println(sayac.CompareAndSwap(5, 10)) // 5 ise 10 yap
fmt.Println(sayac.CompareAndSwap(5, 20)) // artık 5 değil
fmt.Println(sayac.Load())
}
// Çıktı:
// 1000
// 1000
// true
// false
// 10Belge her işlemin anlamını sıradan kodla tarif eder. Swap, eski değeri okuyup yenisini yazar ve eskisini döndürür. CompareAndSwap, değer beklenen eski değere eşitse yenisini yazar ve true döndürür, değilse hiçbir şey yapmadan false döndürür. Add, değeri artırıp yeni değeri döndürür. Fark şudur: bu adımlar atomik olarak, araya başka bir goroutine giremeden gerçekleşir. Bellek modeli açısından bir atomik işlemin etkisini gören başka bir atomik işlem, ondan sonra eşitlenmiş sayılır ve programdaki bütün atomik işlemler sıralı tutarlı bir sırada yürütülmüş gibi davranır. Belgeye göre bu anlam C++'ın sıralı tutarlı atomikleri ve Java'nın volatile değişkenleriyle aynıdır; Go 1.19 sürüm notları da Go'nun daha gevşek bellek sıralama biçimleri sunmadığını belirtir.
CompareAndSwap döngüsü
Add ile ifade edilemeyen güncellemeler, örneğin "en büyük değeri tut", karşılaştır-değiştir döngüsüyle yazılır: güncel değeri okursunuz, yeni değeri hesaplarsınız ve CompareAndSwap ile yazmayı denersiniz. Araya başka bir goroutine girip değeri değiştirdiyse CompareAndSwap false döner ve baştan denersiniz:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// enBuyugeGuncelle, v daha büyükse en yüksek değeri kilitsiz günceller.
func enBuyugeGuncelle(enb *atomic.Int64, v int64) {
for {
eski := enb.Load()
if v <= eski {
return
}
if enb.CompareAndSwap(eski, v) {
return
}
// Başka goroutine araya girdi: yeniden dene.
}
}
func main() {
var enb atomic.Int64
var wg sync.WaitGroup
for _, v := range []int64{17, 3, 99, 42, 58} {
wg.Go(func() { enBuyugeGuncelle(&enb, v) })
}
wg.Wait()
fmt.Println(enb.Load())
}
// Çıktı:
// 99Bu kalıp yalnızca tek bir değeri güncellediğiniz sürece işe yarar. İki ayrı atomik değişkeni birlikte tutarlı tutmanız gerekiyorsa, örneğin toplam ve adet, her biri ayrı ayrı atomik olsa da ikisinin arasındaki tutarlılık korunmaz; bu durumda mutex kullanın.
atomic.Bool ile tek seferlik geçiş
atomic.Bool'un sıfır değeri false'tur. CompareAndSwap(false, true) çağrısı, birçok goroutine arasından durumu yalnızca bir kez değiştiren goroutine'i seçmenin kilitsiz yoludur. Aşağıda beş goroutine kapatma işini üstlenmeye çalışıyor, yalnızca biri başarılı oluyor:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var kapandi atomic.Bool
var ilkKapatan atomic.Int32
var wg sync.WaitGroup
for range 5 {
wg.Go(func() {
// Yalnızca false'tan true'ya ilk geçişi yapan kazanır.
if kapandi.CompareAndSwap(false, true) {
ilkKapatan.Add(1)
}
})
}
wg.Wait()
fmt.Println("kapandı:", kapandi.Load(), "kapatma sayısı:", ilkKapatan.Load())
}
// Çıktı:
// kapandı: true kapatma sayısı: 1Paket belgesi, birçok mimaride kelime büyüklüğünde olmayan tamsayılarda atomik işlemlerin verimsiz ya da imkânsız olduğunu, bu yüzden yalnızca birkaç tamsayı boyutunun desteklendiğini ve bir Bool'un yerleşik bool'dan daha büyük olabileceğini not eder. Tek seferlik bir başlatma için ise sync.Once daha uygundur, çünkü diğer goroutine'leri başlatma bitene kadar bekletir.
atomic.Pointer ve atomic.Value ile yapılandırma değiştirmek
Okunması çok sık, değişmesi seyrek bir yapılandırmayı paylaşmanın bir yolu, onu değişmez bir nesne olarak tutup işaretçisini atomik olarak değiştirmektir. atomic.Pointer[T], *T tipinde atomik bir işaretçidir ve sıfır değeri nil işaretçidir. Okuyucular Load ile güncel nesneyi kilitsiz alır; yazar yeni bir nesne kurar ve Store ya da Swap ile tek adımda değiştirir. Eski nesneyi kullanan okuyucular işlerini bitirince çöp toplayıcı onu temizler:
package main
import (
"fmt"
"sync/atomic"
)
type Ayar struct {
Surum int
Mod string
}
func main() {
var guncel atomic.Pointer[Ayar] // sıfır değeri nil *Ayar
fmt.Println(guncel.Load() == nil)
guncel.Store(&Ayar{Surum: 1, Mod: "normal"})
a := guncel.Load() // okuyucular kilitsiz okur
fmt.Println(a.Surum, a.Mod)
// Yazar: yeni nesne kur, tek seferde değiştir. Eski nesneye dokunma.
eski := guncel.Swap(&Ayar{Surum: 2, Mod: "bakım"})
fmt.Println("eski:", eski.Surum, "yeni:", guncel.Load().Mod)
}
// Çıktı:
// true
// 1 normal
// eski: 1 yeni: bakımBu desenin tek kuralı, yayımlanmış bir nesneyi bir daha değiştirmemektir. Load ile aldığınız işaretçinin gösterdiği alanlara yazarsanız, aynı nesneyi okuyan diğer goroutine'lerle yeniden veri yarışı oluşturursunuz.
Daha eski kodlarda aynı iş için atomic.Value kullanılır. Value herhangi bir değeri saklar ama bütün Store çağrılarının aynı somut tipi kullanması gerekir; farklı tiple ya da nil ile Store panic eder. Hiç Store yapılmamışsa Load nil döndürür:
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var v atomic.Value
fmt.Println(v.Load()) // Store yoksa nil
v.Store("ilk")
fmt.Println(v.Load().(string))
v.Store(42) // farklı somut tip: panic
}
// Çıktı (ilk satırlar):
// <nil>
// ilk
// panic: sync/atomic: store of inconsistently typed value into ValuePaket belgesindeki "readMostly" örneği, Value'nun yazarlar arasında bir mutex ile birlikte kullanıldığı yazma-sırasında-kopyalama (copy-on-write) map desenini gösterir: yazar mevcut map'i kopyalar, kopyayı günceller ve atomik olarak yerine koyar. atomic.Pointer[T] aynı işi tip güvenli biçimde, tip doğrulaması gerektirmeden yapar.
Eski fonksiyonlar ve Go 1.27 go fix
Atomik tiplerden önce paket AddInt64, LoadInt64, CompareAndSwapInt32 gibi düz değişkenin adresini alan fonksiyonlar sunuyordu. Bu fonksiyonlar hâlâ geçerlidir; ancak değişken sıradan bir tamsayı olduğu için kodun bir yerinde yanlışlıkla atomik olmayan erişim yapmak mümkündür. Paket belgesi her fonksiyon için daha ergonomik ve hataya daha az açık olan tip metodunu önerir, 64 bitlik fonksiyonlarda bunu özellikle 32 bitlik platformları hedefliyorsanız vurgular. Go 1.19 sürüm notlarına göre Int64 ve Uint64 tipleri, yapılarda ya da ayrılmış verilerde 32 bitlik sistemlerde bile otomatik olarak 64 bit hizalanır:
package main
import (
"fmt"
"sync/atomic"
)
type istatistik struct {
istek int64 // düz int64: her erişim atomic fonksiyonla yapılmalı
hata atomic.Int64
}
func main() {
var s istatistik
atomic.AddInt64(&s.istek, 3)
fmt.Println(atomic.LoadInt64(&s.istek))
s.hata.Add(2) // tip tabanlı API: yanlışlıkla düz erişim yapılamaz
fmt.Println(s.hata.Load())
}
// Çıktı:
// 3
// 2Go 1.27 sürüm notlarına göre go fix komutuna eklenen yeni modernleştiriciler arasında atomictypes da bulunuyor. Komutun genel kullanımı için go fmt, vet, doc ve fix araçları yazısına bakabilirsiniz.
Race detector: -race bayrağı
Go'nun yerleşik veri yarışı dedektörü, go komutuna -race bayrağı eklenerek kullanılır. Belgedeki kullanım biçimleri şunlardır:
go test -race mypkg # paketi test et
go run -race mysrc.go # kaynak dosyayı çalıştır
go build -race mycmd # komutu derle
go install -race mypkg # paketi kurDedektör bir yarış bulduğunda WARNING: DATA RACE başlığıyla bir rapor yazar. Raporda çakışan erişimlerin yığın izleri (hangi goroutine'in hangi satırda okuduğu ve önceki yazmayı hangi goroutine'in yaptığı) ve ilgili goroutine'lerin oluşturulduğu yerler bulunur. GORACE ortam değişkeniyle seçenek verilir: log_path raporun yazılacağı yeri, exitcode (varsayılan 66) yarış bulunduktan sonraki çıkış kodunu, halt_on_error ilk yarıştan sonra programın durup durmayacağını, strip_path_prefix dosya yollarından atılacak öneki belirler. -race ile derlerken go komutu ayrıca race derleme etiketini tanımlar; bu etiketle belirli testleri dedektör altında dışarıda bırakabilirsiniz (build constraint yazısı).
Belgede dikkat edilmesi gereken birkaç sınır vardır. Dedektör yalnızca çalışma sırasında gerçekten oluşan yarışları bulur; çalışmayan kod yollarındaki yarışları göremez. Bu yüzden testlerinizin kapsamı düşükse, -race ile derlenmiş bir programı gerçekçi bir yük altında çalıştırmak daha fazla yarış ortaya çıkarabilir. Maliyet programa göre değişir; belge tipik bir programda bellek kullanımının 5 ila 10 kat, çalışma süresinin 2 ila 20 kat artabileceğini belirtir. Dedektör cgo'nun etkin olmasını ve Darwin dışındaki sistemlerde kurulu bir C derleyicisini gerektirir; desteklenen platformlar arasında linux/amd64, linux/arm64, darwin/arm64 ve windows/amd64 bulunur. Windows'ta Go 1.21'den itibaren mingw-w64 çalışma zamanı kütüphanelerinin 8 veya üstü sürümünü içeren bir C derleyicisi gerekir.
Atomic mi, mutex mi, kanal mı?

| Durum | Önerilen araç | Neden |
|---|---|---|
| Tek sayaç ya da istatistik | atomic.Int64 | Kilitsiz, kısa, kopyalanmaz |
| Tek seferlik durum geçişi | atomic.Bool CompareAndSwap | Kazananı kilitsiz seçer |
| Seyrek değişen yapılandırma | atomic.Pointer[T] | Okuyucular kilitsiz, nesne değişmez |
| Birlikte tutarlı birden çok alan | sync.Mutex | Atomikler alanlar arası tutarlılık sağlamaz |
| Map'e eşzamanlı erişim | map + sync.Mutex ya da RWMutex | Map eşzamanlı yazmaya güvenli değil |
| Veri sahipliğini devretmek | Kanal | Aynı anda tek sahip |
| Tek seferlik başlatma | sync.Once / OnceValue | Diğerleri başlatmayı bekler |
Sık yapılan hatalar
- "Sadece bir int, zarar vermez" düşüncesi: Basit tiplerdeki yarışlar da derleyici optimizasyonları ve yeniden sıralama yüzünden hata üretir; race detector belgesi bunu ayrıca örnekler.
- Atomik değişkene bir yerde düz erişmek: Eski fonksiyon API'sinde
x++ileatomic.AddInt64(&x, 1)karışırsa yarış geri gelir. Tip tabanlı API bunu engeller. - Boş döngüde bayrak beklemek: Bellek modeline göre döngünün bitmesi bile garanti değildir; kanal ya da
syncaraçlarıyla bekleyin. - İki atomik değeri tek işlem sanmak: Her biri ayrı ayrı atomiktir; birlikte tutarlılık için mutex gerekir.
- Yayımlanmış nesneyi değiştirmek:
atomic.Pointerile paylaşılan nesneyi yerinde güncellemek yeni bir yarıştır. - atomic.Value'ya farklı tipler koymak: İlk
Store'dan farklı somut tip panic üretir. - Atomik tipleri kopyalamak:
atomic.Int64içeren yapıları değerle geçirmeyin; belge kopyalamayı yasaklar. - Race detector'ü yalnızca bir kez çalıştırmak: Yalnızca o çalışmada gerçekleşen yarışları bulur; düzenli çalıştırın.
Sık Sorulan Sorular
Race detector temiz çıktı verdiyse programım yarışsız mıdır?
Hayır. Belgeye göre dedektör yalnızca çalışma sırasında gerçekleşen yarışları bulur. Çalışmayan kod yolları ya da o çalıştırmada denk gelmeyen zamanlamalar raporlanmaz. Kapsamı yüksek testler ve gerçekçi yük altında çalıştırma güveni artırır.
atomic işlemler mutex'ten her zaman hızlı mıdır?
Belgeler böyle bir karşılaştırma vermez. Doğru soru performans değil doğruluktur: tek bir değeri güncelliyorsanız atomik tip daha sade kod üretir, birden çok değerin birlikte tutarlı kalması gerekiyorsa mutex gerekir. Performans önemliyse benchmark ile ölçün.
-race bayrağını Windows'ta kullanabilir miyim?
Evet, windows/amd64 desteklenen platformlar arasındadır. Ancak cgo'nun etkin olması ve Go 1.21'den itibaren mingw-w64 çalışma zamanı kütüphanelerinin 8 veya üstü sürümünü içeren bir C derleyicisinin kurulu olması gerekir.
Go'da gevşek (relaxed) atomik işlemler var mı?
Hayır. Go 1.19 sürüm notları Go'nun yalnızca sıralı tutarlı atomikler sunduğunu, diğer dillerdeki daha gevşek biçimleri sunmadığını belirtir.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Go öğrenme rehberi · sync.Mutex ve WaitGroup · Go goroutine · Go channel · Go test yazma · benchmark ve fuzz testi · Go sık karşılaşılan hatalar