Goroutine, Go'da bir fonksiyonun aynı adres alanındaki diğer fonksiyonlarla eşzamanlı çalışan bağımsız bir kopyasıdır. Bir fonksiyon ya da metot çağrısının önüne go anahtar kelimesini yazdığınızda Go o çağrıyı yeni bir goroutine içinde başlatır ve çağıran taraf beklemeden bir sonraki satıra geçer. Goroutine'ler hafiftir, küçük bir yığınla (stack) başlar ve çalışma zamanı (runtime) onları birden çok işletim sistemi iş parçacığına dağıtır. Dikkat etmeniz gereken asıl kural şudur: main fonksiyonu döndüğünde program biter ve diğer goroutine'lerin işini bitirmesini beklemez.

Bu yazıda go deyiminin tam olarak ne yaptığını, argümanların ne zaman değerlendirildiğini, bir goroutine'in bitişini nasıl bekleyeceğinizi, GOMAXPROCS ayarının ne anlama geldiğini, goroutine içindeki panic'in neden tüm programı düşürdüğünü ve goroutine sızıntısını nasıl önleyeceğinizi Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Go'nun genel tanıtımı için Go nedir yazısına, konular arası yol haritası için Go öğrenme rehberi sayfasına bakabilirsiniz.

Goroutine nedir, iş parçacığından farkı ne?

Goroutine nedir, iş parçacığından farkı ne?

Effective Go, bu yapıya iş parçacığı (thread), eş yordam (coroutine) ya da süreç (process) demek yerine yeni bir ad verilmesinin nedenini mevcut terimlerin yanlış çağrışımlar taşımasıyla açıklar. Model basittir: goroutine, aynı adres alanında başka goroutine'lerle eşzamanlı yürüyen bir fonksiyondur. Maliyeti yığın alanı ayırmanın biraz üzerindedir; yığın küçük başlar ve gerektikçe yığın (heap) belleğinden büyütülür ya da küçültülür. Bu yüzden bir programda binlerce goroutine çalıştırmak olağandır.

Goroutine'ler birden çok işletim sistemi iş parçacığına çoklanır (multiplexing). Bir goroutine örneğin G/Ç (I/O) beklerken bloklanırsa diğerleri çalışmaya devam eder. İş parçacığı oluşturma, zamanlama ve yönetme ayrıntılarını çalışma zamanı üstlenir; siz yalnızca hangi işin eşzamanlı yürüyeceğini söylersiniz. Goroutine'lerin bir kimliği, adı ya da dışarıdan durdurulmasını sağlayan bir tutamacı yoktur. Bir goroutine'in ne zaman biteceğini, onun içinde çalışan kod belirler; dışarıdan durdurmak istiyorsanız iptal sinyalini kanal ya da context paketi ile kendiniz tasarlarsınız.

go deyimi nasıl çalışır?

Spesifikasyona göre go deyiminin ardından gelen ifade bir fonksiyon ya da metot çağrısı olmalıdır ve parantez içine alınamaz. Fonksiyon değeri ve parametreler, sıradan bir çağrıdaki gibi çağıran goroutine içinde değerlendirilir; fark, programın çağrının bitmesini beklememesidir. Fonksiyon sona erdiğinde goroutine de sona erer. Fonksiyonun dönüş değerleri varsa atılır; bir goroutine'den sonuç almak için kanal ya da paylaşılan bir değişkeni senkronizasyonla kullanmanız gerekir.

Argümanların go satırı çalıştığı anda değerlendirilmesi pratikte önemlidir. Aşağıdaki örnekte x değişkeni goroutine başlatıldıktan hemen sonra değişiyor, ancak goroutine'e kopyalanan değer 1 olarak kalıyor:

go
package main

import (
	"fmt"
	"sync"
)

func yaz(n int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("goroutine gördü:", n)
}

func main() {
	var wg sync.WaitGroup
	x := 1
	wg.Add(1)
	go yaz(x, &wg) // x burada, go deyimi çalışırken değerlendirilir
	x = 2
	wg.Wait()
	fmt.Println("x şimdi:", x)
}

// Çıktı:
// goroutine gördü: 1
// x şimdi: 2

