Dilimler Hakkında
Go dili tarafından sağlanan dizilere dayalı soyut bir veri türüdür. Go’da, diziler gerektiren çoğu senaryo dilimlerle değiştirilebilir. Go’da, dilimler dizileri mükemmel bir şekilde değiştirir ve daha esnek ve verimli bir veri dizisi erişim arayüzü sağlar.
Slice Tam Olarak Nedir?
Go’da dilim kavramını anlamadan önce dizileri anlamamız gerekiyor.
Dilim Nedir?
Go’da dizi, aynı türdeki öğeleri tutan sabit uzunlukta sürekli bir dizidir . Bu nedenle, Go dizilerinin iki özelliği vardır: ve . Bu iki türün aynı olduğu diziler eşdeğerdir. Yani:element type
array length
var a [8]int
var b [8]int
Burada dizi uzunluğu a
8’dir ve eleman türü int’tir, yani ile aynıdır , dolayısıyla ve’nin eşdeğer olduğunu b
söyleyebiliriz .a
b
Go’da değer semantiği vardır, yani bir dizi değişkeni dizinin ilk elemanına bir işaretçi değil, tüm diziyi temsil eder (C’de yapıldığı gibi). Go’da bir diziyi geçirmek değer kopyalamayı kullanır, bu da büyük eleman türlerine veya birçok elemana sahip dizileri geçirirken önemli performans kaybına yol açar.
C’de bu tür senaryolar dizi işaretçi türleri kullanılarak işlenebilir , ancak Go’da genellikle dilimler kullanılır.
Dilimler ve Diziler Arasındaki İlişki
Dilimler ve diziler arasındaki ilişki , dosya tanımlayıcılarının dosyalara olan ilişkisine benzer . Go’da, diziler genellikle “altta yatan” veri depolaması olarak “arka plana” çekilirken, dilimler “ön plana” geçerek altta yatan depolamaya harici bir erişim arayüzü sağlar.
Bu nedenle dilimler dizilerin “tanımlayıcıları” olarak adlandırılabilir. Dilimlerin farklı işlevler arasında geçirilebilmesinin nedeni “tanımlayıcıların” özellikleridir; temel veri depolama alanı ne kadar büyük olursa olsun, tanımlayıcının boyutu her zaman sabittir.
Dilimler Oluşturmanın Yolları ve Çalışma Zamanında Temsilleri
Dilimlerin gösterimi Go runtime
:
//$GOROOT/src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
Gördüğümüz gibi bir dilimin üç öğesi vardır:
array
: Altta yatan dizinin bir öğesine işaret eden ve aynı zamanda dilimin ilk öğesi olan bir işaretçi.len
: dilimin uzunluğu, dilimdeki mevcut öğe sayısıdır.cap
: dilimin maksimum kapasitesi,cap >= len
.
Çalışma zamanında, her dilim yapının bir örneğidir runtime.slice
ve aşağıdaki ifadeyi kullanarak bir dilim oluşturabiliriz:
s := make([]byte, 5)

Bir dilim oluşturduğumuzda derleyicinin otomatik olarak altta yatan bir dizi oluşturduğunu görebiliriz. Belirtmediğimizde cap
varsayılan olarak olur cap = len
.
Mevcut bir dizi üzerinde işlem yaparak da bir dilim oluşturabiliriz, buna şunu diyoruz slicing an array
:
u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
s := u[3:7]

