Go dilinde array (dizi) sabit uzunluklu bir değerdir ve uzunluğu tipinin parçasıdır: [3]int ile [4]int iki ayrı tiptir. Slice (dilim) ise bir dizinin bir bölümünü tarif eden küçük bir başlıktır: bir işaretçi, bir uzunluk ve bir kapasite taşır. Diziyi atadığınızda ya da fonksiyona verdiğinizde tüm elemanlar kopyalanır; dilimi atadığınızda yalnızca başlık kopyalanır ve iki dilim aynı alttaki diziyi paylaşır. Günlük Go kodunda neredeyse her zaman dilim kullanılır, dizi çoğunlukla dilimin altındaki depolama alanıdır.

Bu yazıda iki tipin bellekteki görünüşünü, len ve cap değerlerinin nasıl hesaplandığını, dilimleme ifadelerinin kurallarını, nil dilim ile boş dilim arasındaki farkı ve en sık yapılan hataları Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Eleman ekleme ve kopyalama ayrıntıları (append, copy, kapasite büyümesi) için ayrı bir yazımız var: Go slice append ve copy. Serinin tamamı için Go öğrenme rehberi sayfasına bakabilirsiniz.

Dizi (array): uzunluğu tipine dahil bir değer

Dizi (array): uzunluğu tipine dahil bir değer

Spesifikasyona göre dizi, tek bir eleman tipinden oluşan numaralı bir eleman dizisidir ve eleman sayısı negatif olamaz. Uzunluk tipin parçası olduğu için derleme zamanında bilinen, int ile temsil edilebilen sabit bir ifade olmak zorundadır. Bu yüzden çalışma anında hesaplanan bir sayıyla [n]int tipinde dizi tanımlayamazsınız; böyle bir ihtiyaçta dilim kullanılır.

Go dizileri C dizilerinden farklıdır: dizi değişkeni ilk elemanı gösteren bir işaretçi değil, dizinin tamamıdır. Effective Go bunu üç maddeyle özetler: diziler değerdir ve bir diziyi diğerine atamak bütün elemanları kopyalar; bir diziyi fonksiyona verdiğinizde fonksiyon dizinin kopyasını alır; dizinin boyutu tipinin parçasıdır. Aşağıdaki örnek bu davranışları ve dizilerin == ile karşılaştırılabildiğini gösteriyor:

go
package main

import "fmt"

func main() {
	a := [3]int{10, 20, 30}
	b := a // dizinin tamamı kopyalanır
	b[0] = 99

	fmt.Println(a, b)
	fmt.Println(len(a), a == [3]int{10, 20, 30})

	gunler := [...]string{"Pzt", "Sal", "Çar"}
	fmt.Printf("%T %d\n", gunler, len(gunler))

	var bos [4]bool
	fmt.Println(bos)
}

// Çıktı:
// [10 20 30] [99 20 30]
// 3 true
// [3]string 3
// [false false false false]

b := a satırında üç elemanın hepsi kopyalandığı için b[0] değişikliği a dizisine yansımaz. [...] yazımında uzunluğu derleyici eleman sayısından çıkarır; tip yine sabit uzunlukludur ([3]string). Açıkça başlatılmayan dizi kullanıma hazırdır ve tüm elemanları kendi tiplerinin sıfır değerini alır. Dizi tiplerinin eleman tipi karşılaştırılabiliyorsa dizi de karşılaştırılabilir; karşılaştırma indeks sırasıyla yapılır ve ilk farklı elemanda durur.

Uzunluk tipin parçası olduğu için farklı uzunluktaki iki dizi birbirine atanamaz. Aşağıdaki program derlenmez:

go
package main

func main() {
	var a [3]int
	var b [4]int
	a = b
	_ = a
}
text
# command-line-arguments
./main.go:6:6: cannot use b (variable of type [4]int) as [3]int value in assignment

Diziler ne zaman işe yarar? Effective Go, bellek düzenini ayrıntılı planlamanız gereken durumlarda ve bazen ayırmadan (allocation) kaçınmak için dizilerin faydalı olduğunu, ama asıl görevlerinin dilimlere yapı taşı olmak olduğunu söyler. Dönüşüm matrisi gibi boyutu baştan belli olan veriler, sabit boyutlu özet değerleri (ör. [32]byte) ve map anahtarı olarak kullanılacak sabit uzunluklu kayıtlar tipik dizi kullanım alanlarıdır.