Fonksiyon değişmezleri (function literal) ile goroutine başlatmak da yaygındır. Bu durumda ifadenin sonundaki () çağrı parantezlerini unutmamalısınız; go func() { ... } yazıp parantezleri koymazsanız Go 1.27.3 derleyicisi expression in go must be function call hatası verir.Fonksiyon değişmezleri kapanıştır (closure): dışarıdaki değişkenlere argüman olarak değil, referans olarak erişirler. Yani yukarıdaki örnekte x'i parametre yerine kapanış içinde okusaydınız, goroutine'in 1'i mi 2'yi mi göreceği belirsiz olurdu ve bu bir veri yarışı olurdu.

main bitince goroutine'lere ne olur?

Bir Go programı, main paketindeki main fonksiyonu döndüğü anda sona erer. Spesifikasyon bunu açıkça yazar: program main dışındaki goroutine'lerin tamamlanmasını beklemez. Bu yüzden aşağıdaki program çoğu çalıştırmada yalnızca main bitti yazar; selamlama satırı, goroutine'e çalışma fırsatı düşmeden program kapandığı için görünmez. Ancak bunun da bir garantisi yoktur, çıktı çalıştırmadan çalıştırmaya değişebilir:

go
package main

import "fmt"

func selamla(ad string) {
	fmt.Println("Merhaba,", ad)
}

func main() {
	go selamla("Ayşe") // yeni goroutine başlar, main beklemez
	fmt.Println("main bitti")
}

// Çıktı (her çalıştırmada değişebilir):
// main bitti

Bellek modeli bu durumu bir adım öteye taşır: bir goroutine'in sona ermesi programdaki hiçbir olaydan önce eşitlenmiş (synchronized before) sayılmaz. Bir goroutine'in yaptığı bir yazmanın başka bir goroutine tarafından görülmesi için kanal iletişimi ya da kilit gibi açık bir senkronizasyon gerekir. Belge, hiçbir senkronizasyon olayı izlemeyen bir go deyimini agresif bir derleyicinin tamamen silebileceğini bile not eder. Sonuç olarak time.Sleep ile "biraz bekleyeyim, goroutine bitmiştir" yaklaşımı doğru değildir; makine yükü değiştiğinde program sessizce yanlış davranır.

Bir goroutine'in bitmesini beklemek

Bir goroutine'in bitmesini beklemek: Tamponsuz kanal, Tamponlu kanal (n), WaitGroup.Go (Go 1.25+), WaitGroup Add/Done

En temel yöntem bir kanal kullanmaktır. Goroutine işini bitirince kanala bir değer gönderir, main o değeri alana kadar bekler. Tamponsuz kanalda gönderme ve alma buluşmak zorunda olduğu için bu aynı zamanda bir senkronizasyon noktasıdır: goroutine'in kanaldan önce yaptığı her şey main tarafından görülebilir hale gelir.

go
package main

import "fmt"

func selamla(ad string, bitti chan<- bool) {
	fmt.Println("Merhaba,", ad)
	bitti <- true // işin bittiğini haber ver
}

func main() {
	bitti := make(chan bool)
	go selamla("Ayşe", bitti)
	<-bitti // goroutine sinyal gönderene kadar bekle
	fmt.Println("main bitti")
}

// Çıktı:
// Merhaba, Ayşe
// main bitti

Birden çok goroutine beklemeniz gerektiğinde sync.WaitGroup daha okunaklıdır. Go 1.25 ile gelen WaitGroup.Go metodu, sayaç artırma, goroutine başlatma ve bitince sayacı azaltma işini tek çağrıda yapar. Aşağıdaki örnekte her goroutine sonucu kendi indisine yazdığı için aynı dilime eşzamanlı erişim güvenlidir; farklı elemanlar farklı bellek konumlarıdır. Wait döndükten sonra okunan değerlerin tamamı görünürdür, çünkü belgeye göre f'nin dönüşü, onu bekleyen Wait çağrısının dönüşünden önce eşitlenir.

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	kelimeler := []string{"elma", "ğ", "çiçek", "ıhlamur"}
	uzunluk := make([]int, len(kelimeler))

	var wg sync.WaitGroup
	for i, k := range kelimeler {
		wg.Go(func() { // Go 1.25: Add(1) + go + Done tek çağrıda
			uzunluk[i] = len([]rune(k)) // her goroutine kendi indisine yazar
		})
	}
	wg.Wait()
	fmt.Println(uzunluk)
}