s
Dilimin dizi üzerinde bir işlem penceresi açtığını görebiliriz u
, s
‘nin ilk elemanı u[3]
‘dir ve aracılığıyla s
4 dizi elemanını görebilir ve bunlar üzerinde işlem yapabiliriz. cap
Dilimin uzunluğu, alttaki dizinin uzunluğuna bağlıdır; ‘den u[3]
sonuna kadar elemanlar için 7 depolama yuvası vardır, bu yüzden s
‘nin cap
uzunluğu 7’dir.
Elbette, aynı dizi üzerinde işlem yapan birden fazla dilim oluşturabiliriz, ancak birden fazla dilim aynı dizinin tanımlayıcıları olduğundan, herhangi bir dilimde yapılan herhangi bir değişiklik diğer dilimlere de yansıyacaktır.
Dilimler üzerinde işlem yaparak yeni dilimler de oluşturabiliriz, bu işlem olarak bilinir reslicing
. Benzer şekilde, eski dilimden oluşturulan yeni dilim aynı temel diziyi paylaşır, bu nedenle yeni dilimde yapılan değişiklikler eski dilime de yansır.
Bu nedenle, dilimler parametre olarak geçirildiğinde, geçirilen şey ‘dir runtime.slice
, bu nedenle altta yatan dizi ne kadar büyük olursa olsun, dilimlerin getirdiği performans kaybı çok küçük ve sabittir, hatta ihmal edilebilir düzeydedir. Bu, dilimlerin genellikle işlevlerde diziler yerine kullanılmasının bir nedenidir. Başka bir neden de dilimlerin işaretçi dizilerinden daha güçlü işlevsellik sağlayabilmesidir, örneğin dizin erişimi, sınır taşması denetimleri ve dinamik yeniden boyutlandırma.
Slices’ın Gelişmiş Özellikleri
Dinamik Yeniden Boyutlandırma
Dilimler sıfır değerli kullanılabilirlik kavramını karşılar çünkü dilimler gelişmiş bir özelliğe sahiptir: dynamic resizing
. Sıfır değerli bir dilim bile önceden tanımlanmış işlev aracılığıyla eleman atama işlemlerini gerçekleştirebilir append
.
var k []int
k = append(k, 1)
k
sıfır değerine başlatılır, bu nedenle k
karşılık gelen bir altta yatan diziye bağlı değildir. Ancak, append
işlemden sonra, k
açıkça kendi altta yatan dizisine bağlıdır.
Şimdi, her birinden sonra gelen len
ve ifadesini bir dizi pratik işlemle göstereceğim :cap
append
var s []int
s = append(s, 1)
fmt.Println(len(s), cap(s)) // 1 1
s = append(s, 2)
fmt.Println(len(s), cap(s)) // 2 2
s = append(s, 3)
fmt.Println(len(s), cap(s)) // 3 4
s = append(s, 4)
fmt.Println(len(s), cap(s)) // 4 4
s = append(s, 5)
fmt.Println(len(s), cap(s)) // 5 8
len
Değerinin doğrusal olarak büyüdüğü, ancak düzensiz olarak büyüdüğü görülebilir cap
. Bu diyagram, append
işlemin dilimlerin dinamik olarak yeniden boyutlandırılmasına nasıl izin verdiğini görsel olarak gösterir:

s
Başlangıçta sıfır değerinde olduğunu ve bu noktadas
henüz altta yatan bir diziye bağlanmadığını görebiliriz .- Daha sonra
append
, dilime 11 numaralı bir eleman eklenirs
, bu elemanu1
1 uzunluğunda bir temel dizi tahsis eder ve sonras
‘nin iç noktasınıarray
‘ye işaret ederu1
, bu nedenle bu noktadalen = 1, cap = 1
. - Daha sonra,
append
12’yi dilime eklemek için tekrar çağrıldığındas
, dilimin 12’yi depolayacak alanı olmadığı açıktır. Bu nedenle, yeniden boyutlandırılması gerekir.u2
2 (* 2) uzunluğunda yeni bir temel dizi oluşturuluru1 array length
ve tüm öğeleru1
‘e kopyalanıru2
, son olarak ‘array
e işaret ediliru2
ve ayarlanırlen = 2, cap = 2
. append
‘e 13 numaralı bir öğeyi eklemek için tekrar çağrıldığında ,s
dilim alanı yine yetersiz kalır ve başka bir yeniden boyutlandırma gerekir. Böylece,u3
4 ( * 2) uzunluğunda yeni bir temel dizi oluşturuluru2 array length
ve tüm öğeleru2
‘e kopyalanıru3
, sonra ‘array
e işaret ediliru3
velen = 3, cap = 4
ayarlanır.- Tekrar çağrıldığında
append
, 14. eleman ‘es
, dilime eklenircap = 4
ve yeterli alan vardır. Bu nedenle, 14 bir sonraki eleman pozisyonuna, yaniu3
‘in sonuna yerleştirilir ves
‘in iç değerilen
1 artırılarak 4 olur. - Son olarak,
append
‘e 15 eklemek için tekrar çağrıldığındas
, dilimlen = 4, cap = 4
ve alan yine yetersiz kalır, başka bir yeniden boyutlandırma gerekir. Böylece,u4
8 ( * 2) uzunluğunda yeni bir temel dizi oluşturuluru3 array length
ve tüm öğeleru3
‘e kopyalanıru4
, sonra ‘array
e işaret ediliru4
velen = 5, cap = 8
ayarlanır.
Dilimin altta yatan dizi alanı yetersiz olduğunda, dilimin ihtiyaç halinde otomatik olarak yeni bir altta yatan diziye tahsis edileceği gözlemlenebilir append
. Yeni dizi uzunluğu belirli bir algoritmaya göre genişletilecektir ( growslice
işlevdeki işlevi inceleyin $GOROOT/src/runtime/slice.go
). Yeni dizi oluşturulduktan sonra, eski dizideki tüm veriler yeni diziye kopyalanır ve ardından array
yeni diziyi işaret ederek yeni diziyi dilimin altta yatan dizisi yaparken, eski dizi tarafından geri alınacaktır gc
.
Ancak, kullanarak bir dilim oluşturursak slicing an array
, dilim cap
üst sınıra değdiğinde ve dilim üzerinde bir işlem yaptığımızda append
, dilim orijinal diziden ayrılmış olacaktır.
u := [6]int{1, 2, 3, 4, 5, 6}
s := u[2:4]
fmt.Printf("u = %v, s = %v len(s) = %d, cap(s) = %d\n", u, s, len(s), cap(s))
s = append(s, 7)
fmt.Printf("append 7: u = %v, s = %v len(s) = %d, cap(s) = %d\n", u, s, len(s), cap(s))
s = append(s, 8)
fmt.Printf("append 8: u = %v, s = %v len(s) = %d, cap(s) = %d\n", u, s, len(s), cap(s))
s = append(s, 9)
fmt.Printf("append 9: u = %v, s = %v len(s) = %d, cap(s) = %d\n", u, s, len(s), cap(s))
// output:
// u = [1 2 3 4 5 6], s = [3 4] len(s) = 2, cap(s) = 4
// append 7: u = [1 2 3 4 7 6], s = [3 4 7] len(s) = 3, cap(s) = 4
// append 8: u = [1 2 3 4 7 8], s = [3 4 7 8] len(s) = 4, cap(s) = 4
// append 9: u = [1 2 3 4 7 8], s = [3 4 7 8 9] len(s) = 5, cap(s) = 8
// append 10: u = [1 2 3 4 7 8], s = [3 4 7 8 9 10] len(s) = 6, cap(s) = 8
cap(s)
Çıktı sonuçlarından, 9. öğe eklendikten sonra 8’e eşit olduğu, bunun dilimin dinamik yeniden boyutlandırılmasını tetiklediği ve bu noktada dilimin s
artık dizisine bağlı olmadığı u
, yani s
artık bir tanımlayıcı olmadığı görülebilir u
.
Dilimler Oluştururken Cap Parametresini Kullanmayı Deneyin
İşlem, append
dilimlerin sıfır değerli kullanılabilirlik kavramını desteklemesini sağlayan ve dilimlerin kullanımını kolaylaştıran güçlü bir araçtır.
Ancak, prensipten, her yeniden boyutlandırma gerçekleştiğinde, eski temel dizideki tüm öğelerin yeni temel diziye kopyalanması gerektiğini görebiliriz. Bu adımda tüketilen kaynaklar, özellikle çok sayıda öğe olduğunda, hala önemlidir. Peki, aşırı bellek ayırma ve öğeleri kopyalama maliyetlerinden nasıl kaçınabiliriz?
Etkili bir çözüm, kod yazarken dilimin ihtiyaç duyduğu kapasiteyi bilinçli bir şekilde tahmin etmek ve dilimi oluştururken cap
parametreyi yerleşik fonksiyona geçirmektir.make
s := make([]T, len, cap)
Aşağıda iki durum için bir performans testi bulunmaktadır:
const sliceSize = 10000
func BenchmarkSliceInitWithoutCap(b *testing.B) {
for n := 0; n < b.N; n++ {
sl := make([]int, 0)
for i := 0; i < sliceSize; i++ {
sl = append(sl, i)
}
}
}
func BenchmarkSliceInitWithCap(b *testing.B) {
for n := 0; n < b.N; n++ {
sl := make([]int, 0, sliceSize)
for i := 0; i < sliceSize; i++ {
sl = append(sl, i)
}
}
}
Sonuçlar şöyle:
goos: windows
goarch: amd64
pkg: prometheus_for_go
cpu: AMD Ryzen 7 5800H with Radeon Graphics
BenchmarkSliceInitWithoutCap
BenchmarkSliceInitWithoutCap-16 34340 34443 ns/op
BenchmarkSliceInitWithCap
BenchmarkSliceInitWithCap-16 106251 11122 ns/op
PASS
Dilimler için parametrenin kullanılmasının, sırasında cap
ortalama bir performansa sahip olduğu görülebilir ; bu, parametrenin kullanılmamasına göre yaklaşık üç kat daha fazladır .11122 ns/op
append
cap
cap
Bu nedenle dilimler oluşturulurken parametrenin eklenmesi önerilir .