Go Dilinde Diziler

1.1 Dizilerin Tanımı ve Deklerasyonu

Bir dizi, aynı türdeki elemanların sabit boyutlu bir dizisidir. Go dilinde bir dizinin uzunluğu, dizi türünün bir parçası olarak kabul edilir. Bu, farklı uzunluktaki dizilerin farklı türler olarak kabul edildiği anlamına gelir.

Bir dizi deklare etmek için temel sözdizimi aşağıdaki gibidir:

var arr [n]T

Burada, var değişken deklarasyonu için anahtar sözcüğü, arr dizinin adı, n dizinin uzunluğunu temsil eder ve T ise dizideki elemanların türünü temsil eder.

Örneğin, 5 tamsayı içeren bir diziyi deklare etmek için:

var myArray [5]int

Bu örnekte, myArray türü int olan 5 tamsayıyı içerebilen bir dizidir.

1.2 Dizilerin Başlatılması ve Kullanılması

Dizilerin başlatılması, deklarasyon sırasında doğrudan veya indisleri kullanarak değer atayarak yapılabilir. Dizilerin başlatılması için birden fazla yöntem bulunmaktadır:

Doğrudan Başlatma

var myArray = [5]int{10, 20, 30, 40, 50}

Ayrıca, uzunluğun derleyici tarafından inşa edilmesine olanak tanımak için aşağıdaki gibi başlatılabilir:

var myArray = [...]int{10, 20, 30, 40, 50}

Burada ..., dizinin uzunluğunun derleyici tarafından hesaplandığını gösterir.

İndeksler Kullanarak Başlatma

var myArray [5]int
myArray[0] = 10
myArray[1] = 20
// Geriye kalan elemanlar int'in sıfır değeri olan 0'a eşitlenir

Dizilerin kullanımı da basittir ve elemanlara indisleri kullanarak erişilebilir:

fmt.Println(myArray[2]) // Üçüncü elemana erişmek

1.3 Dizi Dolaşımı

Dizi dolaşımı için iki yaygın yöntem, geleneksel for döngüsü ve range kullanarak yapılan dolaşımdır.

for Döngüsü ile Dolaşım

for i := 0; i < len(myArray); i++ {
    fmt.Println(myArray[i])
}

range ile Dolaşım

for index, value := range myArray {
    fmt.Printf("İndeks: %d, Değer: %d\n", index, value)
}

range kullanmanın avantajı, mevcut indeks pozisyonunu ve o pozisyondaki değeri döndürmesidir.

1.4 Dizilerin Karakteristik Özellikleri ve Sınırlamaları

Go dilinde diziler, bir değer türüdür, bu da bir dizinin bir fonksiyona parametre olarak geçirildiğinde, diziye bir kopya geçirildiği anlamına gelir. Bu nedenle, bir fonksiyon içinde orijinal dizinin değiştirilmesi gerekiyorsa, genellikle dilimler veya dizi işaretçileri kullanılır.

2 Go Dilinde Dilimler

2.1 Dilim Kavramı

Go dilinde bir dilim, bir dizi üzerine bir soyutlama olarak tanımlanır. Go dizisinin boyutu değişmez, bu da onun belirli senaryolarda kullanımını sınırlar. Go dilinde dilimler, veri yapılarını seri hale getirmek için kullanışlı, esnek ve güçlü bir arayüz sağlayarak daha esnek olmasını sağlayacak şekilde tasarlanmıştır. Dilimler kendileri veri tutmazlar; onlar sadece temel dizinin referanslarıdır. Dinamik doğası, genellikle aşağıdaki noktalarla karakterize edilir:

  • Dinamik Boyut: Dizilerin aksine, dilimin uzunluğu dinamiktir ve ihtiyaca göre otomatik olarak büyür veya küçülür.
  • Esneklik: Elemanlar kolayca append işlevini kullanarak diziye eklenebilir.
  • Referans Türü: Dilimler, verinin kopyalarını oluşturmadan, temel dizideki elemanlara referans ile erişir.

2.2 Dilimlerin Deklerasyonu ve Başlatılması

