Go dilinde bir dilime eleman eklemek için yerleşik append fonksiyonu, elemanları bir dilimden diğerine aktarmak için yerleşik copy fonksiyonu kullanılır. append her zaman sonucu döndürür ve siz bu sonucu yeniden atamalısınız: s = append(s, x). Kapasite yetiyorsa eleman mevcut alttaki diziye yazılır, yetmiyorsa yeni ve daha büyük bir dizi ayrılır ve eski elemanlar oraya taşınır. copy(dst, src) ise iki dilimin uzunluklarından küçük olanı kadar eleman kopyalar ve kopyalanan sayıyı döndürür.

Kural kısa olsa da iki dilimin aynı diziyi paylaşması yüzünden şaşırtıcı sonuçlar çıkabilir. Bu yazıda kapasite büyümesini, ortak dizi tuzağını ve üç indeksli dilimle çözümünü, copy fonksiyonunun örtüşen dilimlerdeki davranışını, araya ekleme ve silme yöntemlerini ve bellek tutma sorununu Go 1.27.3 ile çalıştırılmış örneklerle ele alıyoruz. Dilimin ne olduğu, len ve cap kavramları için önce Go array ve slice farkı yazısını okumanızı öneririz. Serinin tamamı Go öğrenme rehberi sayfasında.

append nasıl çalışır?

append nasıl çalışır?

Spesifikasyondaki imza şematik olarak append(s S, x ...E) S şeklindedir: S dilim tipi, E onun eleman tipidir. Fonksiyon sıfır ya da daha fazla değeri dilimin sonuna ekler ve aynı tipte bir dilim döndürür. Spesifikasyonun belirlediği davranış iki durumdan ibarettir: s diliminin kapasitesi yeni değerleri sığdırmaya yetmiyorsa append hem eski elemanları hem yenileri alacak büyüklükte yeni bir alttaki dizi ayırır; yetiyorsa mevcut diziyi yeniden kullanır.

Effective Go, append fonksiyonunun yerleşik olmasını, eleman tipi çağıran tarafından belirlenen bu fonksiyonun derleyici desteğine ihtiyaç duymasıyla açıklar; tek bir append her dilim tipiyle çalışır. Sonucun döndürülmesi şarttır, çünkü alttaki dizi değişmiş olabilir ve dilim başlığı (işaretçi, uzunluk, kapasite) değerle geçirilir. Derleyici, sonucu kullanılmayan bir append çağrısını kabul etmez:

go
package main

func main() {
	s := []int{1}
	append(s, 2)
}
text
# command-line-arguments
./main.go:5:2: append(s, 2) (value of type []int) is not used

Kapasite büyümesi: ne zaman yeni dizi ayrılır?

Aşağıdaki program boş bir dilime on eleman ekliyor ve kapasitenin değiştiği her adımı yazdırıyor. Kapasite değiştiğinde dilim artık farklı bir alttaki diziyi gösterir:

go
package main

import "fmt"

func main() {
	var s []int
	eskiCap := cap(s)
	for i := range 10 {
		s = append(s, i)
		if cap(s) != eskiCap {
			fmt.Printf("len=%d cap=%d (yeni dizi)\n", len(s), cap(s))
			eskiCap = cap(s)
		}
	}
	fmt.Println(s)
}

// Go 1.27.3 windows/amd64 ile alınan çıktı
// (büyüme adımları dil tarafından garanti edilmez):
// len=1 cap=1 (yeni dizi)
// len=2 cap=2 (yeni dizi)
// len=3 cap=4 (yeni dizi)
// len=5 cap=8 (yeni dizi)
// len=9 cap=16 (yeni dizi)
// [0 1 2 3 4 5 6 7 8 9]

Bu çıktıdaki sayıları bir kural olarak ezberlemeyin. Spesifikasyon yalnızca "yeterince büyük" yeni bir dizi ayrılacağını söyler, büyüme oranını belirlemez; değerler Go sürümüne ve eleman tipine göre farklı olabilir. Programınız belirli bir kapasiteye güvenmemeli, yalnızca len ve cap değerlerinin gerçekte ne döndürdüğüne bakmalıdır.