// Çıktı:
// [4 1 5 7]

Kanal ve WaitGroup ayrıntıları için Go channel yazısına ve sync.Mutex ve WaitGroup yazısına bakabilirsiniz. Hangisini seçeceğinizi şu tablo özetler:

YöntemNe zaman uygunDikkat
Tamponsuz kanalTek goroutine, sonuç ya da sinyal alınacakAlıcı yoksa gönderen sonsuza kadar bekler
Tamponlu kanal (n)n goroutine sonuç gönderecek, erken dönüş olabilirKapasite gönderen sayısını karşılamalı
WaitGroup.Go (Go 1.25+)Çok sayıda iş, sonuç ayrı yolla toplanıyorf panic etmemeli; Wait'ten önce çağrılmalı
WaitGroup Add/DoneGo 1.25 öncesi kod ya da goroutine dışı işlerAdd, go satırından önce çağrılmalı
context iptaliGoroutine'e dur demek gerekiyorGoroutine ctx.Done() kanalını dinlemeli
time.SleepHiçbir zaman senkronizasyon için değilGörünürlük garantisi vermez

Döngü içinde goroutine başlatmak ve Go 1.22 değişikliği

Döngü içinde goroutine başlatıp döngü değişkenini kapanışta kullanmak eskiden en bilinen Go hatalarından biriydi. Go 1.22 öncesinde for döngüsünün değişkeni tüm yinelemeler boyunca tek bir değişkendi; goroutine'ler çoğu zaman son değeri görürdü. Effective Go'daki ilgili örneğin altında da 1.22 öncesi sürümlerde bu kodun hatalı olduğu notu durur. Go 1.22'den itibaren döngüde := ile tanımlanan değişkenler her yinelemede yeniden oluşturulur, bu yüzden aşağıdaki kod doğru çalışır:

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	kareler := make([]int, 5)
	var wg sync.WaitGroup
	for i := range 5 { // Go 1.22+: her yinelemede yeni i
		wg.Go(func() {
			kareler[i] = i * i
		})
	}
	wg.Wait()
	fmt.Println(kareler)
}

// Çıktı:
// [0 1 4 9 16]

Bu davranış modülünüzün go.mod dosyasındaki go satırına bağlıdır; ayrıntıları for döngüsü ve range yazısında bulabilirsiniz. Eski bir modülde çalışıyorsanız değeri argüman olarak geçirmek (go func(i int) { ... }(i)) hâlâ geçerli ve açık bir çözümdür.

Eşzamanlılık, paralellik ve GOMAXPROCS

Effective Go iki kavramı ayırmanızı ister: eşzamanlılık (concurrency) programı bağımsız yürüyen parçalar halinde yapılandırmaktır; paralellik (parallelism) hesaplamaları verim için birden çok işlemcide aynı anda yürütmektir. Go eşzamanlı bir dildir; eşzamanlılık araçları bazı problemleri paralel hale getirmeyi kolaylaştırır ama her paralelleştirme problemi bu modele uymaz. Tek çekirdekli bir makinede de goroutine'ler eşzamanlı çalışır, yalnızca aynı anda yürümezler.

Aynı anda Go kodu çalıştırabilecek işlemci sayısını runtime.GOMAXPROCS belirler. Fonksiyonu 0 ile çağırırsanız ayar değişmez, yalnızca mevcut değer döner; 1 veya daha büyük bir sayı verirseniz yeni değer atanır ve eski değer döner. Aynı adlı ortam değişkeniyle de ayarlanabilir. runtime.NumCPU ise sürecin kullanabileceği mantıksal işlemci sayısını verir. Aşağıdaki örneğin ilk iki satırı makineye göre değişir; bu çıktı 32 mantıksal işlemcili bir Windows makinesinden alındı. runtime.NumGoroutine o anda var olan goroutine sayısını verir:

go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println("NumCPU:", runtime.NumCPU())
	fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 0: yalnızca sorgula
	fmt.Println("goroutine sayısı:", runtime.NumGoroutine())

	dur := make(chan struct{})
	for range 3 {
		go func() { <-dur }() // kanal kapanana kadar bekler
	}
	fmt.Println("goroutine sayısı:", runtime.NumGoroutine())
	close(dur)
}

// Çıktı (makineye göre değişir):
// NumCPU: 32
// GOMAXPROCS: 32
// goroutine sayısı: 1
// goroutine sayısı: 4

Go 1.25 sürüm notlarına göre varsayılan GOMAXPROCS davranışı değişti. Linux'ta çalışma zamanı, sürecin bağlı olduğu cgroup'un CPU bant genişliği sınırını dikkate alır ve bu sınır mantıksal işlemci sayısından düşükse varsayılanı ona indirir; Kubernetes gibi ortamlarda bu sınır genellikle "CPU limit" ayarına karşılık gelir, "CPU request" dikkate alınmaz. Ayrıca tüm işletim sistemlerinde çalışma zamanı, işlemci sayısı ya da cgroup sınırı değişirse GOMAXPROCS'u düzenli aralıklarla günceller. GOMAXPROCS'u ortam değişkeniyle ya da fonksiyonla elle ayarlarsanız bu otomatik davranışlar kapanır; runtime.SetDefaultGOMAXPROCS varsayılana geri döndürür. Bu yeni varsayılanlar go.mod dil sürümü 1.24 ve altı olan programlarda devreye girmez.

Sınırsız goroutine yerine sabit sayıda işçi

Goroutine ucuzdur ama bedava değildir. Effective Go, gelen her istek için yeni goroutine başlatan bir sunucunun, istekler hızlı gelirse sınırsız kaynak tüketebileceğini gösterir. Çözümlerden biri, aynı iş kanalından okuyan sabit sayıda işçi goroutine başlatmaktır. İşçi sayısı, aynı anda yapılan iş sayısının üst sınırı olur. Aşağıdaki örnekte üç işçi altı işi paylaşıyor; sonuçların geliş sırası her çalıştırmada değişebileceği için yazdırmadan önce sıralıyoruz:

go
package main

import (
	"fmt"
	"slices"
	"sync"
)

func main() {
	isler := make(chan int)
	sonuclar := make(chan int)

	var wg sync.WaitGroup
	for range 3 { // sabit sayıda işçi
		wg.Go(func() {
			for n := range isler {
				sonuclar <- n * n
			}
		})
	}

	go func() {
		for n := 1; n <= 6; n++ {
			isler <- n
		}
		close(isler) // işçilerin range döngüsü biter
	}()

	go func() {
		wg.Wait()
		close(sonuclar) // tüm işçiler bitince sonuç kanalını kapat
	}()

	var hepsi []int
	for s := range sonuclar {
		hepsi = append(hepsi, s)
	}
	slices.Sort(hepsi) // geliş sırası her çalıştırmada farklı olabilir
	fmt.Println(hepsi)
}

// Çıktı:
// [1 4 9 16 25 36]

Örnekte iki küçük ayrıntı önemli. İş kanalını işleri gönderen goroutine kapatır; bu, işçilerin range döngüsünü bitirir. Sonuç kanalını ise bütün işçiler bittikten sonra ayrı bir goroutine kapatır. Sonuç kanalını işçilerden biri kapatsaydı, diğer işçiler kapalı kanala göndermeye çalışıp panic üretebilirdi. Aynı anda çalışan iş sayısını kanal tamponuyla sınırlayan semafor deseni için kanallar yazısına bakabilirsiniz.

Goroutine içindeki panic tüm programı düşürür

Bir goroutine içinde yakalanmayan panic yalnızca o goroutine'i değil bütün programı sonlandırır. main içindeki recover başka bir goroutine'de oluşan panic'i yakalayamaz, çünkü recover yalnızca aynı goroutine'deki ertelenmiş (deferred) fonksiyonlardan çağrıldığında işe yarar. Aşağıdaki programın son satırı hiç çalışmaz; çıktının ilk satırı panic mesajıdır, ardından yığın izi gelir:

go
package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		var m map[string]int
		m["a"] = 1 // nil map'e yazma: panic
	}()
	time.Sleep(100 * time.Millisecond)
	fmt.Println("buraya ulaşılmaz")
}