Dilimi deklare etme sözdizimi, bir diziyi deklare etme şekline benzer, ancak deklare ederken elemanların sayısını belirtmeniz gerekmez. Örneğin, tamsayı bir dilim deklare etme şekli aşağıdaki gibidir:

var slice []int

Dilimi bir dilim literali kullanarak başlatabilirsiniz:

slice := []int{1, 2, 3}

Yukarıdaki slice değişkeni, üç tamsayı içeren bir dilim olarak başlatılacaktır.

Ayrıca, dilimi uzunluğunu ve kapasitesini belirtmenizi sağlayan make işlevini kullanarak da başlatabilirsiniz:

slice := make([]int, 5)  // Uzunluğu ve kapasitesi 5 olan bir tamsayı dilimi oluştur

Daha büyük bir kapasite gerekiyorsa, make işlevine üçüncü parametre olarak kapasiteyi iletebilirsiniz:

slice := make([]int, 5, 10)  // Uzunluğu 5, kapasitesi 10 olan bir tamsayı dilimi oluştur

2.3 Dilimler ve Diziler Arasındaki İlişki

Dilimler, bir dizinin bir segmentini belirterek oluşturulabilir ve bu segmente bir referans oluşturur. Örneğin, aşağıdaki diziyi ele alalım:

dizi := [5]int{10, 20, 30, 40, 50}

Ardından aşağıdaki şekilde bir dilim oluşturabiliriz:

dilim := dizi[1:4]

Bu dilim dilim, dizi dizisindeki 1. dizinden 3. diziye kadar olan elemanlara (1. diziden başlanarak, 4. diziden önce) referans oluşturacaktır.

Dikkat edilmesi gereken önemli bir nokta, dilimin aslında dizi elemanlarının değerlerini kopyalamaması; yalnızca orijinal dizinin sürekli bir segmentine işaret etmesidir. Bu nedenle, dilime yapılan değişiklikler aynı zamanda temel diziyi etkileyecek ve tam tersi de geçerli olacaktır. Bu referans ilişkisini anlamak, dilimleri etkili bir şekilde kullanmak için hayati önem taşır.

2.4 Dilimlerde Temel İşlemler

2.4.1 İndeksleme

Dilimler, indisleri kullanarak elemanlara erişir, dizilerle benzer şekilde, indis 0'dan başlayarak. Örneğin:

dilim := []int{10, 20, 30, 40}
// İlk ve üçüncü elemanlara erişme
fmt.Println(dilim[0], dilim[2])

2.4.2 Uzunluk ve Kapasite

Dilimlerin uzunluk (len) ve kapasite (cap) olmak üzere iki özelliği vardır. Uzunluk, dilimdeki elemanların sayısıdır ve kapasite, dilimin ilk elemanından başlayarak temel dizinin sonuna kadar olan eleman sayısıdır.

dilim := []int{10, 20, 30, 40}
// Dilimin uzunluğunu ve kapasitesini yazdırma
fmt.Println(len(dilim), cap(dilim))

2.4.3 Eleman Ekleme

append işlevi, bir dilime eleman eklemek için kullanılır. Dilimin kapasitesi yeni elemanları barındırmak için yeterli olmadığında, append işlevi otomatik olarak dilimin kapasitesini genişletir.

dilim := []int{10, 20, 30}
// Tek bir eleman eklemek
dilim = append(dilim, 40)
// Birden fazla eleman eklemek
dilim = append(dilim, 50, 60)
fmt.Println(dilim)

Eklemek için append kullanırken önemli bir nokta, yeni dilim döndürebilmesidir. Temel dizinin kapasitesi yetersizse, append işlemi dilimi yeni, daha büyük bir diziye işaret edecek şekilde değiştirir.

2.5 Dilimlerin Genişletilmesi ve Kopyalanması

copy işlevi, bir dilimin elemanlarını başka bir dilime kopyalamak için kullanılabilir. Hedef dilimin, kopyalanan elemanları barındırmak için zaten yeterli alan ayırmış olması gerekir ve işlem hedef dilimin kapasitesini değiştirmeyecektir.