Dilim (slice): bir dizinin parçasını tarif eden başlık

Dilim tipi []T şeklinde yazılır ve uzunluk içermez. Spesifikasyon dilimi, alttaki bir dizinin bitişik bir bölümünü tarif eden bir tanımlayıcı (descriptor) olarak tanımlar. Go Blog yazıları bu tanımlayıcıya slice header der ve içinde üç bilgi bulunduğunu anlatır: dizideki bir elemanı gösteren işaretçi, dilimin uzunluğu ve kapasitesi. Bu başlık programcıya görünen bir struct değildir, ama davranışı anlamak için böyle düşünmek doğru sonuç verir.

Uzunluk (len) dilimin eriştiği eleman sayısıdır. Kapasite (cap) dilimin uzunluğu ile alttaki dizide dilimin bittiği yerden sonra kalan eleman sayısının toplamıdır; başka bir deyişle dilimin ilk elemanından alttaki dizinin sonuna kadar olan alan. Her zaman 0 <= len(s) <= cap(s) ilişkisi geçerlidir. Dilimleme veri kopyalamaz; yeni başlık aynı diziyi gösterir:

go
package main

import "fmt"

func main() {
	dizi := [6]string{"a", "b", "c", "d", "e", "f"}
	s := dizi[1:4]

	fmt.Println(s, len(s), cap(s))

	s[0] = "B" // dizinin 1. elemanı değişir
	fmt.Println(dizi)

	t := s[:cap(s)] // kapasiteye kadar yeniden dilimleme
	fmt.Println(t, len(t), cap(t))

	u := s[1:2]
	fmt.Println(u, len(u), cap(u))
}

// Çıktı:
// [b c d] 3 5
// [a B c d e f]
// [B c d e f] 5 5
// [c] 1 4

dizi[1:4] yarı açık bir aralıktır: 1 dahil, 4 hariç, yani üç eleman. Kapasite 5 çıkar çünkü dilim dizinin 1. konumundan başlıyor ve dizinin sonuna kadar beş eleman var. s[0] = "B" ataması dizinin kendisini değiştirir, çünkü ortada tek bir depolama alanı vardır. s[:cap(s)] ile dilimi kapasitesine kadar yeniden genişletebilirsiniz; bu, kapasite sınırını aşmadığınız sürece geçerlidir. Son satırda s[1:2] diliminin kapasitesi 4 olur: dilimin başlangıcı ileri kaydıkça kapasite azalır.

Dilimleme ifadelerinin kuralları

Dilimleme ifadelerinin kuralları

Basit dilimleme ifadesi a[low:high] biçimindedir. İki indeksten biri ya da ikisi atlanabilir: eksik low sıfır, eksik high dilimlenen değerin uzunluğu kabul edilir. Diziler ve stringler için indeksler 0 <= low <= high <= len(a) aralığında olmalıdır; dilimler için üst sınır uzunluk değil kapasitedir. Bu ayrıntı önemlidir: bir dilimi uzunluğunun ötesine, kapasitesine kadar yeniden dilimleyebilirsiniz ama kapasiteyi aşamazsınız.

Üçüncü bir indeks alan tam dilimleme ifadesi a[low:high:max] elde edilen dilimin kapasitesini max - low olarak sınırlar. Stringlerde kullanılamaz. Bu biçimin en önemli kullanım alanı, bir dilimi başkasına verirken onun ortak diziye yazmasını engellemektir; ayrıntılı örneği append ve copy yazısında bulabilirsiniz.

İndeksler çalışma anında aralık dışına çıkarsa program panikler. Kapasitesi 5 olan bir dilimi 6. konuma kadar dilimlemeye çalışmak şu sonucu verir:

go
package main

import "fmt"

func main() {
	s := make([]int, 3, 5)
	i := 6
	fmt.Println(s[:i])
}
text
panic: runtime error: slice bounds out of range [:6] with capacity 5

Sabit indekslerde derleyici bu hatayı daha derlerken yakalar; örnekte indeksi bir değişkene koymamızın nedeni budur. Bir dizi dilimlenecekse dizinin adreslenebilir olması gerekir, bu yüzden bir fonksiyonun döndürdüğü dizi değerini doğrudan dilimleyemezsiniz; önce bir değişkene atamanız gerekir.