Kaç eleman ekleyeceğinizi önceden biliyorsanız kapasiteyi baştan ayırmak tekrar tekrar yeni dizi ayrılmasını önler. Bunun için make([]T, 0, n) kullanabilir ya da mevcut bir dilim için slices.Grow(s, n) çağırabilirsiniz. slices paketinin belgesine göre Grow, en az n elemanın yeni ayırma olmadan eklenebileceğini garanti eder ve n negatifse panikler.

Birden fazla eleman, dilim ve string ekleme

append değişken sayıda argüman alır. Bir dilimin tamamını eklemek için çağrı noktasında ... ile dilimi argüman listesine açarsınız. Effective Go, ... olmadan bu çağrının derlenmeyeceğini, çünkü ikinci argümanın tipinin eleman tipi olmadığını belirtir. Spesifikasyon bir özel durum daha tanımlar: []byte tipine eklerken ikinci argüman olarak ... ile birlikte bir string verebilirsiniz; bu durumda stringin baytları eklenir.

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	a := []string{"elma", "armut"}
	b := []string{"kiraz", "erik"}
	a = append(a, b...) // bir dilimi açarak ekleme
	fmt.Println(a)

	var bayt []byte
	bayt = append(bayt, "Go"...) // string baytlarını ekleme
	fmt.Println(bayt)

	// Araya ekleme ve silme
	s := []int{1, 2, 4, 5}
	s = slices.Insert(s, 2, 3)
	fmt.Println(s)

	s = slices.Delete(s, 0, 2)
	fmt.Println(s, len(s))

	// Elle silme: i indeksindeki elemanı çıkar
	t := []int{10, 20, 30, 40}
	i := 1
	t = append(t[:i], t[i+1:]...)
	fmt.Println(t)
}

// Çıktı:
// [elma armut kiraz erik]
// [71 111]
// [1 2 3 4 5]
// [3 4 5] 3
// [10 30 40]

Örnekteki [71 111] çıktısı "Go" stringinin iki baytıdır. Aynı programın ikinci yarısı araya ekleme ve silme işlemlerini gösteriyor; bunlara aşağıda ayrıca değiniyoruz.

Ortak dizi tuzağı

append kapasite yettiğinde mevcut diziye yazdığı için, aynı tabandan türetilmiş iki dilim birbirinin verisini ezebilir. Bu, dilimlerle ilgili en sık karşılaşılan şaşırtıcı davranıştır:

go
package main

import "fmt"

func main() {
	taban := make([]int, 3, 10)
	a := append(taban, 1)
	b := append(taban, 2) // aynı alttaki dizinin 3. konumuna yazar

	fmt.Println(a, b)
	fmt.Println(len(taban), cap(taban))
}

// Çıktı:
// [0 0 0 2] [0 0 0 2]
// 3 10

taban diliminin uzunluğu 3, kapasitesi 10 olarak ayarlandı. a := append(taban, 1) kapasite yettiği için 1 değerini alttaki dizinin 3. konumuna yazar. b := append(taban, 2) de aynı başlıktan başlar ve aynı konuma 2 yazar. Sonuçta a ile b aynı diziyi gösteren, aynı içeriğe sahip iki dilim olur ve a[3] değeri artık 1 değildir. taban değişkeninin kendisi değişmez; uzunluğu hâlâ üçtür.

Bu tuzak gerçek kodda genellikle şu biçimde ortaya çıkar: bir fonksiyon kendisine verilen dilime append yapıp sonucu saklar, çağıran da aynı dilime başka bir şey ekler. Kapasite fazlası varsa iki taraf aynı belleğe yazar.

Üç indeksli dilimle kapasiteyi sınırlama

Tam dilimleme ifadesi a[low:high:max] elde edilen dilimin kapasitesini max - low olarak ayarlar. Kapasiteyi uzunluğa eşitlediğinizde bir sonraki append mevcut diziye yazamaz ve yeni dizi ayırmak zorunda kalır:

go
package main

import "fmt"