2.5.1 copy İşlevinin Kullanımı

Aşağıdaki kod, copy işlevinin nasıl kullanılacağını göstermektedir:

kaynak := []int{1, 2, 3}
hedef := make([]int, 3)
// Elemanları hedef dilime kopyalama
kopyalanan := copy(hedef, kaynak)
fmt.Println(hedef, kopyalanan)

copy işlevi, kopyalanan elemanların sayısını döndürür ve bu sayı, hedef dilimin uzunluğunu veya kaynak dilimin uzunluğunu aşmayacaktır, hangisi daha küçükse onu geçmeyecektir.

2.5.2 Dikkat Edilmesi Gerekenler

copy işlevini kullanırken, eğer kopyalanacak yeni elemanlar eklenirse ve hedef dilim yeterli alanı içermiyorsa, yalnızca hedef dilimin alabileceği elemanlar kopyalanacaktır.

2.6 Çok Boyutlu Dilimler

Çok boyutlu bir dilim, birden fazla dilimi içeren bir dilimdir. Bu, çok boyutlu bir diziyi andırır, ancak dilimlerin değişken uzunluğundan dolayı, çok boyutlu dilimler daha esnektir.

2.6.1 Çok Boyutlu Dilimlerin Oluşturulması

İki boyutlu bir dilim (dilimlerin dilimi) oluşturma:

ikiD := make([][]int, 3)
for i := 0; i < 3; i++ {
    ikiD[i] = make([]int, 3)
    for j := 0; j < 3; j++ {
        ikiD[i][j] = i + j
    }
}
fmt.Println("İki boyutlu dilim: ", ikiD)

2.6.2 Çok Boyutlu Dilimlerin Kullanımı

Çok boyutlu bir dilimi kullanmak, indis ile erişmek şeklinde tek boyutlu bir dilimi kullanmaya benzer:

// İki boyutlu dilimin elemanlarına erişme
değer := ikiD[1][2]
fmt.Println(değer)

3 Dizi ve Dilim Uygulamalarının Karşılaştırılması

3.1 Kullanım Senaryolarının Karşılaştırılması

Go'da diziler ve dilimler (slice), aynı türdeki veri koleksiyonlarını depolamak için kullanılır ancak kullanım senaryolarında belirgin farklılıkları bulunmaktadır.

Diziler:

  • Bir dizinin uzunluğu bildirimde sabittir, bu nedenle bilinen, sabit sayıda elemanı depolamak için uygundur.
  • Örneğin, sabit boyutta bir matrisi temsil etmek gibi sabit boyutta bir konteyner gerektiğinde, dizi en iyi seçenektir.
  • Diziler yığına (stack) atanabilir, bu da dizinin boyutu büyük olmadığında daha yüksek performans sağlar.

Dilimler (Slice):

  • Bir dilim, değişken uzunluklu bir soyut dinamik dizi olarak, bilinmeyen miktarda veya dinamik olarak değişebilen bir koleksiyonu depolamak için uygundur.
  • Kullanıcı girdisinin belirsiz bir şekilde depolanması gibi, ihtiyaç duyulan zaman dinamik olarak büyüyebilen veya küçülebilen bir dinamik dizi gerektiğinde, bir dilim daha uygun bir seçenektir.
  • Bir dilimin bellek yapısı, genellikle alt dizinin referansının veya tamamının rahatça işaret edilmesine izin verir, bu durum sıklıkla alt dizileri işlemede, dosya içeriğini ayırma gibi senaryolarda kullanılır.

Özetle, diziler, sabit boyut gereksinimlerine sahip senaryolar için uygundur ve Go'nun statik bellek yönetimi özelliklerini yansıtırken, dilimler daha esnek olup dizilerin soyut bir genişlemesi olarak hizmet eder ve dinamik koleksiyonlarla rahatça işlem yapmak için uygundur.

3.2 Performans Düşünceleri

Dizi veya dilim kullanımı arasında seçim yapmamız gerektiğinde, performansı dikkate almak önemli bir faktördür.