Fonksiyona geçirme: kopya dizi, kopya başlık

Go SSS sayfası Go dilinde her şeyin değerle geçirildiğini vurgular: fonksiyon, kendisine verilen şeyin bir kopyasını alır. Dizi için bu kopya bütün elemanlardır. Dilim için ise kopyalanan şey yalnızca başlıktır; başlığın içindeki işaretçi aynı diziyi gösterdiği için fonksiyon elemanları değiştirebilir, ama başlığın uzunluk ve kapasite alanlarını çağıranın değişkeninde değiştiremez:

go
package main

import "fmt"

func diziArttir(d [3]int) {
	for i := range d {
		d[i]++
	}
}

func dilimArttir(s []int) {
	for i := range s {
		s[i]++
	}
}

func dilimUzat(s []int) {
	s = append(s, 100)
	fmt.Println("fonksiyon içinde:", s)
}

func main() {
	d := [3]int{1, 2, 3}
	diziArttir(d)
	fmt.Println("dizi:", d)

	s := []int{1, 2, 3}
	dilimArttir(s)
	fmt.Println("dilim:", s)

	dilimUzat(s)
	fmt.Println("çağıran:", s, len(s))
}

// Çıktı:
// dizi: [1 2 3]
// dilim: [2 3 4]
// fonksiyon içinde: [2 3 4 100]
// çağıran: [2 3 4] 3

diziArttir kopya üzerinde çalıştığı için çağıranın dizisi aynı kalır. dilimArttir elemanları doğrudan alttaki dizide değiştirdiği için değişiklik görünür. dilimUzat içindeki append ise yerel başlık kopyasının uzunluğunu artırır; çağıranın s değişkeni hâlâ üç uzunluklu eski başlığı tutar. Bu yüzden dilimin uzunluğunu değiştiren fonksiyonlar yeni dilimi döndürür ve çağıran onu yeniden atar: s = append(s, x) deyimi bu kuralın en bilinen örneğidir. Alternatif olarak dilime işaretçi (*[]int) alabilirsiniz; Go Blog, dilimi değiştiren metotlarda işaretçi alıcı kullanmanın deyimsel olduğunu belirtir.

Büyük bir diziyi kopyalamadan fonksiyona vermek isterseniz diziye işaretçi geçirebilirsiniz. Ancak Effective Go bu tarzın da deyimsel olmadığını, bunun yerine dilim kullanılmasını önerir. Aşağıdaki örnekte range doğrudan dizi işaretçisi üzerinde çalışır; ayrıca Go dilimden diziye ve dizi işaretçisine dönüşümü destekler:

go
package main

import "fmt"

func topla(a *[3]float64) (t float64) {
	for _, v := range a {
		t += v
	}
	return
}

func main() {
	s := []byte{'g', 'o', 'l', 'a', 'n', 'g'}

	a := [2]byte(s) // kopya: dilimin ilk 2 elemanı
	p := (*[2]byte)(s[4:])

	s[0] = 'G'
	p[0] = 'N' // aynı alttaki diziyi gösterir
	fmt.Println(string(a[:]), string(s))

	notlar := [...]float64{7.0, 8.5, 9.1}
	fmt.Println(topla(&notlar))
}

// Çıktı:
// go GolaNg
// 24.6

[2]byte(s) dönüşümü dilimin ilk iki elemanını yeni bir diziye kopyalar, bu yüzden sonradan s[0] değişse de a etkilenmez. (*[2]byte)(s[4:]) ise kopya üretmez, alttaki diziyi gösteren bir işaretçi verir ve onun üzerinden yapılan yazma s içinde görünür. Spesifikasyona göre her iki dönüşümde de dilimin uzunluğu dizi uzunluğundan küçükse çalışma anında panik oluşur.

nil dilim, boş dilim ve make

Dilimin sıfır değeri nildir. nil dilimin uzunluğu ve kapasitesi sıfırdır; range ile gezildiğinde hiç döngü çalışmaz ve append ile büyütülebilir. []int{} yazımı ise başlatılmış ama boş bir dilim üretir: uzunluğu yine sıfırdır, ancak nil değildir. Go Blog bu farkı başlıktaki işaretçinin nil olup olmamasıyla açıklar.