func main() {
	taban := make([]int, 3, 10)
	kirpik := taban[0:3:3] // len 3, cap 3

	a := append(kirpik, 1) // kapasite dolu: yeni dizi
	b := append(kirpik, 2) // yine yeni dizi
	fmt.Println(a, b)
	fmt.Println(len(kirpik), cap(kirpik))
}

// Çıktı:
// [0 0 0 1] [0 0 0 2]
// 3 3

Burada kirpik diliminin uzunluğu ve kapasitesi üçtür. Her iki append de yeni bir dizi ayırdığı için a ile b birbirinden bağımsızdır. Bir dilimi başka bir koda verirken o kodun sizin dizinize yazmasını istemiyorsanız s[:len(s):len(s)] biçimini kullanabilirsiniz. slices.Clip(s) fonksiyonu tam olarak bunu yapar; paket belgesine göre s[:len(s):len(s)] döndürür.

copy: kaç eleman kopyalanır?

copy(dst, src) elemanları kaynaktan hedefe kopyalar ve kopyalanan eleman sayısını döndürür. Bu sayı len(src) ile len(dst) değerlerinden küçük olanıdır; kapasite değil uzunluk dikkate alınır. İki argümanın eleman tipi aynı olmalıdır. Özel bir durum olarak hedef []byte ise kaynak bir string olabilir. Spesifikasyon, kaynak ile hedefin aynı belleği paylaştığı durumda da sonucun doğru olduğunu belirtir; bu yüzden copy bir dilim içinde elemanları kaydırmak için güvenle kullanılabilir.

go
package main

import "fmt"

func main() {
	kaynak := []int{1, 2, 3, 4, 5}

	hedef := make([]int, 3)
	n := copy(hedef, kaynak)
	fmt.Println(n, hedef)

	n = copy(kaynak, kaynak[2:]) // örtüşen dilimler
	fmt.Println(n, kaynak)

	b := make([]byte, 5)
	n = copy(b, "Merhaba")
	fmt.Println(n, string(b))

	var bos []int
	fmt.Println(copy(bos, kaynak))
}

// Çıktı:
// 3 [1 2 3]
// 3 [3 4 5 4 5]
// 5 Merha
// 0

Örnekteki son satır sık yapılan bir hatayı gösteriyor: var bos []int uzunluğu sıfır olan bir dilimdir, bu yüzden copy hiçbir şey kopyalamaz ve 0 döndürür. copy hedefi büyütmez; hedefin önceden make([]int, len(src)) gibi bir çağrıyla yeterli uzunlukta oluşturulması gerekir. Bağımsız bir kopya almanın kısa yolları slices.Clone(s) ve append([]T(nil), s...) biçimleridir. slices.Clone belgesine göre kopya sığdır (elemanlar atamayla kopyalanır) ve kaynağın nil olma durumunu korur.

Araya ekleme ve silme

Araya ekleme ve silme: Sona ekleme, Dilim ekleme, Bağımsız kopya, Hedefe kopyalama

Go dilinde dilimden eleman silmek ya da araya eleman eklemek için yerleşik bir operatör yoktur; bu işlemler append, copy veya slices paketiyle yapılır. Yukarıdaki örnekte gördüğünüz append(t[:i], t[i+1:]...) deyimi i konumundaki elemanı çıkarır: önce i öncesindeki kısım alınır, sonra sonrasındaki elemanlar aynı dizide bir konum sola kaydırılarak eklenir. Bu işlem alttaki diziyi yerinde değiştirir, yani orijinal dilimin içeriği de değişir.

Go 1.21 ile gelen slices paketi bu işlemleri adlandırılmış fonksiyonlarla sunar. slices.Insert(s, i, v...) değerleri i konumuna ekler ve i değeri len(s) değerinden büyükse panikler. slices.Delete(s, i, j) s[i:j] aralığını çıkarır; belgeye göre işlem O(len(s)-i) maliyetindedir ve dilimin sonunda boşalan elemanları sıfırlar. Çok sayıda eleman silinecekse belge, bunları tek tek değil tek bir çağrıyla silmeyi önerir. Paketin diğer fonksiyonları için Go slices, maps ve sort paketleri yazısına bakın.