Dizi:

  • Sürekli bellek ve sabit indeksleme nedeniyle hızlı erişim hızı.
  • Bellek yığınına (stack) alan atanması (dizi boyutu bilindiğinde ve çok büyük değilse), ek bellek yükü gerektirmeden.
  • Uzunluk ve kapasiteyi saklamak için ekstra bellek harcaması olmaması, bu da bellek açısından hassas programlar için faydalı olabilir.

Dilim:

  • Dinamik büyüme veya küçülme, performans üzerinde etkili olabilir: büyüme mevcut elemanları kopyalayarak yeni bellek ayırmaya ve küçülme, işaretçileri ayarlamayı gerektirebilir.
  • Dilim işlemleri kendileri hızlı olsa da, sık eleman eklemeleri veya çıkarmaları bellek parçalanmasına neden olabilir.
  • Dilim erişimi küçük dolaylı bir maliyet gerektirir, genellikle çok büyük bir etkisi olmaz, ancak son derece performans odaklı kodlarda bu fark edebilir.

Bu nedenle, performans önemli bir faktör ise ve veri boyutu önceden biliniyorsa, dizinin kullanılması daha uygundur. Ancak esneklik ve kullanım kolaylığı önemli ise, özellikle büyük veri kümeleriyle uğraşmak için dilimin kullanılması tavsiye edilir.

4 Ortak Sorunlar ve Çözümler

Go dilinde dizileri ve dilimleri kullanırken, geliştiriciler aşağıda belirtilen ortak sorunlarla karşılaşabilir.

Sorun 1: Dizi Sınırlarının Dışında

  • Dizi sınırlarının dışında, dizinin uzunluğunu aşan bir indeksi erişmek anlamına gelir. Bu, çalışma zamanı hatasına yol açar.
  • Çözüm: Dizi elemanlarına erişmeden önce indeks değerinin dizinin geçerli aralığında olup olmadığını her zaman kontrol edin. Bu, indeksi ve dizinin uzunluğunu karşılaştırarak sağlanabilir.
var arr [5]int
index := 10 // Geçersiz bir indeks olduğunu varsayalım
if index < len(arr) {
    fmt.Println(arr[index])
} else {
    fmt.Println("İndeks dizinin aralığının dışında.")
}

Sorun 2: Dilimlerde Bellek Sızıntıları

  • Dilimler, istenirse sadece küçük bir kısmına ihtiyaç olsa bile, yanlışlıkla orijinal dizinin bir kısmına veya tamamına referans tutabilir. Orijinal dizi büyükse bellek sızıntısına neden olabilir.
  • Çözüm: Geçici bir dilim gerekiyorsa, gerekli kısmı kopyalayarak yeni bir dilim oluşturmayı düşünün.
original := make([]int, 1000000)
smallSlice := make([]int, 10)
copy(smallSlice, original[:10]) // Sadece gereken kısmı kopyalayın
// Bu şekilde, smallSlice'ın diğer orijinal kısımlara referans vermemesi, gereksiz belleğin geri kazanılmasına yardımcı olur

Sorun 3: Dilim Yeniden Kullanımından Kaynaklanan Veri Hataları

  • Dilimlerin aynı temel dizine bir referans tutması nedeniyle, farklı dilimlerdeki veri değişikliklerinin etkisini görebilmek ve beklenmeyen hatalara neden olabilir.
  • Çözüm: Bu durumu önlemek için en iyisi, yeni bir dilim kopyası oluşturmaktır.
sliceA := []int{1, 2, 3, 4, 5}
sliceB := make([]int, len(sliceA))
copy(sliceB, sliceA)
sliceB[0] = 100
fmt.Println(sliceA[0]) // Çıktı: 1
fmt.Println(sliceB[0]) // Çıktı: 100

Yukarıda belirtilenler, Go dilinde dizileri ve dilimleri kullanırken karşılaşılabilecek bazı ortak sorunlar ve çözümlerdir. Gerçek geliştirmede dikkat edilmesi gereken daha fazla ayrıntı olabilir, ancak bu temel prensiplere uyulması, birçok ortak hatadan kaçınmaya yardımcı olabilir.