Bu yazıda elimden geldikçe, Go’da listeler (arrays, slices) üzerinde döngü kullanırken dikkatli olmamız gereken noktaları anlatmaya çalışacağım. Keyifli okumalar.
Yazının sonunda konuyu pekiştirebilmek için bir demo hazırladım. Denemeyi unutmayın.
Direkt olarak aşağıdaki gibi bir kod örneği ile başlayalım.
cities := []string{
"İstanbul",
"Ankara",
"İzmir",
"Bursa",
}
fmt.Println("cities:")
for i, v := range cities {
fmt.Println(i, v)
cityPointers = append(cityPointers, &v)
}
Çıktımız gayet beklendiği gibi olacaktır.
cities:
0 İstanbul
1 Ankara
2 İzmir
3 Bursa
Bu kod örneğine ufak bir kaç ekleme yapalım.
cities := []string{
"İstanbul",
"Ankara",
"İzmir",
"Bursa",
}
cityPointers := make([]*string, 0, len(cities))
fmt.Println("cities:")
for i, v := range cities {
fmt.Println(i, v)
cityPointers = append(cityPointers, &v)
}
fmt.Println("cityPointers:")
for i, v := range cityPointers {
fmt.Println(i, *v)
}
Devam etmeden önce, yukarıdaki kodun nasıl bir çıktı vereceğini düşünün.
Şimdi de çıktısını görelim.
cities:
0 İstanbul
1 Ankara
2 İzmir
3 Bursa
cityPointers:
0 Bursa
1 Bursa
2 Bursa
3 Bursa
Kodlarımızı çalıştırdığımızda yukarıdaki gibi bir sonuç veriyor. İki listenin döndürülüp yazdırıldığında aynı çıktıyı vereceğini düşünüyorsanız, yanılıyorsunuz. Çünkü işler pek öyle değil.
Peki ilk döngüde v
değişkeninin değeri farklı şehir isimleri halinde geliyor da, citiesPointer
‘da eklediğimizde neden hepsi Bursa
değerinde oluyor.
İlk olarak v
olarak isimlendirdiğimiz döngü değişkeninin aslında nasıl çalıştığını anlayalım.
for range
kullanarak döndürme işleminin hangi indekste olduğu (i) ve döndürülen indekste bulunan elemanın değerine (v) ulaşabiliyoruz.
Oluşturulan i
ve v
değişkenlerinin değerlerinin bellekte saklanabilmesi için bir tahsis (malloc) işlemi yapılıyor -yani ram’de yer ayrılıyor. Bu tahsis işleminin döngünün her adımında yeniden yapılması yerine, sadece ilk seferde yapılarak, bütün adımlarda ayrılan bu adresler üzerinde değerler değiştirilerek yapılıyor.
Daha açıklayıcı olması için bir kod örneği görelim.
cities := []string{
"İstanbul",
"Ankara",
"İzmir",
"Bursa",
}
for i, v := range cities {
fmt.Println(&i, i, &v, v)
}
Çıktımızda aşağıdaki gibi;
0x14000122008 0 0x1400010a230 İstanbul
0x14000122008 1 0x1400010a230 Ankara
0x14000122008 2 0x1400010a230 İzmir
0x14000122008 3 0x1400010a230 Bursa
Çıktıda görebileceğimiz gibi, döngünün her bir adımında i
ve v
için bellekte alan tahsis edilmesi yerine sadece bir defa alan tahsis ediliyor ve her bir adımda bu alandaki değer güncelleniyor.
cityPointers
listemize append
(ekleme) yapıyorken &v
şeklinde ekleyerek aslında v
‘nin bellekteki adresini ekledik.
Döngünün son adımında v
‘nin son değeri "Bursa"
olarak belirlendiği için, bu adresteki değerde bu şekilde kaldı.
cityPointers
listesindeki tüm elemanlar aynı adresi işaret ettiği için hepsinde aynı değeri, yani "Bursa"
‘yı görmekteyiz.
Eğer cities
içerisindeki elemanları cityPointers
listesine farklı değişkenler olarak eklemek isteseydik aşağıdaki gibi ufak bir değişiklik yapmamız yeterli olurdu.
for i, v := range cities {
fmt.Println(i, v)
c:= v
cityPointers = append(cityPointers, &c)
}
v
değişkenini kullanarak c
isminde yeni bir değişken oluşturduk. Bu tahsis işlemi her adımda yeniden gerçekleşeceği için, her adımda yeni bir c
değişkeni oluşturulacaktır, bu yüzden oluşturulan her c
değişkenin bellekteki adresi farklı olacaktır.
Her döngü adımında i
, v
ve c
değişkenlerinin adreslerini yazdırdığımızda aşağıdaki gibi c
‘nin farklı adresleri gösterdiğini görebiliriz.
for i, v := range cities {
c := v
fmt.Println("i:", &i, "-v:", &v, "-c:", &c)
cityPointers = append(cityPointers, &c)
}
Çıktımızı görelim:
i: 0x14000122010 -v: 0x1400010a230 -c: 0x1400010a240
i: 0x14000122010 -v: 0x1400010a230 -c: 0x1400010a250
i: 0x14000122010 -v: 0x1400010a230 -c: 0x1400010a260
i: 0x14000122010 -v: 0x1400010a230 -c: 0x1400010a270
Bu duruma dikkat ederek cityPointers
listesinin elemanlarını yazdıralım.
for i, v := range cityPointers {
fmt.Println(i, *v)
}
Çıktımızı görelim:
0 İstanbul
1 Ankara
2 İzmir
3 Bursa
Yukarıda bahsedilen senaryodaki yapabileceğimiz dikkatsizliğin, veri bütünlüğünün bozulmasıyla sonuçlanacağı apaçık ortada.
Bahsedilen bu senaryo aslında bir ısınma ve sebebini anlama aşamasıydı.
Senaryoyu bir tık daha evirelim ve neler ile sonuçlanacağını görelim.
Verilen pointer’lı bir değişken ile işlem yapan aşağıdaki gibi bir fonksiyonumuz olduğunu varsayalım.
func doSomething(v *string) {
for i := 0; i < 5; i++ {
fmt.Println(*v)
}
}
Döngünün her adımında go routine kullanarak eş zamanlı olarak bu fonksiyonu kullanalım.
cities := []string{
"İstanbul",
"Ankara",
"İzmir",
"Bursa",
}
fmt.Println("cities:")
for _, v := range cities {
go doSomething(&v)
}
// go routineleri görebilmek için programı bekletelim
time.Sleep(time.Second * 10)
Çalıştırdığımız zaman tüm işlemlerdeki değerlerin Bursa
olduğunu göreceğiz.
Bunun olmaması için, döngü içerisinde aynı şekilde;
for _, v := range cities {
c := v
go doSomething(&c)
}
şeklinde kullanmamız mantıklı olurdu.
Eğer anonim fonksiyon kullansaydık, iki farklı şekilde bunu yapabilirdik.
for _, v := range cities {
c := v
go func() {
doSomething(&c)
}()
}
veya
for _, v := range cities {
go func(c string) {
doSomething(&c)
}(v)
}
kullanarak sorunumuzu çözebilirdik. Tabi sizin için bir sorunsa. İkinci anonim fonksiyon örneğinde parametre olarak c
isimli bir string tipinde değer aldığından, zaten orijinali yerine v
değerini kopyaladığı için değişkenin adresi farklı olmaktadır.
Eğer daha performanslı bir döngü istiyorsak, i
değişkeni de her şartta kullanılacaksa, v
değişkenini kullanmadan direkt olarak indeks ile liste üzerinden veriyi alabiliriz.
Örnek olarak;
for i := range cities {
fmt.Println(cities[i])
// i değişkeninin kullanılacağı diğer işlemler...
}
Bu sayede v
kullanmayarak tek seferlik tahsis, cities
listesinin uzunluğu kadar v
değişkenine atama ve gc
maliyetinden tasarruf ettik.
Tabi bu önerinin farklı bir senaryoda gereksiz veya mantıksız olabileceğini aklımızda bulundurmalıyız.
Go’ya yapılan 1.21.0 güncellemesi ile döngümüzde kullandığımız değer değişkeninin (örnekte v
dedik) her döngü adımında bellekten yeni adres alması için bir ortam değişkeni (env) eklemesi getirildi. Bu ortam değişkeni varsayılan olarak kullanılmıyor. O yüzden eski döngü mantığımız hala daha geçerlidir.
Aşağıdaki şekilde kodumuzu çalıştırırken veya derlerken ortam değişkeni vermemiz yeterli:
GOEXPERIMENT=loopvar go run .
İlk örneğimizi bu ortam değişkeni kullanılıyorken çalıştıralım.
İlk örneğimizi hatırlamak için görelim.
cities := []string{
"İstanbul",
"Ankara",
"İzmir",
"Bursa",
}
cityPointers := make([]*string, 0, len(cities))
fmt.Println("cities:")
for i, v := range cities {
fmt.Println(i, v)
cityPointers = append(cityPointers, &v)
}
fmt.Println("cityPointers:")
for i, v := range cityPointers {
fmt.Println(i, *v)
}
Bahsettiğimiz ortam değişkeni ile çalıştırdığımızda aşağıdaki gibi çıktı verecektir.
cities:
0 İstanbul
1 Ankara
2 İzmir
3 Bursa
cityPointers:
0 İstanbul
1 Ankara
2 İzmir
3 Bursa
Aşağıdaki demo ile interaktif olarak deneyebilirsiniz.