Go'nun sync paketi, birden çok goroutine'in aynı veriye güvenle erişmesini ve birbirini beklemesini sağlayan temel araçları içerir. Paylaşılan bir değişkeni korumak için sync.Mutex (ya da çok okuyuculu durumlar için sync.RWMutex), bir grup goroutine'in bitmesini beklemek için sync.WaitGroup, bir işi programın ömrü boyunca yalnızca bir kez yapmak için sync.Once ve onun fonksiyon biçimleri OnceFunc, OnceValue, OnceValues kullanılır. Go 1.25 ile gelen WaitGroup.Go, goroutine başlatma ve sayma işini tek çağrıya indirir.
Bu yazıda bu tiplerin doğru kullanımını, bellek modeli açısından ne garanti ettiklerini, kilit kopyalama ve yanlış yerde Add gibi hataları go vet'in nasıl yakaladığını ve sync.Map, sync.Cond, sync.Pool'un hangi durumlarda anlamlı olduğunu Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Goroutine ve kanal temelleri için goroutine ve channel yazılarına, seri için Go öğrenme rehberi sayfasına bakabilirsiniz.
sync paketi ne zaman kullanılır?

Paket belgesi açık bir yönlendirmeyle başlar: Once ve WaitGroup dışındaki tiplerin çoğu düşük seviyeli kütüphane kodu içindir; daha üst seviyeli senkronizasyonu kanallar ve iletişimle kurmak daha iyidir. Bu, mutex'in kötü olduğu anlamına gelmez. Effective Go da bir referans sayacını korumak için tamsayının çevresine mutex koymanın en iyi yol olabileceğini söyler. Pratik bir ayrım şöyledir: verinin sahipliği bir goroutine'den diğerine geçiyorsa kanal, birden çok goroutine aynı yapının iç durumunu kısa süreliğine okuyup değiştiriyorsa mutex daha sade kod üretir.
Paketteki bütün tipler için ortak bir kural vardır: bu tipleri içeren değerler kopyalanmamalıdır. Mutex, RWMutex, WaitGroup, Once, Cond, Map ve Pool belgelerinin her birinde "ilk kullanımdan sonra kopyalanmamalıdır" uyarısı bulunur. Kopya, kendi bağımsız kilit durumunu taşır; iki goroutine farklı kopyaları kilitlediğinde aslında hiçbir şeyi korumamış olursunuz.
sync.Mutex: karşılıklı dışlama
Mutex'in sıfır değeri kilitsiz bir mutex'tir, ayrıca başlatmanız gerekmez. Lock kilidi alır; kilit başka bir goroutine'deyse çağıran goroutine kilit boşalana kadar bekler. Unlock kilidi bırakır. Kilitle korunan kod parçasına kritik bölge denir ve bu bölgeye aynı anda yalnızca bir goroutine girebilir. Tour of Go'daki sayaç örneğinin uyarlaması şöyle:
package main
import (
"fmt"
"sync"
)
// SafeCounter eşzamanlı kullanım için güvenlidir.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock() // fonksiyon nereden dönerse dönsün kilit açılır
c.v[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
var wg sync.WaitGroup
for range 1000 {
wg.Go(func() { c.Inc("ziyaret") })
}
wg.Wait()
fmt.Println(c.Value("ziyaret"))
}
// Çıktı:
// 1000defer c.mu.Unlock() kalıbı, fonksiyon hangi yoldan dönerse dönsün, panic olsa bile kilidin bırakılmasını sağlar. Mutex yapı alanı olarak tanımlandığı ve metotlar işaretçi alıcı (*SafeCounter) kullandığı için tüm goroutine'ler aynı mutex'i paylaşır. Kilidin kapsamı, korunan veriyle aynı yerde durmalıdır; mutex alanını koruduğu alanların hemen üstüne yazmak yaygın bir alışkanlıktır.
Bellek modeli mutex'in ne garanti ettiğini tanımlar: n'inci Unlock çağrısı, n < m olmak üzere m'inci Lock çağrısının dönüşünden önce eşitlenir. Yani bir goroutine kilidi bırakmadan önce yaptığı yazmalar, kilidi daha sonra alan goroutine tarafından mutlaka görülür. Belge iki ayrıntıyı daha not eder. Kilitli bir mutex belirli bir goroutine'e bağlı değildir; bir goroutine kilitleyip başka bir goroutine açabilir. Kilitli olmayan bir mutex'i açmak ise çalışma zamanı hatasıdır; Go 1.27.3 bu durumda programı fatal error ile sonlandırır:
package main
import "sync"
func main() {
var mu sync.Mutex
mu.Unlock() // kilitli değilken açmak
}
// Çıktı (ilk satırlar):
// fatal error: sync: unlock of unlocked mutexTryLock kilidi almayı dener ve başarılı olup olmadığını döndürür. Belge, doğru kullanımlarının bulunduğunu ama nadir olduğunu, TryLock ihtiyacının çoğu zaman mutex kullanımındaki daha derin bir sorunun işareti olduğunu söyler. Başarısız bir TryLock hiçbir eşitleme ilişkisi kurmaz.
Kilidi kopyalamak ve go vet
Mutex içeren bir yapıyı değer olarak bir fonksiyona geçirmek ya da değer alıcılı bir metot tanımlamak, yapıyı ve içindeki mutex'i kopyalar. Derleyici bunu hata saymaz ama go vet uyarır. Aşağıdaki modülde vet'in Go 1.27.3 ile ürettiği gerçek çıktıyı görüyorsunuz:
package main
import (
"fmt"
"sync"
)
type Sayac struct {
mu sync.Mutex
n int
}
func yazdir(s Sayac) { // YANLIŞ: Mutex değerle kopyalanıyor
fmt.Println(s.n)
}
func main() {
var s Sayac
yazdir(s)
}go mod init ornek/vet_kopya
go vet ./..../main.go:13:15: yazdir passes lock by value: ornek/vet_kopya.Sayac contains sync.Mutex
./main.go:19:9: call of yazdir copies lock value: ornek/vet_kopya.Sayac contains sync.MutexÇözüm, yapıyı işaretçiyle geçirmek (func yazdir(s *Sayac)) ve mutex taşıyan tiplerin metotlarında işaretçi alıcı kullanmaktır. Değer ve işaretçi alıcı farkı için metotlar ve alıcı tipleri yazısına bakabilirsiniz. go vet'in genel kullanımı fmt, vet ve doc araçları yazısında anlatılıyor.
sync.RWMutex: çok okuyucu, tek yazıcı