İşlemDeyimNot
Sona eklemes = append(s, x)Kapasite yetmezse yeni dizi
Dilim eklemes = append(s, t...)t aynı tipte dilim olmalı
Bağımsız kopyac := slices.Clone(s)Sığ kopya, nil durumu korunur
Hedefe kopyalaman := copy(dst, src)min(len(dst), len(src)) eleman
i konumunu silmes = slices.Delete(s, i, i+1)Yerinde değişir, sondaki eleman sıfırlanır
Araya eklemes = slices.Insert(s, i, v)i > len(s) ise panik
Kapasiteyi kırpmas = s[:len(s):len(s)]Sonraki append yeni dizi ayırır
Önceden yer ayırmas := make([]T, 0, n)n elemana kadar yeni ayırma yok

make ile uzunluk ve kapasite karıştırmak

make([]int, 3) çağrısı uzunluğu 3 olan, yani zaten üç sıfır içeren bir dilim oluşturur. Bu dilime append yaptığınızda yeni elemanlar dördüncü konumdan itibaren eklenir. Amacınız boş bir dilimle başlayıp yer ayırmaksa uzunluğu sıfır verin:

go
package main

import "fmt"

func main() {
	s := make([]int, 3) // len 3: üç sıfır zaten içeride
	s = append(s, 1, 2)
	fmt.Println(s)

	d := make([]int, 0, 3) // len 0, cap 3
	d = append(d, 1, 2)
	fmt.Println(d)
}

// Çıktı:
// [0 0 0 1 2]
// [1 2]

Kural basit: dilimi indeksle dolduracaksanız (s[i] = v) uzunluğu verin; append ile dolduracaksanız uzunluğu sıfır, kapasiteyi beklenen eleman sayısı yapın.

Bellek tutma sorunu

Go Blog'daki "Go Slices: usage and internals" yazısı şu durumu anlatır: bir dilimi yeniden dilimlemek alttaki diziyi kopyalamaz, dolayısıyla küçük bir parça bile tüm diziyi bellekte tutar. Dilim yaşadığı sürece çöp toplayıcı diziyi serbest bırakamaz. Büyük bir dosyayı okuyup içinden birkaç bayt döndüren bir fonksiyon, dosyanın tamamını bellekte tutmaya devam edebilir. Çözüm, ihtiyacınız olan kısmı yeni bir dilime kopyalamaktır:

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	buyuk := make([]byte, 1<<20) // 1 MiB
	copy(buyuk, "başlık")

	parca := buyuk[:3]               // 1 MiB'lik diziyi canlı tutar
	kopya := slices.Clone(buyuk[:3]) // yalnızca 3 bayt

	fmt.Println(len(parca), cap(parca))
	fmt.Println(len(kopya), cap(kopya) < 1<<20)

	onceden := make([]int, 0, 100) // kapasiteyi önceden ayır
	for i := range 100 {
		onceden = append(onceden, i)
	}
	fmt.Println(len(onceden), cap(onceden))

	g := slices.Grow([]int{1}, 50)
	fmt.Println(len(g), cap(g) >= 51)
}

// Çıktı:
// 3 1048576
// 3 true
// 100 100
// 1 true

parca yalnızca 3 elemanlı görünse de kapasitesi 1 MiB kadardır ve bu dizinin tamamını canlı tutar. slices.Clone ile alınan kopya ise yalnızca gereken baytları içeren küçük bir diziye işaret eder. Örneğin ikinci yarısı önceden kapasite ayırmanın etkisini gösteriyor: 100 elemanlık kapasiteyle başlayan dilime 100 append yapıldığında kapasite değişmez, yani yeni dizi ayrılmaz.

Eleman işaretçileri ve range değişkenleri

Bir dilim elemanının adresini aldıktan sonra append yeni bir dizi ayırırsa, eski işaretçi artık eski diziyi gösterir. Bu işaretçi üzerinden yapılan yazma yeni dilimde görünmez. Benzer biçimde for _, v := range s döngüsündeki v elemanın kopyasıdır; ona yapılan atama dilimi değiştirmez:

go
package main

import "fmt"