go
package main

import "fmt"

func main() {
	var nilDilim []int
	bosDilim := []int{}

	fmt.Println(nilDilim == nil, len(nilDilim), cap(nilDilim))
	fmt.Println(bosDilim == nil, len(bosDilim), cap(bosDilim))
	fmt.Println(nilDilim, bosDilim)

	nilDilim = append(nilDilim, 7)
	fmt.Println(nilDilim)

	// İndeksli dilim literali: verilmeyen konumlar sıfır değer alır.
	seviye := []string{2: "yüksek", 0: "düşük"}
	fmt.Printf("%q %d\n", seviye, len(seviye))

	m := make([]int, 2, 5)
	fmt.Println(m, len(m), cap(m))
}

// Çıktı:
// true 0 0
// false 0 0
// [] []
// [7]
// ["düşük" "" "yüksek"] 3
// [0 0] 2 5

İki dilim de fmt ile [] olarak yazdırılır; farkı yalnızca == nil karşılaştırması ve bazı kodlama paketleri ortaya çıkarır. Örneğin encoding/json belgelerine göre nil dilim JSON null olarak kodlanır, boş dilim ise boş bir JSON dizisi olur; ayrıntılar için Go encoding/json yazısına bakın. Çoğu durumda fonksiyonlarınızın ikisini de aynı şekilde ele alması, yani s == nil yerine len(s) == 0 kontrolü yapması daha sağlamdır.

Dilim literallerinde elemanlara indeks anahtarı verebilirsiniz; anahtarsız eleman bir önceki indeksin bir fazlasını alır, atlanan konumlar sıfır değerle dolar. Örnekteki seviye diliminde 1. konum boş string olarak kalır ve uzunluk en büyük indeksin bir fazlası olan 3 olur.

make([]T, len, cap) yerleşik fonksiyonu yeni, gizli bir dizi ayırır ve onu gösteren bir dilim döndürür. Kapasite verilmezse uzunluğa eşit kabul edilir. Spesifikasyon make([]int, 50, 100) ile new([100]int)[0:50] ifadelerinin aynı dilimi ürettiğini söyler; bu eşitlik dilimin bir dizi üzerine kurulduğunu bir kez daha gösterir. make ile new arasındaki genel fark için Go pointer yazısına bakabilirsiniz.

Çok boyutlu yapılar

Go dizileri ve dilimleri her zaman tek boyutludur; çok boyutlu yapı kurmak için dizi dizisi ya da dilim dilimi oluşturursunuz. Dizi dizisinde iç diziler yapı gereği hep aynı uzunluktadır ve tek bir bitişik blok olarak durur. Dilim diliminde ise her iç dilimin uzunluğu farklı olabilir ve her biri ayrıca başlatılmalıdır:

go
package main

import "fmt"

func main() {
	satir, sutun := 2, 3
	tablo := make([][]int, satir)
	for i := range tablo {
		tablo[i] = make([]int, sutun)
	}
	tablo[1][2] = 5

	var matris [2][3]int
	matris[1][2] = 5

	fmt.Println(tablo, matris)
	fmt.Println(len(tablo[0]), len(matris[0]))
}

// Çıktı:
// [[0 0 0] [0 0 5]] [[0 0 0] [0 0 5]]
// 3 3

Effective Go, iki boyutlu dilim ayırmanın iki yolunu anlatır: her satırı ayrı ayrı make ile ayırmak ya da tek büyük bir dilim ayırıp satırları onun parçaları olarak dilimlemek. Satırlar büyüyüp küçülecekse ayrı ayrı ayırmak, birbirinin üzerine yazmayı önler; boyut sabitse tek ayırma daha verimli olabilir.

Dizi ve dilim karşılaştırması

Dizi ve dilim karşılaştırması: Uzunluk, Atama ve fonksiyona geçirme, Sıfır değer, == ile karşılaştırma
ÖzellikDizi [n]TDilim []T
UzunlukTipin parçası, derleme zamanında sabitÇalışma anında değişebilir
Atama ve fonksiyona geçirmeTüm elemanlar kopyalanırYalnızca başlık kopyalanır, dizi paylaşılır
Sıfır değerElemanları sıfır olan hazır dizinil (len ve cap 0)
== ile karşılaştırmaEleman tipi karşılaştırılabiliyorsa evetHayır, yalnızca nil ile
Map anahtarı olabilir miEvet (karşılaştırılabilirse)Hayır
cap değeriHer zaman len ile aynılen ile alttaki dizinin kalanı
Tipik kullanımSabit boyutlu veri, özet değerleri, dilimin deposuNeredeyse tüm dizi işleri