// Çıktı (ilk satırlar):
// panic: assignment to entry in nil map

Panic'in programı düşürmesini istemediğiniz durumlarda, örneğin dışarıdan gelen ve hatalı olabilecek bir görevi çalıştırıyorsanız, kurtarma kodunu goroutine'in kendi içine koymanız gerekir:

go
package main

import (
	"fmt"
	"sync"
)

func guvenli(wg *sync.WaitGroup, is func()) {
	go func() {
		defer wg.Done()
		defer func() {
			if r := recover(); r != nil {
				fmt.Println("yakalandı:", r)
			}
		}()
		is()
	}()
}

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	guvenli(&wg, func() {
		var s []int
		_ = s[3]
	})
	wg.Wait()
	fmt.Println("program devam ediyor")
}

// Çıktı:
// yakalandı: runtime error: index out of range [3] with length 0
// program devam ediyor

Bu deseni her yere yaymak yerine panic'in nedenini düzeltmek çoğu zaman daha doğrudur; panic ve recover'ın ne zaman uygun olduğunu defer, panic ve recover yazısında ele aldık. Ayrıca belgeye göre WaitGroup.Go'ya verilen fonksiyon panic etmemelidir.

Goroutine sızıntısı ve Go 1.27 sızıntı profili

Go Blog'daki pipelines yazısı önemli bir gerçeği hatırlatır: goroutine'ler çöp toplayıcı tarafından toplanmaz, kendi başlarına çıkmaları gerekir. Bir kanala gönderim yapmak isteyen ama alıcısı artık hiç gelmeyecek olan bir goroutine sonsuza kadar bekler; yığını ve yığının referans verdiği veriler bellekte kalır. Buna goroutine sızıntısı denir.

Go 1.26 sürüm notları tipik bir örnek verir: işleri paralel işleyip sonuçları tamponsuz bir kanaldan toplayan fonksiyon, ilk hatada erken dönerse geri kalan goroutine'ler gönderemedikleri sonuçla birlikte takılı kalır. Aşağıda aynı yapının düzeltilmiş halini görüyorsunuz: kanalın kapasitesi iş sayısı kadar olduğu için her goroutine sonucunu tampona bırakıp çıkabiliyor. Program, erken dönüşten sonra kalan goroutine'lerin bittiğini NumGoroutine ile gösteriyor:

go
package main

import (
	"errors"
	"fmt"
	"runtime"
	"time"
)

type sonuc struct {
	n   int
	err error
}

func isle(n int) (int, error) {
	if n == 3 {
		return 0, errors.New("3 işlenemedi")
	}
	time.Sleep(time.Duration(n) * 10 * time.Millisecond)
	return n * 10, nil
}

func hepsiniIsle(isler []int) ([]int, error) {
	ch := make(chan sonuc, len(isler)) // tampon: erken dönüşte gönderenler takılmaz
	for _, n := range isler {
		go func() {
			v, err := isle(n)
			ch <- sonuc{v, err}
		}()
	}
	var out []int
	for range len(isler) {
		s := <-ch
		if s.err != nil {
			return nil, s.err // erken dönüş
		}
		out = append(out, s.n)
	}
	return out, nil
}

func main() {
	_, err := hepsiniIsle([]int{1, 2, 3, 4, 5})
	fmt.Println("hata:", err)
	time.Sleep(200 * time.Millisecond) // kalan goroutine'lerin bitmesine zaman tanı
	fmt.Println("kalan goroutine:", runtime.NumGoroutine())
}

// Çıktı:
// hata: 3 işlenemedi
// kalan goroutine: 1

Tamponu kullanabilmek için gönderici sayısını önceden bilmeniz gerekir. Bilmiyorsanız ya da goroutine'in daha erken durması gerekiyorsa iptal sinyali vermelisiniz; bunun standart yolu context paketi, zaman aşımlı seçimler için de select ve zaman aşımı yazısıdır.