RWMutex kilidin ya istenen sayıda okuyucu ya da tek bir yazıcı tarafından tutulmasına izin verir. Okuma yapan kod RLock ve RUnlock, yazma yapan kod Lock ve Unlock kullanır. Okumaların yazmalardan çok daha sık olduğu bir önbellek tipik kullanım alanıdır:
package main
import (
"fmt"
"sync"
)
type Onbellek struct {
mu sync.RWMutex
veri map[string]string
}
func (o *Onbellek) Al(k string) (string, bool) {
o.mu.RLock() // birden çok okuyucu aynı anda girebilir
defer o.mu.RUnlock()
v, ok := o.veri[k]
return v, ok
}
func (o *Onbellek) Koy(k, v string) {
o.mu.Lock() // yazıcı tek başına girer
defer o.mu.Unlock()
o.veri[k] = v
}
func main() {
o := &Onbellek{veri: map[string]string{}}
o.Koy("dil", "Go")
var wg sync.WaitGroup
sonuc := make([]string, 4)
for i := range 4 {
wg.Go(func() {
v, _ := o.Al("dil")
sonuc[i] = v
})
}
wg.Wait()
fmt.Println(sonuc)
}
// Çıktı:
// [Go Go Go Go]Belgedeki önemli kural yazıcının aç kalmamasıyla ilgilidir: kilit bir ya da daha fazla okuyucudayken bir goroutine Lock çağırırsa, sonradan gelen RLock çağrıları yazıcı kilidi alıp bırakana kadar bekler. Bu yüzden aynı goroutine'in okuma kilidini tutarken tekrar RLock çağırması (özyinelemeli okuma kilidi) yasaktır; arada bir yazıcı beklemeye başlarsa kilitlenme oluşur. Okuma kilidi yazma kilidine yükseltilemez, yazma kilidi de okuma kilidine düşürülemez. Okuma kilidinin içinde yazma yapmanız gerekiyorsa önce RUnlock, sonra Lock çağırmalı ve aradaki sürede verinin değişmiş olabileceğini hesaba katmalısınız.
sync.WaitGroup ve Go 1.25 WaitGroup.Go
WaitGroup, bir grup goroutine ya da görevin bitmesini beklemek için kullanılan sayaçlı bir semafordur. Go 1.25 sürüm notlarına göre yeni WaitGroup.Go metodu, goroutine oluşturup sayma kalıbını kolaylaştırır: wg.Go(f) f'yi yeni bir goroutine'de çağırır ve görevi gruba ekler; f döndüğünde görev gruptan çıkar. Wait sayaç sıfırlanana kadar bekler. Belgeye göre f'nin dönüşü, onun açtığı Wait çağrısının dönüşünden önce eşitlenir; Wait'ten sonra goroutine'lerin yazdığı sonuçları güvenle okuyabilirsiniz. Yukarıdaki mutex ve RWMutex örnekleri de bu yöntemi kullanıyor.
WaitGroup.Go öncesinde yazılmış kodlarda Add ve Done kalıbını görürsünüz: goroutine'den önce wg.Add(1), goroutine içinde defer wg.Done(). Belge yeni kod için Go'yu tercih etmenizi önerir ve Add için şu kuralları koyar: sayaç sıfırken yapılan pozitif Add çağrıları Wait'ten önce gerçekleşmelidir; bu da pratikte Add'in goroutine'i başlatan satırdan önce çalışması demektir. Grup birden çok bağımsız iş kümesi için yeniden kullanılıyorsa yeni Add çağrıları önceki bütün Wait çağrıları döndükten sonra yapılmalıdır. Sayaç negatife düşerse Add panic eder:
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
wg.Done()
wg.Done() // sayaç negatife düşer
}
// Çıktı (ilk satırlar):
// panic: sync: negative WaitGroup counterAdd'i goroutine içine koymak sinsi bir hatadır: Wait, goroutine'ler daha Add'e ulaşmadan çalışırsa sayaç sıfır olduğu için hemen döner. Go 1.25 ile go vet'e yanlış yerdeki WaitGroup.Add çağrılarını bildiren waitgroup denetimi eklendi:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := range 3 {
go func() {
wg.Add(1) // YANLIŞ: Add goroutine içinde
defer wg.Done()
fmt.Println(i)
}()
}
wg.Wait()
}./main.go:12:10: WaitGroup.Add called from inside new goroutineWaitGroup.Go bu hatayı yapısal olarak imkânsız kılar. Belgeye göre Go'ya verilen fonksiyon panic etmemelidir; grup boşken Go, Wait'ten önce çağrılmalıdır, grup boş değilse her an çağrılabilir. Bu da Go ile başlatılan bir goroutine'in kendisinin de Go çağırabileceği anlamına gelir. Go 1.27 sürüm notlarında go fix içindeki waitgroup modernleştiricisinin adının waitgroupgo olarak değiştirildiği belirtilir.
sync.Once, OnceFunc, OnceValue ve OnceValues
Once tam olarak bir kez yapılacak bir eylemi temsil eder. once.Do(f) kaç kez ve kaç goroutine'den çağrılırsa çağrılsın f yalnızca ilk çağrıda çalışır; diğer çağrılar f dönene kadar bekler. Bellek modeli, f'nin tamamlanmasının herhangi bir once.Do(f) çağrısının dönüşünden önce eşitlendiğini garanti eder, bu yüzden Do döndükten sonra f'nin hazırladığı her şey görünürdür. Go 1.21 ile gelen OnceFunc, OnceValue ve OnceValues, bu kalıbın en yaygın kullanımını, yani bir değeri ilk ihtiyaç anında tembel biçimde hazırlamayı, tek fonksiyonla sarar:
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
var wg sync.WaitGroup
for range 5 {
wg.Go(func() {
once.Do(func() { fmt.Println("yalnızca bir kez") })
})
}
wg.Wait()
ayarYukle := sync.OnceValue(func() map[string]string {
fmt.Println("ayarlar yükleniyor")
return map[string]string{"port": "8080"}
})
fmt.Println(ayarYukle()["port"])
fmt.Println(ayarYukle()["port"]) // ikinci çağrı önbellekten
oku := sync.OnceValues(func() (int, error) {
fmt.Println("hesaplanıyor")
return 42, nil
})
n, err := oku()
fmt.Println(n, err)
n, err = oku()
fmt.Println(n, err)
}
// Çıktı:
// yalnızca bir kez
// ayarlar yükleniyor
// 8080
// 8080
// hesaplanıyor
// 42 <nil>
// 42 <nil>Belgede dikkat edilmesi gereken üç ayrıntı vardır. f içinden aynı Once üzerinde tekrar Do çağrılırsa, hiçbir Do çağrısı f dönmeden dönmediği için kilitlenme olur. f panic ederse Do onu dönmüş sayar ve sonraki çağrılar f'yi tekrar denemez. OnceFunc, OnceValue ve OnceValues'ta ise f panic ederse döndürülen fonksiyon her çağrıda aynı değerle panic eder. Hata dönen bir başlatmayı yeniden denemek istiyorsanız Once doğru araç değildir.
Bellek modeli belgesi Once'ın yerine kullanılan "önce bayrağa bak, sonra kilitle" (double-checked locking) kalıbının neden yanlış olduğunu da gösterir: senkronizasyonsuz okunan bir bayrağın true görülmesi, bayraktan önce yazılan verinin de görüleceği anlamına gelmez. Ayrıntılar veri yarışı yazısında.
sync.Map ne zaman kullanılır?
sync.Map, birden çok goroutine'in ek kilit olmadan kullanabildiği, map[any]any benzeri bir yapıdır ve sıfır değeri kullanıma hazırdır. Belge onu açıkça özelleşmiş bir araç olarak tanımlar: çoğu kod, tip güvenliği ve map içeriğiyle birlikte başka değişmezleri korumanın kolaylığı için ayrı bir kilitle korunan sıradan bir map kullanmalıdır. sync.Map'in iki hedef senaryosu vardır: bir anahtarın bir kez yazılıp çok kez okunduğu, yalnızca büyüyen önbellekler ve birden çok goroutine'in ayrık anahtar kümelerini okuyup yazdığı durumlar.
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map // sıfır değeri kullanıma hazır
v, yuklendi := m.LoadOrStore("tr", "Türkçe")
fmt.Println(v, yuklendi) // yoktu: kaydedildi
v, yuklendi = m.LoadOrStore("tr", "Turkish")
fmt.Println(v, yuklendi) // vardı: eski değer döner
deger, ok := m.Load("tr")
fmt.Println(deger.(string), ok) // değer any: tip doğrulaması gerekir
m.Delete("tr")
_, ok = m.Load("tr")
fmt.Println(ok)
}
// Çıktı:
// Türkçe false
// Türkçe true
// Türkçe true
// falseDeğerler any olarak saklandığı için okurken tip doğrulaması gerekir. Sürüm notlarına göre Go 1.20'de Swap, CompareAndSwap ve CompareAndDelete, Go 1.23'te Clear eklendi; Go 1.24'te ise uygulama değiştirilerek özellikle değişiklik işlemlerinde performans iyileştirildi. Sıradan map kullanımı için Go map yazısına bakabilirsiniz.
sync.Cond ve sync.Pool kısaca
sync.Cond, bir olayı bekleyen ya da duyuran goroutine'ler için bir buluşma noktasıdır. Wait, ilişkili kilidi atomik olarak bırakıp goroutine'i askıya alır ve uyandığında kilidi yeniden alır; bu yüzden koşul bir döngü içinde yeniden kontrol edilmelidir. Belge, basit kullanımların çoğunda kanalların daha iyi olacağını ve Broadcast'in bir kanalı kapatmaya, Signal'in bir kanala göndermeye karşılık geldiğini söyler.
sync.Pool, ayrılmış ama kullanılmayan geçici nesneleri yeniden kullanmak ve çöp toplayıcı üzerindeki baskıyı azaltmak için vardır. Havuzdaki bir nesne her an, haber verilmeden silinebilir; Get havuzu boş sayıp New'i çağırmayı seçebilir. Belgedeki iyi kullanım örneği fmt paketinin geçici çıktı tamponlarıdır. Kısa ömürlü bir nesnenin parçası olan serbest liste ise uygun bir kullanım değildir; New fonksiyonunun genellikle işaretçi tipi döndürmesi önerilir.
Hangi araç ne için?