func main() {
	notlar := []int{40, 75, 90}
	ilk := &notlar[0]

	notlar = append(notlar, 100) // kapasite dolu: yeni dizi
	*ilk = 0

	fmt.Println(notlar[0], *ilk)

	// Döngüde eleman değiştirmek için indeks kullanın
	for i := range notlar {
		notlar[i] += 5
	}
	for _, v := range notlar {
		v *= 2 // yalnızca kopya değişir
	}
	fmt.Println(notlar)
}

// Çıktı:
// 40 0
// [45 80 95 105]

Kapasitesi 3 olan dilime dördüncü eleman eklendiğinde yeni dizi ayrıldığı için *ilk = 0 ataması yalnızca eski diziyi etkiler ve notlar[0] hâlâ 40 değerini taşır. Elemanları döngüde değiştirmek istediğinizde indeksle (notlar[i]) yazın. Döngü değişkenlerinin her yinelemede yeni oluşturulması (Go 1.22) gibi ayrıntılar Go for döngüsü ve range yazısında anlatılıyor.

Sık yapılan hatalar

Sık yapılan hatalar: Sonucu atamamak, Aynı tabandan iki append, Uzunluğu sıfır hedefe copy, make ile len vermek ve append yapmak
  • Sonucu atamamak: append(s, x) tek başına derlenmez; s = append(s, x) yazın. Sonucu başka bir değişkene atamak da ortak dizi tuzağına yol açabilir.
  • Aynı tabandan iki append: a := append(t, 1) ve b := append(t, 2) fazla kapasite varsa aynı belleğe yazar. Bağımsızlık gerekiyorsa üç indeksli dilim ya da slices.Clone kullanın.
  • Uzunluğu sıfır hedefe copy: copy hedefi büyütmez, kopyalanan eleman sayısı hedefin uzunluğuyla sınırlıdır.
  • make ile len vermek ve append yapmak: make([]int, n) sonrası append yeni elemanları n sıfırın arkasına ekler.
  • Kapasite değerlerine güvenmek: büyüme oranı dil tarafından garanti edilmez; kodunuz belirli bir cap değerine dayanmamalıdır.
  • Büyük diziden küçük parça döndürmek: alt dilim tüm diziyi canlı tutar; uzun ömürlü küçük parçaları kopyalayın.
  • append sonrası eski eleman işaretçisini kullanmak: yeni dizi ayrıldıysa işaretçi eski belleği gösterir.

Derleyici ve çalışma zamanı hata mesajlarının toplu listesi için Go sık karşılaşılan hatalar yazısına bakabilirsiniz.

Sık Sorulan Sorular

append neden yeni dilim döndürüyor?

Dilim başlığı değerle geçirildiği için append çağıranın değişkenindeki uzunluğu değiştiremez. Ayrıca kapasite yetmediğinde tamamen farklı bir dizi ayrılır. Güncel uzunluğu ve yeni diziyi taşıyan başlık ancak dönüş değeriyle size ulaşabilir.

Bir dilimin bağımsız kopyasını nasıl alırım?

slices.Clone(s) en kısa yoldur. Alternatif olarak c := make([]T, len(s)) ve copy(c, s) yazabilirsiniz. İki yöntem de sığ kopyadır: elemanlar işaretçi, map ya da dilim içeriyorsa onların gösterdiği veri paylaşılmaya devam eder.

copy örtüşen dilimlerde güvenli mi?

Evet. Spesifikasyon, append ve copy için sonucun argümanların belleği paylaşıp paylaşmamasından bağımsız olduğunu belirtir. copy(s, s[2:]) gibi bir çağrı elemanları doğru şekilde sola kaydırır.

Dilimin kapasitesini nasıl küçültürüm?

Yalnızca görünür kapasiteyi sınırlamak için s[:len(s):len(s)] veya slices.Clip(s) kullanın; bu alttaki diziyi küçültmez. Belleği gerçekten serbest bırakmak istiyorsanız elemanları yeni, küçük bir dilime kopyalayın ve eski dilime referans bırakmayı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 array ve slice farkı · Go slices, maps ve sort paketleri · Go map kullanımı · Go pointer (işaretçi) · Go sık karşılaşılan hatalar