Sızıntıları bulmak için Go 1.26'da deneysel olarak gelen goroutineleak profili Go 1.27 ile genel kullanıma açıldı. Profil runtime/pprof paketinde ve net/http/pprof üzerinden /debug/pprof/goroutineleak uç noktasında sunulur. Çalışma zamanı, bloklandığı eşzamanlılık nesnesine (kanal, sync.Mutex, sync.Cond gibi) çalışabilir hiçbir goroutine'den ulaşılamayan goroutine'leri çöp toplayıcı yardımıyla tespit eder. Sürüm notları bu yöntemin ulaşılabilirliğe dayandığını, bu yüzden global değişkenler ya da çalışan goroutine'lerin yerel değişkenleri üzerinden erişilebilen nesnelerdeki sızıntıları kaçırabileceğini belirtir.

Sık yapılan hatalar

Sık yapılan hatalar: main'in beklemesini unutmak, Senkronizasyon yerine time.Sleep, Paylaşılan değişkene korumasız yazmak, Add çağrısını goroutine içine koymak
  • main'in beklemesini unutmak: Goroutine başlatıp main'den dönmek işin yarıda kalmasına neden olur. Kanal ya da WaitGroup ile bekleyin.
  • Senkronizasyon yerine time.Sleep: Uyku süresi doğru görünse bile bellek modeli açısından hiçbir garanti vermez.
  • Paylaşılan değişkene korumasız yazmak: İki goroutine aynı değişkene eşitleme olmadan erişiyorsa ve en az biri yazıyorsa veri yarışı vardır. Ayrıntılar sync/atomic ve veri yarışı yazısında.
  • Add çağrısını goroutine içine koymak: wg.Add(1) goroutine içinde çalışırsa Wait ondan önce dönebilir. Go 1.25 ile go vet'e bu hatayı bildiren bir denetim eklendi; en kolayı wg.Go kullanmak.
  • Gönderen sayısı kadar alıcı olmaması: Erken dönen bir fonksiyon, sonuç göndermeye çalışan goroutine'leri sızdırır.
  • Her iş için sınırsız goroutine: Yük altında bellek ve dosya tanıtıcısı tükenebilir; işçi havuzu ya da semafor kullanın.
  • Goroutine içindeki panic'i main'de yakalamaya çalışmak: recover yalnızca aynı goroutine'de çalışır.

Sık Sorulan Sorular

Bir programda kaç goroutine çalıştırabilirim?

Belgeler sabit bir üst sınır vermez. Effective Go goroutine'in maliyetinin yığın alanı ayırmanın biraz üzerinde olduğunu ve yığınların küçük başladığını söyler; bu yüzden çok sayıda goroutine olağandır. Pratik sınırı belleğiniz ve goroutine'lerin tuttuğu kaynaklar (bağlantılar, dosyalar) belirler. Dış kaynaklara erişen işlerde eşzamanlılığı bilinçli olarak sınırlayın.

Goroutine'den dönüş değeri nasıl alınır?

go deyimiyle çağrılan fonksiyonun dönüş değerleri atılır. Sonucu bir kanal üzerinden gönderebilir ya da her goroutine'in kendi indisine yazdığı bir dilim kullanıp WaitGroup ile bekleyebilirsiniz. Hata da döndürmeniz gerekiyorsa sonuç ve hatayı bir yapı içinde gönderin.

Bir goroutine'i dışarıdan öldürebilir miyim?

Hayır. Go'da goroutine'i dışarıdan durduran bir API yoktur. Goroutine'in iptal sinyalini dinlemesi gerekir: kapatılan bir kanal ya da context.Context'in Done kanalı bu iş için kullanılır. runtime.Goexit ise yalnızca onu çağıran goroutine'i, ertelenmiş çağrılarını çalıştırarak sonlandırır.

GOMAXPROCS'u elle ayarlamalı mıyım?

Çoğu durumda gerekmez. Go 1.25 ve sonrasında varsayılan değer işlemci sayısını, CPU afinite maskesini ve Linux'ta cgroup kotasını dikkate alır ve zamanla güncellenir. Elle ayarlarsanız otomatik güncelleme kapanır; bu yüzden ölçümle desteklenen bir nedeniniz olmadıkça varsayılanı kullanın.

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 channel (kanallar) · select ve zaman aşımı · sync.Mutex ve WaitGroup · context paketi · sync/atomic ve veri yarışı · defer, panic, recover · Go nedir?