Tablodaki karşılaştırılabilirlik farkı pratikte önemlidir: dizi map anahtarı olabilir, dilim olamaz. Go SSS sayfası bunun nedenini dilimler için eşitliğin iyi tanımlanmamış olmasıyla açıklar (sığ mı derin mi karşılaştırma, işaretçi mi değer mi gibi sorular). Map tarafındaki kurallar için Go map kullanımı yazısına bakabilirsiniz.

Sık yapılan hatalar

  • Diziyi dilim sanmak: [3]int parametresi alan bir fonksiyonda elemanları değiştirip çağıranda değişiklik beklemek. Dizi kopyalanır; değişikliğin görünmesi için dilim ya da işaretçi kullanın.
  • Dilimin bağımsız bir kopya olduğunu düşünmek: t := s[1:3] sonrasında t üzerinden yapılan yazma s içinde de görünür. Bağımsız kopya için slices.Clone veya copy kullanın.
  • append sonucunu atamamak: dilimi büyüten bir fonksiyonun yeni dilimi döndürmesi ve çağıranın onu ataması gerekir. Derleyici kullanılmayan append çağrısını reddeder.
  • len ile cap farkını unutmak: make([]int, 5) beş sıfır içeren bir dilimdir; üzerine append yaptığınızda yeni eleman 6. konuma eklenir.
  • Kapasiteyi aşan dilimleme: s[:n] ifadesinde n değeri cap(s) değerinden büyükse çalışma anında panik oluşur.
  • nil kontrolüne güvenmek: boş dilimin nil olmadığını unutup s == nil ile boşluk kontrolü yapmak. len(s) == 0 her iki durumu da kapsar.

Bu hataların derleyici ve çalışma zamanı mesajlarıyla birlikte toplu bir listesi Go sık karşılaşılan hatalar yazısında yer alıyor. Dilimler üzerinde sıralama, arama ve karşılaştırma yapan hazır fonksiyonlar için slices, maps ve sort paketleri yazısına göz atabilirsiniz.

Sık Sorulan Sorular

Go dilinde dizi mi dilim mi kullanmalıyım?

Uzunluk çalışma anında değişebiliyorsa ya da bir fonksiyona veri kümesi geçiriyorsanız dilim kullanın. Boyutun tipin parçası olmasının anlamlı olduğu durumlarda (sabit uzunluklu özet, matris, map anahtarı olacak kayıt) dizi seçin. Effective Go açık boyutlu matrisler dışında dizi programlamanın büyük kısmının dilimlerle yapıldığını belirtir.

Dilim bir işaretçi midir?

Hayır. Dilim, içinde işaretçi bulunan bir değerdir. Fonksiyona verildiğinde başlık kopyalanır; elemanlar paylaşılır, ama uzunluk ve kapasite çağıranda değişmez. Bu yüzden uzunluğu değiştiren işlemler yeni dilimi döndürür.

cap neden len değerinden büyük olabiliyor?

Kapasite, dilimin ilk elemanından alttaki dizinin sonuna kadar olan alanı ölçer. Dilim dizinin sadece bir bölümünü gösterdiğinde ya da make ile fazladan yer ayrıldığında kapasite uzunluktan büyük olur. Bu ek alan, yeni dizi ayırmadan append yapılabilmesini sağlar.

İki dilimi == ile karşılaştırabilir miyim?

Hayır; dilim tipleri karşılaştırılabilir değildir, yalnızca nil ile karşılaştırılabilir. İçerik karşılaştırması için slices.Equal fonksiyonunu kullanabilirsiniz. Diziler ise eleman tipi karşılaştırılabiliyorsa == ile doğrudan karşılaştırılır.

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 slice append ve copy · Go map kullanımı · Go string, rune ve byte · Go for döngüsü ve range · Go slices ve maps paketleri · Go nedir?