| Araç | Kullanım | Önemli kural |
|---|---|---|
| Mutex | Paylaşılan durumu korumak | Kopyalanmaz; kilitsizken Unlock ölümcül hata |
| RWMutex | Yoğun okuma, seyrek yazma | Özyinelemeli RLock yok, yükseltme yok |
| WaitGroup.Go | Goroutine grubunu başlatıp beklemek | f panic etmemeli; Go 1.25+ |
| WaitGroup Add/Done | Eski kod, goroutine dışı görevler | Add, go satırından önce |
| Once | Tek seferlik başlatma | f içinden Do çağırmak kilitlenir |
| OnceValue/OnceValues | Tembel hesaplanan değer | Go 1.21+; panic her çağrıda tekrarlanır |
| sync.Map | Yalnızca büyüyen önbellek, ayrık anahtarlar | Değerler any; çoğu durumda map + Mutex |
| Cond | Koşul bekleme | Çoğu durumda kanal daha basit |
| Pool | Geçici nesneleri yeniden kullanmak | Nesneler her an silinebilir |
Sık yapılan hatalar
- Mutex içeren yapıyı değerle geçirmek: Kilidin kopyası hiçbir şeyi korumaz;
go vetuyarısını ciddiye alın. - Unlock'u unutmak ya da erken dönüşte atlamak: Kilidi aldığınız satırın hemen altına
defer mu.Unlock()yazın. - Kilidi tutarken uzun iş yapmak: G/Ç ya da ağ çağrısı sırasında kilidi tutmak diğer goroutine'leri bekletir; kritik bölgeyi kısa tutun.
- Aynı goroutine'de Mutex'i iki kez kilitlemek: Go mutex'i özyinelemeli değildir; ikinci
Locksonsuza kadar bekler. - Add'i goroutine içinde çağırmak:
Waiterken döner;wg.Gokullanın. - Hatalı başlatmayı Once ile tekrar denemeye çalışmak:
Dopanic ya da hata sonrasıf'yi bir daha çalıştırmaz. - Her map için sync.Map seçmek: Tip güvenliği kaybolur; belge çoğu durumda kilitli sıradan map önerir.
Sık Sorulan Sorular
Mutex'i başlatmam gerekir mi?
Hayır. Mutex ve RWMutex'in sıfır değeri kilitsiz, kullanıma hazır bir kilittir. Yapı alanı olarak tanımlamanız yeterlidir; önemli olan yapının kopyalanmaması ve işaretçiyle paylaşılmasıdır.
WaitGroup.Go hangi sürümde geldi, eski kodu değiştirmeli miyim?
WaitGroup.Go Go 1.25 ile geldi. Eski Add ve Done kalıbı çalışmaya devam eder; ancak belge yeni kod için Go'yu önerir ve bu metot Add'in yanlış yere konması hatasını ortadan kaldırır.
RWMutex her zaman Mutex'ten hızlı mıdır?
Belgeler böyle bir garanti vermez. RWMutex okuyucuların aynı anda girmesine izin verir, ancak bunun programınıza kazanç getirip getirmediği iş yüküne bağlıdır. Düz Mutex ile başlayın ve gerekirse benchmark ile ölçerek karar verin.
Sayaç için Mutex mi, atomic mi?
Tek bir tamsayıyı artırıp okuyorsanız sync/atomic'in Int64 gibi tipleri daha sade ve kilitsizdir. Birden çok alanın birlikte tutarlı kalması gerekiyorsa mutex kullanın. Karşılaştırma için sync/atomic yazısına bakabilirsiniz.
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 · Go goroutine · Go channel · sync/atomic ve veri yarışı · context paketi · metotlar ve alıcı tipleri · gofmt, go vet, go doc