go
Go dersleri
Go/Temeller

Diziler ve Dilimler (Slices)

Dizilerin değer semantiği, slice başlığı, append ve kapasite büyümesi.

Ders 5 / 2130 dkBaşlangıç
Bu derste öğreneceklerin
  • Diziler (arrays) ve değer semantiği
  • Slice yapısı: işaretçi, uzunluk, kapasite
  • make, append ve kapasite büyümesi
  • Dilimleme ve ortak alttaki dizi tuzakları
  • copy ve tam dilim ifadesi (a[low:high:max])
  • Çok boyutlu dilimler
  • slices paketi: Sort, Contains, Index, Reverse

Bir programın büyük kısmı veri listeleriyle uğraşır: bir dosyadaki satırlar, bir API'den dönen kullanıcılar, bir sensörden okunan ölçümler. Go'da bu listelerin neredeyse tamamı dilim (slice) ile temsil edilir. Dilimleri iyi anlamak, Go kodunu hem doğru hem de verimli yazmanın ön koşuludur. Kullanımı çok kolay görünür ama altında birkaç önemli ayrıntı saklar ve bu ayrıntıları bilmeyen geliştiriciler er ya da geç "bu değer nasıl değişti?" diye saatlerce hata ayıklar.

Dilimleri anlamanın en iyi yolu, önce üzerine kurulu oldukları diziyi (array) tanımaktır. Bir diziyi sabit sayıda gözü olan bir yumurta kolisi gibi düşünebilirsin: Koli üretildiği anda göz sayısı bellidir ve bir daha değişmez. Dilim ise bu kolinin üzerine konmuş, belirli bir aralığı gösteren hareketli bir çerçevedir. Çerçeveyi kaydırabilir, daraltabilir, yer kalmadığında daha büyük bir koliye taşınmasını isteyebilirsin. Ama çerçeve yumurtaları taşımaz, yalnızca onları gösterir.

Bu dersin sonunda bir dilimin bellekte üç alanlı küçük bir başlıktan (işaretçi, uzunluk, kapasite) oluştuğunu, append'in kapasiteyi nasıl büyüttüğünü, iki dilim aynı alttaki diziyi paylaştığında hangi hataların ortaya çıktığını ve bunları copy ile tam dilim ifadesiyle nasıl önleyeceğini bileceksin. Son olarak çok boyutlu dilimleri kuracak ve standart kütüphanedeki slices paketiyle günlük işleri birkaç satırda halledeceksin.

Diziler: sabit uzunluk ve değer semantiği

Dizi (array), aynı tipten sabit sayıda elemanın bellekte yan yana durduğu yapıdır. Go'da bir dizinin uzunluğu tipinin bir parçasıdır: [3]int ile [4]int birbirinden tamamen farklı iki tiptir ve biri diğerinin yerine kullanılamaz. Bu yüzden uzunluk derleme zamanında bilinmelidir; bir değişkenle [n]int tanımlayamazsın.

main.go
package main

import "fmt"

func main() {
	var scores [5]int // tüm elemanlar sıfır değerle başlar
	scores[0] = 90
	scores[4] = 75

	days := [3]string{"Pazartesi", "Salı", "Çarşamba"}
	primes := [...]int{2, 3, 5, 7, 11} // uzunluğu derleyici sayar
	sparse := [5]int{1: 10, 3: 30}     // indeksli literal

	fmt.Println(scores, len(scores))
	fmt.Println(days, len(days))
	fmt.Println(primes, len(primes))
	fmt.Println(sparse)
	fmt.Printf("%T ve %T farklı tiplerdir\n", days, primes)

	for i, d := range days {
		fmt.Println(i, d)
	}
}
Çıktı
[90 0 0 0 75] 5
[Pazartesi Salı Çarşamba] 3
[2 3 5 7 11] 5
[0 10 0 30 0]
[3]string ve [5]int farklı tiplerdir
0 Pazartesi
1 Salı
2 Çarşamba

Burada dört farklı tanımlama biçimi var. var scores [5]int ile beş elemanlı bir dizi oluşur ve her eleman int tipinin sıfır değeri olan 0 ile başlar. [...]int{...} yazdığında uzunluğu literaldeki eleman sayısından derleyici çıkarır; sonuç yine sabit uzunluklu bir dizidir. [5]int{1: 10, 3: 30} ise indeksli literaldir: yalnızca belirttiğin indeksler doldurulur, gerisi sıfır kalır.

Dizi sınırlarının dışına çıkmak Go'da hiçbir zaman sessizce geçmez. İndeks sabitse (scores[7]) derleyici hatayı yakalar; indeks çalışma zamanında hesaplanıyorsa program index out of range paniğiyle durur. C'deki gibi komşu belleği okuyup yazmak mümkün değildir.

Atama ve fonksiyona geçirme kopyalar

Go'da diziler değer tipleridir. Bir diziyi başka bir değişkene atadığında veya bir fonksiyona argüman olarak verdiğinde, bütün elemanlar tek tek kopyalanır. Bu davranışa değer semantiği denir ve dilimlerle aradaki en temel farktır.

main.go
package main

import "fmt"

func resetFirst(a [3]int) {
	a[0] = 0 // yalnızca kopyayı değiştirir
}

func resetFirstPtr(a *[3]int) {
	a[0] = 0 // işaretçi üzerinden orijinali değiştirir
}

func main() {
	original := [3]int{1, 2, 3}

	copied := original // üç elemanın tamamı kopyalanır
	copied[1] = 99
	fmt.Println("original:", original)
	fmt.Println("copied:  ", copied)

	resetFirst(original)
	fmt.Println("resetFirst sonrası:", original)

	resetFirstPtr(&original)
	fmt.Println("resetFirstPtr sonrası:", original)

	fmt.Println("eşit mi?", original == [3]int{0, 2, 3})
}
Çıktı
original: [1 2 3]
copied:   [1 99 3]
resetFirst sonrası: [1 2 3]
resetFirstPtr sonrası: [0 2 3]
eşit mi? true

copied := original satırından sonra bellekte birbirinden bağımsız iki blok vardır:

original             copied
┌───┬───┬───┐        ┌───┬────┬───┐
│ 1 │ 2 │ 3 │        │ 1 │ 99 │ 3 │
└───┴───┴───┘        └───┴────┴───┘
  ayrı bellek          ayrı bellek

resetFirst fonksiyonu da kendi kopyasını alır, bu yüzden original değişmez. Orijinali değiştirmek istiyorsan diziye bir işaretçi (*[3]int) geçirmen gerekir. Dizilerin değer olmasının güzel bir yan etkisi de karşılaştırılabilir olmalarıdır: Aynı tipteki iki dizi == ile eleman eleman karşılaştırılır. Bu sayede diziler bir haritada anahtar olarak da kullanılabilir.

Diziler ne zaman kullanılır?

Günlük Go kodunda doğrudan dizi yazmak nadirdir, çünkü uzunluğun sabit olması çoğu durumda kısıtlayıcıdır. Yine de dizinin tam olarak doğru araç olduğu yerler vardır:

  • Boyutu doğası gereği sabit olan veriler: bir RGB rengi [3]uint8, bir IPv4 adresi [4]byte, satranç tahtası [8][8]Piece.
  • Hash değerleri: sha256.Sum256 fonksiyonu [32]byte döndürür.
  • Harita anahtarı olarak kullanılacak sabit boyutlu bileşik değerler, örneğin map[[2]int]bool ile ızgara koordinatları.

Büyük dizileri değer olarak geçirmenin maliyetli olduğunu da unutma: 1 MB'lık bir dizi her fonksiyon çağrısında baştan sona kopyalanır. Esnek uzunluk ve ucuz geçirme gerektiğinde dilimlere geçiyoruz.

Dilim nedir? Başlık yapısı

Dilim, bir dizinin ardışık bir bölümüne bakan penceredir. Kendisi eleman saklamaz; elemanlar her zaman bir alttaki dizide (backing array) durur. Dilim değişkeninin kendisi yalnızca üç alandan oluşan küçük bir başlıktır (slice header):

  • ptr: Alttaki dizide dilimin ilk elemanının adresi.
  • len (uzunluk): Dilimden erişilebilen eleman sayısı. len(s) ile okunur.
  • cap (kapasite): İlk elemandan alttaki dizinin sonuna kadar olan eleman sayısı. cap(s) ile okunur.

Go çalışma zamanının kaynak kodunda bu başlık gerçekten de üç alanlı bir struct olarak tanımlıdır:

// runtime/slice.go
type slice struct {
	array unsafe.Pointer // alttaki dizinin ilgili elemanını gösterir
	len   int
	cap   int
}

Bir diziden dilim aldığında ortaya çıkan tablo şöyledir:

arr := [6]int{10, 20, 30, 40, 50, 60}
s   := arr[1:4]

   s (dilim başlığı)
   ┌───────┬───────┬───────┐
   │ ptr ● │ len 3 │ cap 5 │
   └───┼───┴───────┴───────┘


arr ┌────┬────┬────┬────┬────┬────┐
    │ 10 │ 20 │ 30 │ 40 │ 50 │ 60 │
    └────┴────┴────┴────┴────┴────┘
      0    1    2    3    4    5
           └──── len ───┘
           └───────── cap ──────────┘

s yalnızca 20, 30 ve 40'ı görür (len 3), ama arkasında dizinin sonuna kadar iki eleman daha vardır (cap 5). Aşağıdaki program bu tabloyu doğrular:

main.go
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	arr := [6]int{10, 20, 30, 40, 50, 60}
	s := arr[1:4]

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

	s[0] = 99 // dilim üzerinden yazmak alttaki diziyi değiştirir
	fmt.Println("arr:", arr)

	fmt.Println("s[:cap(s)]:", s[:cap(s)]) // kapasiteye kadar genişletilebilir
	fmt.Println("başlık boyutu:", unsafe.Sizeof(s), "byte")
}
Çıktı
s: [20 30 40] len: 3 cap: 5
arr: [10 99 30 40 50 60]
s[:cap(s)]: [99 30 40 50 60]
başlık boyutu: 24 byte

Üç önemli gözlem var. Birincisi, s[0] = 99 ataması arr[1]'i değiştirdi, çünkü ikisi aynı bellek hücresidir. İkincisi, s[:cap(s)] ile dilimi kapasitesine kadar yeniden genişletebildik; len bir sınır değil, yalnızca pencerenin o anki genişliğidir. Ancak s[3] gibi len'in dışındaki bir indekse doğrudan erişmek, kapasite içinde olsa bile paniğe yol açar. Üçüncüsü, dilim başlığı 64 bit sistemlerde 24 byte'tır (üç tane 8 byte'lık alan). Dilimde bir milyon eleman da olsa başlık 24 byte kalır; bu yüzden dilimleri kopyalamak ve fonksiyonlara geçirmek ucuzdur. (unsafe paketini burada sadece boyutu göstermek için kullandık; normal kodda ihtiyacın olmayacak.)

Aşağıdaki görselleştirmede bir dilim oluşturup append ve dilimleme işlemleri uygula. Başlıktaki ptr, len ve cap değerlerinin nasıl değiştiğini, alttaki dizinin ne zaman yerinde kaldığını ve ne zaman yenisiyle değiştirildiğini izle.

make, append ve kapasite büyümesi

Dilim oluşturmanın yolları

Dilim oluşturmanın birkaç yolu vardır ve her birinin başlığı farklı başlar:

main.go
package main

import "fmt"

func show(name string, s []int) {
	fmt.Printf("%s: %v len=%d cap=%d nil=%t\n", name, s, len(s), cap(s), s == nil)
}

func main() {
	var a []int             // nil dilim: alttaki dizi yok
	b := []int{}            // boş ama nil olmayan dilim
	c := []int{7, 8, 9}     // literal: len ve cap eleman sayısı kadar
	d := make([]int, 3)     // len=3, cap=3, elemanlar sıfır değerli
	e := make([]int, 2, 10) // len=2, cap=10
	arr := [5]int{1, 2, 3, 4, 5}
	f := arr[1:3] // mevcut bir diziden

	show("a", a)
	show("b", b)
	show("c", c)
	show("d", d)
	show("e", e)
	show("f", f)
}
Çıktı
a: [] len=0 cap=0 nil=true
b: [] len=0 cap=0 nil=false
c: [7 8 9] len=3 cap=3 nil=false
d: [0 0 0] len=3 cap=3 nil=false
e: [0 0] len=2 cap=10 nil=false
f: [2 3] len=2 cap=4 nil=false

make([]T, len, cap) alttaki diziyi ayırır ve ilk len elemanı sıfır değerle doldurur. Kapasiteyi vermezsen len ile aynı olur. Dilim literali ([]int{7, 8, 9}) ise arka planda tam o boyutta bir dizi oluşturup ona bakan bir dilim döndürür. Dikkat et: Dilim literalinde köşeli parantezin içi boştur; [3]int{...} yazarsan dizi, []int{...} yazarsan dilim elde edersin.

append nasıl çalışır?

append(s, v1, v2, ...) elemanları dilimin sonuna ekler ve yeni bir dilim başlığı döndürür. Bu yüzden sonucu her zaman bir değişkene atarsın: s = append(s, x). Sonucu hiçbir yere atamazsan kod derlenmez. append perde arkasında iki durumdan birini yaşar:

Durum 1: len < cap (yer var)            s = append(s, 4)

  önce: len=3 cap=5                     sonra: len=4 cap=5
  ┌───┬───┬───┬───┬───┐                 ┌───┬───┬───┬───┬───┐
  │ 1 │ 2 │ 3 │   │   │      ───►       │ 1 │ 2 │ 3 │ 4 │   │
  └───┴───┴───┴───┴───┘                 └───┴───┴───┴───┴───┘
  Aynı dizi kullanılır, yalnızca len artar. Kopyalama yok.

Durum 2: len == cap (yer yok)           s = append(s, 4)

  önce: len=3 cap=3                     sonra: len=4 cap=6
  ┌───┬───┬───┐                         ┌───┬───┬───┬───┬───┬───┐
  │ 1 │ 2 │ 3 │    kopyala ───►         │ 1 │ 2 │ 3 │ 4 │   │   │
  └───┴───┴───┘                         └───┴───┴───┴───┴───┴───┘
  eski dizi (kimse göstermiyorsa        yeni, daha büyük dizi;
  çöp toplayıcı temizler)               s.ptr artık burayı gösterir

İkinci durumda çalışma zamanı daha büyük yeni bir dizi ayırır, mevcut elemanları oraya kopyalar, yeni elemanı ekler ve yeni diziyi gösteren bir başlık döndürür. Kopyalama O(n) sürer ama kapasite her seferinde katlanarak büyüdüğü için bu pahalı adım nadiren yaşanır. Sonuçta n kez append yapmanın toplam maliyeti O(n), yani ekleme başına amortize O(1) olur. Bu analizin ayrıntılarını ve sıfırdan bir dinamik dizi yazmayı Diziler ve Dinamik Diziler dersinde bulabilirsin.

Kapasite büyümesini gözlemlemek

Teoriyi bir kenara bırakıp gerçekte ne olduğuna bakalım. Aşağıdaki program boş bir dilime tek tek eleman ekliyor ve kapasitenin her değiştiği anı işaretliyor:

main.go
package main

import "fmt"

func main() {
	var s []int
	for i := range 10 {
		oldCap := cap(s)
		s = append(s, i)
		note := ""
		if cap(s) != oldCap {
			note = "  <- yeni dizi ayrıldı"
		}
		fmt.Printf("len=%-2d cap=%-2d %v%s\n", len(s), cap(s), s, note)
	}
}
Çıktı
len=1  cap=1  [0]  <- yeni dizi ayrıldı
len=2  cap=2  [0 1]  <- yeni dizi ayrıldı
len=3  cap=4  [0 1 2]  <- yeni dizi ayrıldı
len=4  cap=4  [0 1 2 3]
len=5  cap=8  [0 1 2 3 4]  <- yeni dizi ayrıldı
len=6  cap=8  [0 1 2 3 4 5]
len=7  cap=8  [0 1 2 3 4 5 6]
len=8  cap=8  [0 1 2 3 4 5 6 7]
len=9  cap=16 [0 1 2 3 4 5 6 7 8]  <- yeni dizi ayrıldı
len=10 cap=16 [0 1 2 3 4 5 6 7 8 9]

Kapasite 1, 2, 4, 8, 16 diye ikiye katlanıyor. Dolayısıyla on eleman eklerken yalnızca beş kez yeni dizi ayrıldı; diğer altı append mevcut dizideki boş yere yazdı. Küçük dilimler için kural gerçekten "kapasiteyi ikiye katla"dır. Peki dilim büyüdükçe ne olur? Her seferinde ikiye katlamak, milyonlarca elemanlık bir dilimde gereğinden çok boş bellek ayırmak demektir. Bu yüzden Go belli bir eşikten sonra büyüme oranını yumuşatır:

main.go
package main

import "fmt"

func main() {
	var s []int
	oldCap := cap(s)
	for i := range 6000 {
		s = append(s, i)
		if c := cap(s); c != oldCap {
			if oldCap >= 128 {
				fmt.Printf("cap %4d -> %4d  (x%.2f)\n", oldCap, c, float64(c)/float64(oldCap))
			}
			oldCap = c
		}
	}
}
Çıktı
cap  128 ->  256  (x2.00)
cap  256 ->  512  (x2.00)
cap  512 ->  848  (x1.66)
cap  848 -> 1280  (x1.51)
cap 1280 -> 1792  (x1.40)
cap 1792 -> 2560  (x1.43)
cap 2560 -> 3408  (x1.33)
cap 3408 -> 5120  (x1.50)
cap 5120 -> 7168  (x1.40)

Kapasite 256'ya ulaşana kadar oran 2'dir. Sonrasında çalışma zamanı (runtime/slice.go içindeki nextslicecap fonksiyonu) yeni kapasiteyi yaklaşık olarak newcap += (newcap + 3*256) / 4 formülüyle hesaplar. Bu formül büyük dilimlerde oranı yavaş yavaş 1.25'e yaklaştırır. Çıktıdaki oranların düzgün bir seri oluşturmamasının nedeni ise ikinci bir adımdır: Hesaplanan boyut, bellek ayırıcının kullandığı hazır blok boyutlarına (size class) yuvarlanır. Örneğin formül 512'den sonra 832 verir, ama 832 × 8 byte en yakın blok boyutuna yuvarlanınca kapasite 848 olur.

Kapasiteyi önceden ayırmak

Dilimin sonunda kaç eleman olacağını biliyorsan, kapasiteyi baştan ayırmak tüm ara kopyalamaları ortadan kaldırır:

// Kaç eleman geleceği belli: tek bir ayırma yeterli.
names := make([]string, 0, len(users))
for _, u := range users {
	names = append(names, u.Name)
}

Bu, özellikle döngü içinde çok sayıda eleman eklenen sıcak kod yollarında gözle görülür bir fark yaratır. Kesin sayıyı bilmiyorsan makul bir tahmin de işe yarar; tahmin aşılırsa append yine sorunsuz büyütür.

Dilimleme ve ortak alttaki dizi

Dilimleme ifadesi: s[low:high]

s[low:high] ifadesi, low indeksinden başlayıp high indeksine kadar (high dahil değil) giden yeni bir dilim başlığı üretir. Yeni dilimin uzunluğu high - low, kapasitesi ise cap(s) - low olur. low yazılmazsa 0, high yazılmazsa len(s) kabul edilir: s[:3], s[2:] ve s[:] geçerli ifadelerdir.

En önemli nokta şudur: Dilimleme hiçbir elemanı kopyalamaz. Yalnızca yeni bir başlık oluşturur, bu yüzden O(1) sürer. Ortaya çıkan dilim, orijinaliyle aynı alttaki diziyi paylaşır.

main.go
package main

import "fmt"

func main() {
	months := []string{"Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran"}

	q2 := months[3:6]
	firstTwo := months[:2]

	fmt.Println(q2, len(q2), cap(q2))
	fmt.Println(firstTwo, len(firstTwo), cap(firstTwo))

	q2[0] = "NİSAN" // months[3] ile aynı hücre
	fmt.Println(months)
}
Çıktı
[Nisan Mayıs Haziran] 3 3
[Ocak Şubat] 2 6
[Ocak Şubat Mart NİSAN Mayıs Haziran]

firstTwo yalnızca iki ay gösterse de kapasitesi 6'dır: Arkasında dizinin sonuna kadar dört hücre daha vardır. İşte bu görünmez kapasite, dilimlerle ilgili en sinsi hataların kaynağıdır.

Tuzak: append ile komşunun verisini ezmek

Bir alt dilime append yaptığında, kapasite yetiyorsa yeni eleman alttaki dizide sıradaki hücreye yazılır. O hücre başka bir dilimin, hatta orijinal dilimin görünen bir elemanı olabilir:

main.go
package main

import "fmt"

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

	head := nums[:2] // len=2, cap=5
	fmt.Println("head:", head, "len:", len(head), "cap:", cap(head))

	head = append(head, 100) // kapasite yeterli: yeni dizi ayrılmaz
	fmt.Println("head:", head)
	fmt.Println("nums:", nums) // nums[2] ezildi!
}
Çıktı
head: [1 2] len: 2 cap: 5
head: [1 2 100]
nums: [1 2 100 4 5]

Bellekte olan biten şudur:

head := nums[:2]

nums ┌───┬───┬───┬───┬───┐
     │ 1 │ 2 │ 3 │ 4 │ 5 │
     └───┴───┴───┴───┴───┘
head └─len─┘
     └─────── cap ───────┘

head = append(head, 100)     // head'in len'i dolu ama cap'te yer var

nums ┌───┬───┬─────┬───┬───┐
     │ 1 │ 2 │ 100 │ 4 │ 5 │   <- 3 kayboldu
     └───┴───┴─────┴───┴───┘
head └──── len ────┘

Bu hatayı daha da kafa karıştırıcı yapan şey, append'in yeni dizi ayırıp ayırmadığının kapasiteye bağlı olmasıdır. Aynı kod bir durumda veriyi paylaşır, başka bir durumda paylaşmaz:

main.go
package main

import "fmt"

func main() {
	base := make([]int, 3, 4) // bir boş yer var
	x := append(base, 1)
	y := append(base, 2) // x'in yazdığı hücreye yazar
	fmt.Println("x:", x, "y:", y)

	full := []int{0, 0, 0} // len=3, cap=3: boş yer yok
	p := append(full, 1)   // yeni dizi
	q := append(full, 2)   // başka bir yeni dizi
	fmt.Println("p:", p, "q:", q)
}
Çıktı
x: [0 0 0 2] y: [0 0 0 2]
p: [0 0 0 1] q: [0 0 0 2]

İlk durumda x ve y aynı alttaki diziyi paylaşır ve ikinci append, birincinin yazdığı 1'in üzerine 2 yazar. İkinci durumda kapasite dolu olduğu için her append kendi dizisini ayırır ve sonuçlar bağımsızdır. Kodunun davranışı gizli bir kapasite değerine bağlıysa, bu bir hata beklemektedir.

Çözüm: tam dilim ifadesi s[low:high:max]

Go bu sorun için üç indeksli bir dilimleme biçimi sunar: tam dilim ifadesi (full slice expression). s[low:high:max] ifadesinde uzunluk yine high - low'dur, ama kapasite max - low ile sınırlanır. max değerini high'a eşit verirsen kapasite uzunluğa eşit olur ve bir sonraki append yeni bir dizi ayırmak zorunda kalır:

main.go
package main

import "fmt"

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

	head := nums[0:2:2] // len=2, cap=2
	fmt.Println("head:", head, "len:", len(head), "cap:", cap(head))

	head = append(head, 100) // kapasite dolu: yeni dizi ayrılır
	head[0] = -1             // artık nums'ı etkilemez
	fmt.Println("head:", head)
	fmt.Println("nums:", nums)
}
Çıktı
head: [1 2] len: 2 cap: 2
head: [-1 2 100]
nums: [1 2 3 4 5]
head := nums[0:2:2]

nums ┌───┬───┬───┬───┬───┐
     │ 1 │ 2 │ 3 │ 4 │ 5 │   <- dokunulmadı
     └───┴───┴───┴───┴───┘
head └─len─┘
     └─cap─┘

head = append(head, 100)     // cap doluydu, yeni dizi ayrıldı

head ┌────┬───┬─────┬───┐
     │ -1 │ 2 │ 100 │   │    ayrı bellek
     └────┴───┴─────┴───┘

İndeksler için kural 0 <= low <= high <= max <= cap(s) şeklindedir; bu sınırların dışına çıkan bir ifade panik oluşturur. Bir fonksiyondan iç verinin bir parçasını döndürürken veya bir dilimi parçalara bölerken tam dilim ifadesi kullanmak, çağıranın yapacağı bir append'in senin verini ezmesini önler.

copy ile gerçek kopya

Paylaşım istemediğinde elemanları gerçekten kopyalaman gerekir. Yerleşik copy(dst, src) fonksiyonu src'deki elemanları dst'ye kopyalar ve kopyalanan eleman sayısını döndürür. Kopyalanan sayı her zaman min(len(dst), len(src)) olur; copy hedefi asla büyütmez.

main.go
package main

import "fmt"

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

	// 1) Bağımsız kopya: hedefin uzunluğu kaynak kadar olmalı
	dst := make([]int, len(src))
	n := copy(dst, src)
	dst[0] = 100
	fmt.Println("kopyalanan:", n, "src:", src, "dst:", dst)

	// 2) copy, iki uzunluktan küçük olanı kadar kopyalar
	small := make([]int, 2)
	fmt.Println("kopyalanan:", copy(small, src), "small:", small)

	// 3) Klasik hata: len=0 olan hedefe hiçbir şey kopyalanmaz
	empty := make([]int, 0, 5)
	fmt.Println("kopyalanan:", copy(empty, src), "empty:", empty)

	// 4) Örtüşen bölgeler güvenlidir: indeks 1'deki elemanı sil
	i := 1
	copy(src[i:], src[i+1:])
	src = src[:len(src)-1]
	fmt.Println("silme sonrası:", src)

	// 5) string'den []byte'a kopyalama
	buf := make([]byte, 3)
	copy(buf, "Gopher")
	fmt.Println(string(buf))
}
Çıktı
kopyalanan: 5 src: [1 2 3 4 5] dst: [100 2 3 4 5]
kopyalanan: 2 small: [1 2]
kopyalanan: 0 empty: []
silme sonrası: [1 3 4 5]
Gop

Programın her bölümü ayrı bir dersi gösteriyor. İlk bölümde dst kendi dizisine sahip olduğu için dst[0] = 100 ataması src'yi etkilemedi. Üçüncü bölüm, en sık yapılan copy hatasıdır: make([]int, 0, 5) ile oluşturulan dilimin kapasitesi 5 olsa da uzunluğu 0'dır ve copy yalnızca uzunluğa bakar. Dördüncü bölümde kaynak ve hedef aynı dizinin örtüşen parçalarıdır; copy bu durumu doğru yönetir. Böylece elemanları bir sola kaydırıp son elemanı kırparak ortadan silme yapmış olduk. Beşinci bölüm ise copy'nin string'den byte dilimine kopyalayabildiğini gösteriyor.

Bir dilimin tam kopyasını almanın kısa yolları da vardır: slices.Clone(s) veya append([]int(nil), s...). İkisi de yeni bir dizi ayırıp elemanları kopyalar. slices.Clone nil bir dilimi nil olarak korur.

nil dilim ve boş dilim

Sıfır değerli bir dilim, yani var s []int ile tanımlanan dilim nil dilimdir: işaretçisi nil, uzunluğu ve kapasitesi 0'dır. []int{} veya make([]int, 0) ile oluşturulan dilim ise boş ama nil olmayan bir dilimdir. İkisi çoğu durumda aynı davranır ama tamamen aynı değildir:

main.go
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var nilSlice []string
	emptySlice := []string{}

	fmt.Println(nilSlice == nil, len(nilSlice), cap(nilSlice))
	fmt.Println(emptySlice == nil, len(emptySlice), cap(emptySlice))
	fmt.Println(nilSlice, emptySlice) // fmt ikisini de [] yazar

	for range nilSlice {
		fmt.Println("bu satır hiç çalışmaz")
	}

	grown := append(nilSlice, "artık dolu") // nil dilime append güvenlidir
	fmt.Println(grown, len(grown))

	a, _ := json.Marshal(nilSlice)
	b, _ := json.Marshal(emptySlice)
	fmt.Println(string(a), string(b))
}
Çıktı
true 0 0
false 0 0
[] []
[artık dolu] 1
null []
Özelliknil dilim (var s []int)boş dilim ([]int{})
s == niltruefalse
len(s), cap(s)0, 00, 0
range ile gezinmeHiç dönmezHiç dönmez
appendÇalışırÇalışır
s[0] ile erişimPanikPanik
fmt.Println[][]
JSON kodlamasınull[]

Pratikteki kural basit: Bir dilimin boş olup olmadığını s == nil ile değil, len(s) == 0 ile kontrol et; bu kontrol iki durumu da doğru yakalar. Yeni bir dilime ihtiyaç duyduğunda var s []int ile başlamak idiomatik Go'dur. Fark yalnızca dışarıya veri verirken önemlidir: Bir JSON API'si boş listeyi null yerine [] olarak göndermek zorundaysa boş dilim kullanmalısın. JSON ayrıntılarını Dosyalar, I/O ve JSON dersinde göreceksin.

Bir de şunu bil: Dilimler yalnızca nil ile karşılaştırılabilir. a == b gibi iki dilimi karşılaştıran bir ifade derleme hatası verir; eleman eleman karşılaştırma için slices.Equal kullanılır.

Dilimleri fonksiyonlara geçirmek

Fonksiyonlar dersinden hatırlarsan Go'da her şey değer ile geçirilir. Dilimler de bu kurala uyar: Fonksiyona başlık kopyalanır, alttaki dizi kopyalanmaz. Sonuç olarak iki başlık aynı diziyi gösterir:

main içindeki nums                 doubleAll içindeki s
┌───────┬───────┬────────┐         ┌───────┬───────┬────────┐
│ ptr ● │ len 3 │ cap 10 │         │ ptr ● │ len 3 │ cap 10 │
└───┼───┴───────┴────────┘         └───┼───┴───────┴────────┘
    │        başlık kopyalandı         │
    └───────────────┬──────────────────┘

          ┌───┬───┬───┬───┬─────────┐
          │ 1 │ 2 │ 3 │   │   ...   │   ortak alttaki dizi
          └───┴───┴───┴───┴─────────┘

Bu tablodan iki sonuç çıkar: Fonksiyon elemanları değiştirirse çağıran bunu görür. Fonksiyon başlığı değiştirirse (örneğin append ile uzunluğu artırırsa) çağıranın başlığı etkilenmez.

main.go
package main

import "fmt"

func doubleAll(s []int) {
	for i := range s {
		s[i] *= 2 // ortak dizideki elemanları değiştirir
	}
}

func appendLost(s []int) {
	s = append(s, 4) // yalnızca yerel başlığın len'i artar
	fmt.Println("fonksiyon içinde:", s)
}

func appendReturned(s []int) []int {
	return append(s, 4)
}

func main() {
	nums := make([]int, 3, 10)
	nums[0], nums[1], nums[2] = 1, 2, 3

	doubleAll(nums)
	fmt.Println("doubleAll sonrası:", nums)

	appendLost(nums)
	fmt.Println("appendLost sonrası:", nums, "len:", len(nums))
	fmt.Println("dizide gizli duran:", nums[:4])

	nums = appendReturned(nums)
	fmt.Println("appendReturned sonrası:", nums, "len:", len(nums))
}
Çıktı
doubleAll sonrası: [2 4 6]
fonksiyon içinde: [2 4 6 4]
appendLost sonrası: [2 4 6] len: 3
dizide gizli duran: [2 4 6 4]
appendReturned sonrası: [2 4 6 4] len: 4

doubleAll elemanları ikiye katladı ve main bunu gördü. appendLost ise 4'ü eklediğini sanıyor ama main'deki nums'ın uzunluğu hâlâ 3. Asıl ilginç satır nums[:4]: Kapasite yettiği için append, 4'ü gerçekten ortak diziye yazmış. main bu elemanı yalnızca kendi len'i 3 olduğu için görmüyor. Kapasite dolu olsaydı append yeni bir dizi ayıracak ve fonksiyon içindeki sonraki değişiklikler çağırana hiç ulaşmayacaktı.

Buradan çıkan idiomatik kural: Bir dilimin uzunluğunu değiştiren fonksiyonlar, tıpkı append gibi yeni dilimi döndürmelidir (nums = appendReturned(nums)). Yalnızca elemanları okuyan veya yerinde değiştiren fonksiyonlar ise dilimi doğrudan alabilir. Bir dilime işaretçi (*[]int) geçirmek de mümkündür ama nadiren gerekir. İşaretçilerin ayrıntılarını İşaretçiler dersinde göreceksin.

Çok boyutlu dilimler

Go'da ayrı bir "matris" tipi yoktur; iki boyutlu veri, dilimlerden oluşan bir dilim ([][]int) ile temsil edilir. Dış dilimin her elemanı kendi başlığı ve kendi alttaki dizisi olan bağımsız bir dilimdir. Bu yüzden her satırı ayrıca oluşturman gerekir ve satırların uzunlukları birbirinden farklı olabilir:

main.go
package main

import "fmt"

func main() {
	// 3x4 ızgara: önce satırları tutan dilim, sonra her satır
	rows, cols := 3, 4
	grid := make([][]int, rows)
	for i := range grid {
		grid[i] = make([]int, cols)
	}
	grid[1][2] = 7
	for _, row := range grid {
		fmt.Println(row)
	}

	// Satır uzunlukları farklı olabilir: Pascal üçgeni
	pascal := make([][]int, 5)
	for i := range pascal {
		pascal[i] = make([]int, i+1)
		pascal[i][0], pascal[i][i] = 1, 1
		for j := 1; j < i; j++ {
			pascal[i][j] = pascal[i-1][j-1] + pascal[i-1][j]
		}
	}
	for _, row := range pascal {
		fmt.Println(row)
	}
}
Çıktı
[0 0 0 0]
[0 0 7 0]
[0 0 0 0]
[1]
[1 1]
[1 2 1]
[1 3 3 1]
[1 4 6 4 1]

make([][]int, rows) yalnızca rows tane nil satır üretir. Satırları oluşturmadan grid[1][2] = 7 yazmaya çalışsaydın index out of range paniği alırdın. Bellekteki düzen şöyledir:

grid (dış dilim)          her satırın kendi dizisi
┌──────────────────┐
│ [0] ptr,len,cap ─┼──►  ┌───┬───┬───┬───┐
├──────────────────┤     │ 0 │ 0 │ 0 │ 0 │
│ [1] ptr,len,cap ─┼──►  ┌───┬───┬───┬───┐
├──────────────────┤     │ 0 │ 0 │ 7 │ 0 │
│ [2] ptr,len,cap ─┼──►  ┌───┬───┬───┬───┐
└──────────────────┘     │ 0 │ 0 │ 0 │ 0 │

Tek ayırmalı matris

Satır sayısı kadar ayrı ayırma yapmak çoğu zaman sorun değildir. Ama büyük ve dikdörtgen bir matriste tüm hücreleri tek bir bitişik dizide tutmak hem ayırma sayısını bire indirir hem de önbellek dostu erişim sağlar. Satırları bu tek dizinin parçaları olarak dilimleriz; her satırın kapasitesini tam dilim ifadesiyle sınırlamak, bir satıra yapılan append'in komşu satırı ezmesini önler:

main.go
package main

import "fmt"

func newMatrix(rows, cols int) [][]float64 {
	data := make([]float64, rows*cols) // tüm hücreler için tek ayırma
	m := make([][]float64, rows)
	for i := range m {
		start := i * cols
		m[i] = data[start : start+cols : start+cols]
	}
	return m
}

func main() {
	m := newMatrix(2, 3)
	m[0][2] = 1.5
	m[1][0] = 2.5
	fmt.Println(m)
	fmt.Println("satır len:", len(m[0]), "cap:", cap(m[0]))
}
Çıktı
[[0 0 1.5] [2.5 0 0]]
satır len: 3 cap: 3

Boyutu derleme zamanında belli olan küçük ızgaralar için dizilerin dizisi ([3][3]int) de iyi bir seçenektir: Tek parça bellekte durur, sıfır değeri hazırdır ve == ile karşılaştırılabilir. Ama değer semantiği gereği atamada tamamı kopyalanır.

slices paketi

Go 1.21 ile standart kütüphaneye gelen slices paketi, dilimlerle yapılan yaygın işleri jenerik fonksiyonlar olarak sunar. Eskiden elle döngü yazdığın arama, sıralama ve karşılaştırma işlemleri artık tek satırdır. Önce arama ve sıralamaya bakalım:

main.go
package main

import (
	"fmt"
	"slices"
)

func main() {
	cities := []string{"İzmir", "Ankara", "Bursa", "Adana", "Trabzon"}

	fmt.Println(slices.Contains(cities, "Bursa"))
	fmt.Println(slices.Index(cities, "Adana"), slices.Index(cities, "Van"))

	slices.Sort(cities) // yerinde sıralar
	fmt.Println(cities)

	i, found := slices.BinarySearch(cities, "Bursa")
	fmt.Println(i, found)
	i, found = slices.BinarySearch(cities, "Denizli")
	fmt.Println(i, found) // yoksa, eklenseydi gireceği konum

	slices.Reverse(cities) // yerinde ters çevirir
	fmt.Println(cities)
}
Çıktı
true
3 -1
[Adana Ankara Bursa Trabzon İzmir]
2 true
3 false
[İzmir Trabzon Bursa Ankara Adana]

Contains ve Index dilimi baştan sona tarar, yani O(n) sürer; Index bulamazsa -1 döndürür. Sort dilimi yerinde, O(n log n) sürede sıralar (Go bunun için pdqsort algoritmasını kullanır; ayrıntılar Verimli Sıralama Algoritmaları dersinde). BinarySearch yalnızca sıralı bir dilimde doğru çalışır ve O(log n) sürer; iki değer döndürür: elemanın konumu (yoksa eklenmesi gereken konum) ve bulunup bulunmadığı. İkili aramanın nasıl çalıştığını Arama Algoritmaları dersinde adım adım göreceksin.

Çıktıdaki sıralamaya dikkat et: "İzmir" en sona düştü. slices.Sort string'leri byte değerlerine göre karşılaştırır ve İ harfi UTF-8'de ASCII harflerinden daha büyük byte'larla kodlanır. Türkçe alfabetik sıralama için dile duyarlı bir karşılaştırma gerekir; bu konuya String, Byte ve Rune dersinde değineceğiz.

Şimdi dilimi değiştiren ve karşılaştıran fonksiyonlara geçelim:

main.go
package main

import (
	"fmt"
	"slices"
)

func main() {
	nums := []int{10, 20, 30, 40, 50}
	backup := slices.Clone(nums) // bağımsız kopya

	nums = slices.Insert(nums, 2, 25, 27) // indeks 2'ye iki eleman ekle
	fmt.Println("Insert:", nums)

	nums = slices.Delete(nums, 0, 2) // [0, 2) aralığını sil
	fmt.Println("Delete:", nums)

	fmt.Println("backup:", backup)
	fmt.Println("eşit mi?", slices.Equal(nums, backup))
	fmt.Println("eşit mi?", slices.Equal(backup, []int{10, 20, 30, 40, 50}))

	data := []int{1, 2, 3, 4, 5}
	short := slices.Delete(data, 1, 3)
	fmt.Println(short, data) // Delete, geride kalan hücreleri sıfırlar
}
Çıktı
Insert: [10 20 25 27 30 40 50]
Delete: [25 27 30 40 50]
backup: [10 20 30 40 50]
eşit mi? false
eşit mi? true
[1 4 5] [1 4 5 0 0]

Insert ve Delete tıpkı append gibi yeni bir dilim döndürür, bu yüzden sonucu atamayı unutma. İkisi de araya ekleme veya silme için sonraki elemanları kaydırdığından O(n) sürer. Son iki satır önemli bir ayrıntıyı gösteriyor: Delete aynı alttaki dizi üzerinde çalışır ve kısalan dilimin arkasında kalan hücreleri sıfır değerle doldurur (Go 1.22'den beri). Böylece silinen elemanlar işaretçi içeriyorsa çöp toplayıcı onları serbest bırakabilir. Ama eski data değişkenini kullanmaya devam edersen bu sıfırları görürsün; silme sonrası yalnızca döndürülen dilimi kullan.

FonksiyonNe yapar?Maliyet
slices.Contains(s, v)v dilimde var mı?O(n)
slices.Index(s, v)İlk konum, yoksa -1O(n)
slices.Sort(s)Yerinde artan sıralamaO(n log n)
slices.BinarySearch(s, v)Sıralı dilimde arama: (konum, bulundu)O(log n)
slices.Reverse(s)Yerinde ters çevirmeO(n)
slices.Insert(s, i, v...)i konumuna ekleme, yeni dilimi döndürürO(n)
slices.Delete(s, i, j)s[i:j] aralığını silme, yeni dilimi döndürürO(n)
slices.Equal(a, b)Aynı uzunluk ve aynı elemanlar mı?O(n)
slices.Clone(s)Sığ kopyaO(n)

Pakette bunlardan fazlası var: Özel sıralama için slices.SortFunc(s, func(a, b T) int { ... }) ve cmp.Compare, en büyük ve en küçük eleman için slices.Max ve slices.Min, ardışık tekrarları silmek için slices.Compact. Jenerik fonksiyonların nasıl yazıldığını Jenerikler dersinde, slices.Sorted ve slices.Collect gibi iteratör alan fonksiyonları İteratörler dersinde göreceksin.

İşlemlerin maliyeti

İşlemZaman karmaşıklığıNot
s[i] okuma/yazmaO(1)Bitişik bellek sayesinde doğrudan adres hesabı
len(s), cap(s)O(1)Başlıkta saklanır
s[low:high] dilimlemeO(1)Kopyalama yok, yalnızca yeni başlık
append(s, v)Amortize O(1)Kapasite doluysa o çağrı O(n)
Ortaya ekleme / ortadan silmeO(n)Sonraki elemanlar kaydırılır
copy(dst, src)O(n)n = kopyalanan eleman sayısı
Dilimi fonksiyona geçirmeO(1)Yalnızca 24 byte'lık başlık kopyalanır

Sık yapılan hatalar

  • append sonucunu atamamak. append(s, x) tek başına derlenmez; s = append(s, x) yazmalısın. Başka bir değişkene atarsan (t := append(s, x)) s ve t'nin aynı diziyi paylaşıp paylaşmadığı kapasiteye bağlı olur.
  • Alt dilime append yapıp orijinal veriyi ezmek. Bir alt dilimi başka koda verirken ya da ona ekleme yapacaksan tam dilim ifadesi (s[low:high:high]) veya slices.Clone kullan.
  • make([]T, n) sonrası append yapmak. Başta n tane sıfır değer oluşur. Boş başlayıp dolduracaksan make([]T, 0, n) yaz.
  • copy hedefinin uzunluğunu 0 bırakmak. copy kapasiteye değil uzunluğa bakar; hedefi make([]T, len(src)) ile oluştur.
  • range değişkenini değiştirip dilimin değiştiğini sanmak. for _, v := range s { v *= 2 } yalnızca v kopyasını değiştirir; elemanı değiştirmek için s[i] *= 2 yaz.
  • Çok boyutlu dilimde tüm satırlara aynı dilimi atamak. Döngü dışında tek bir row := make([]int, cols) oluşturup her grid[i] = row yaparsan bütün satırlar aynı diziyi gösterir; bir hücreyi değiştirmek tüm sütunu değiştirir. Her satır için döngü içinde ayrı make çağır.
  • Boşluğu s == nil ile kontrol etmek. Boş ama nil olmayan dilimler bu kontrolden kaçar; len(s) == 0 kullan.
  • Büyük bir dizinin küçük parçasını uzun süre saklamak. Parça yaşadıkça dizinin tamamı bellekte kalır; parçayı slices.Clone ile kopyala.

Alıştırmalar

Alıştırma·Dilimi yerinde ters çevir
Kolay

reverse(s []int) fonksiyonunu slices.Reverse kullanmadan yaz. Fonksiyon yeni bir dilim oluşturmamalı, elemanları yerinde yer değiştirmelidir. Boş ve tek elemanlı dilimlerde de doğru çalıştığından emin ol.

İpucu

İki indeksle başla: biri en başta (i), biri en sonda (j). s[i] ile s[j]'yi takas et, sonra i'yi artır, j'yi azalt. i < j olduğu sürece devam et. Go'da takas tek satırdır: s[i], s[j] = s[j], s[i].

Çözümü göster
main.go
package main

import "fmt"

func reverse(s []int) {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
}

func main() {
	nums := []int{1, 2, 3, 4, 5}
	reverse(nums)
	fmt.Println(nums)

	even := []int{10, 20, 30, 40}
	reverse(even)
	fmt.Println(even)

	var empty []int
	reverse(empty)
	fmt.Println(empty, len(empty))
}
Çıktı
[5 4 3 2 1]
[40 30 20 10]
[] 0

Fonksiyon dilimi döndürmüyor, çünkü uzunluğu değiştirmiyor; yalnızca ortak dizideki elemanların yerini değiştiriyor ve çağıran bunu doğrudan görüyor. Boş dilimde j başta -1 olduğu için döngü hiç çalışmaz. Döngü n/2 kez döner: zaman karmaşıklığı O(n), ek alan O(1). Jenerikler dersinden sonra aynı fonksiyonu her tip için func reverse[T any](s []T) biçiminde yazabileceksin.

Alıştırma·Sıralı dilimde tekrarları yerinde sil
Orta

Artan sırada sıralanmış bir []int alan ve tekrar eden elemanları silen dedupSorted(s []int) []int fonksiyonunu yaz. Yeni bir dilim ayırma; aynı alttaki diziyi kullan ve sonuç dilimini döndür. Örneğin [1 1 2 3 3 3 4 5 5] için sonuç [1 2 3 4 5] olmalı.

İpucu

İki indeks kullan: r her elemanı okur, w bir sonraki benzersiz elemanın yazılacağı konumu tutar. Dilim sıralı olduğu için bir eleman, en son yazılan benzersiz elemandan (s[w-1]) farklıysa yenidir. Sonunda s[:w] döndür.

Çözümü göster
main.go
package main

import (
	"fmt"
	"slices"
)

func dedupSorted(s []int) []int {
	if len(s) == 0 {
		return s
	}
	w := 1 // s[:w] her zaman benzersiz elemanları tutar
	for r := 1; r < len(s); r++ {
		if s[r] != s[w-1] {
			s[w] = s[r]
			w++
		}
	}
	return s[:w]
}

func main() {
	fmt.Println(dedupSorted([]int{1, 1, 2, 3, 3, 3, 4, 5, 5}))
	fmt.Println(dedupSorted([]int{7, 7, 7}))
	fmt.Println(dedupSorted([]int{}))

	// Standart kütüphanedeki karşılığı:
	fmt.Println(slices.Compact([]int{1, 1, 2, 3, 3, 3, 4, 5, 5}))
}
Çıktı
[1 2 3 4 5]
[7]
[]
[1 2 3 4 5]

s[:w] bölgesi her adımda "şimdiye kadar görülen benzersiz elemanlar" olarak kalır. Okuma indeksi yazma indeksinin her zaman önünde veya onunla aynı yerde olduğu için, henüz okunmamış bir elemanın üzerine yazma riski yoktur. Her eleman bir kez okunur: zaman O(n), ek alan O(1). Fonksiyon uzunluğu değiştirdiği için yeni dilimi döndürür. slices.Compact aynı işi yapar ve ek olarak geride kalan hücreleri sıfırlar.

Alıştırma·Dilimi güvenli parçalara böl
Orta

chunk(s []int, size int) [][]int fonksiyonunu yaz: Dilimi en fazla size elemanlık ardışık parçalara bölsün, son parça daha kısa olabilir. Parçalar elemanları kopyalamamalı, orijinal diziyi paylaşmalı. Ama bir parçaya append yapıldığında bir sonraki parçanın verisi ezilmemeli. [1 2 3 4 5 6 7] ve size=3 için [[1 2 3] [4 5 6] [7]] bekleniyor.

İpucu

Her parçayı s[start:end] ile alırsan kapasitesi dizinin sonuna kadar uzanır. Kapasiteyi parçanın sonunda kesmek için tam dilim ifadesini kullan. end değerini hesaplarken min yerleşik fonksiyonu işine yarar.

Çözümü göster
main.go
package main

import "fmt"

func chunk(s []int, size int) [][]int {
	var chunks [][]int
	for start := 0; start < len(s); start += size {
		end := min(start+size, len(s))
		chunks = append(chunks, s[start:end:end])
	}
	return chunks
}

func main() {
	nums := []int{1, 2, 3, 4, 5, 6, 7}
	parts := chunk(nums, 3)
	fmt.Println(parts)

	for _, p := range parts {
		fmt.Printf("len=%d cap=%d ", len(p), cap(p))
	}
	fmt.Println()

	parts[0] = append(parts[0], 100) // kapasite dolu: yeni dizi
	fmt.Println("parts:", parts)
	fmt.Println("nums: ", nums)
}
Çıktı
[[1 2 3] [4 5 6] [7]]
len=3 cap=3 len=3 cap=3 len=1 cap=1
parts: [[1 2 3 100] [4 5 6] [7]]
nums:  [1 2 3 4 5 6 7]

s[start:end:end] sayesinde her parçanın kapasitesi uzunluğuna eşittir. parts[0]'a yapılan append yeni bir dizi ayırdı ve nums ile ikinci parça olduğu gibi kaldı. s[start:end] yazsaydın ilk parçanın kapasitesi 7 olurdu ve append, nums[3]'ü (yani ikinci parçanın ilk elemanını) 100 ile ezerdi. Fonksiyon her elemanı bir kez dilimler ama kopyalamaz: zaman O(n/size), parça başlıkları için alan O(n/size).

Alıştırma·Matrisi saat yönünde döndür
Zor

rotateClockwise(m [][]int) [][]int fonksiyonunu yaz. rows × cols boyutlu dikdörtgen bir matrisi saat yönünde 90 derece döndürüp cols × rows boyutlu yeni bir matris döndürsün; girdi değişmesin. Örneğin

1 2 3        4 1
4 5 6   →    5 2
             6 3
İpucu

Önce çıktının boyutlarını belirle ve her satırı make ile oluştur. Sonra tek bir hücrenin nereye gittiğini düşün: Girdideki ilk satır, çıktıda en sağ sütun olur; girdideki son satır en sol sütun olur. Yani m[r][c] değeri out[c][rows-1-r] konumuna gider.

Çözümü göster
main.go
package main

import "fmt"

func rotateClockwise(m [][]int) [][]int {
	rows := len(m)
	if rows == 0 {
		return nil
	}
	cols := len(m[0])

	out := make([][]int, cols)
	for i := range out {
		out[i] = make([]int, rows)
	}
	for r := range rows {
		for c := range cols {
			out[c][rows-1-r] = m[r][c]
		}
	}
	return out
}

func printMatrix(m [][]int) {
	for _, row := range m {
		fmt.Println(row)
	}
	fmt.Println("---")
}

func main() {
	m := [][]int{
		{1, 2, 3},
		{4, 5, 6},
	}
	printMatrix(rotateClockwise(m))
	printMatrix(m) // girdi değişmedi

	square := [][]int{
		{1, 2, 3},
		{4, 5, 6},
		{7, 8, 9},
	}
	printMatrix(rotateClockwise(square))
}
Çıktı
[4 1]
[5 2]
[6 3]
---
[1 2 3]
[4 5 6]
---
[7 4 1]
[8 5 2]
[9 6 3]
---

Her m[r][c] hücresi tam bir kez okunur ve out[c][rows-1-r] konumuna yazılır. Formülü doğrulamak için köşelere bak: Sol üst m[0][0], çıktıda ilk satırın sonuna (out[0][rows-1]) gider; sol alt m[rows-1][0] ise çıktının sol üst köşesine (out[0][0]) gelir. Çıktı için ayrı satırlar ayırdığımızdan girdi korunur. Zaman karmaşıklığı O(rows × cols), yeni matris için alan da O(rows × cols). Kare matrislerde aynı işi ek bellek kullanmadan yapmak da mümkündür: önce transpozunu alıp sonra her satırı ters çevirmek yeterlidir.

Kısa sınav

Kısa sınav

a := [3]int{1, 2, 3}; b := a; b[0] = 9; fmt.Println(a[0]) ne yazdırır?

s := make([]int, 2, 10); t := s[1:3] sonrasında len(t) ve cap(t) nedir?

a := []int{1, 2, 3, 4}; b := a[:2]; b = append(b, 9); fmt.Println(a) ne yazdırır?

dst := make([]int, 0, 5); n := copy(dst, []int{1, 2, 3}) sonrasında n kaçtır?

var s []int ile tanımlanan nil dilim için hangisi YANLIŞTIR?

func add(s []int) { s = append(s, 4) } fonksiyonu nums := make([]int, 3, 10) ile çağrıldıktan sonra len(nums) kaçtır?

Özet

  • Dizinin uzunluğu tipinin parçasıdır ([3]int[4]int) ve diziler değer tipidir: atama ve fonksiyona geçirme tüm elemanları kopyalar.
  • Dilim, alttaki bir diziye bakan 24 byte'lık bir başlıktır: işaretçi, uzunluk ve kapasite. Dilimleme kopyalama yapmaz, O(1) sürer.
  • append kapasite yettiğinde aynı diziye yazar, yetmediğinde daha büyük bir dizi ayırıp kopyalar. Kapasite küçük dilimlerde ikiye katlanır, büyüklerde oran yavaşça 1.25'e iner; bu sayede ekleme amortize O(1)'dir.
  • Aynı diziyi paylaşan dilimlerde append komşu veriyi ezebilir. Tam dilim ifadesi s[low:high:max] kapasiteyi sınırlayarak bunu önler.
  • copy yalnızca min(len(dst), len(src)) kadar kopyalar; bağımsız kopya için make([]T, len(src)) veya slices.Clone kullan.
  • nil dilim ve boş dilim çoğu işlemde aynı davranır; boşluk kontrolünü len(s) == 0 ile yap, fark JSON gibi dış temsillerde ortaya çıkar.
  • Fonksiyona dilim geçirildiğinde başlık kopyalanır, elemanlar paylaşılır. Uzunluğu değiştiren fonksiyonlar yeni dilimi döndürmelidir.
  • [][]T her satırı ayrı oluşturulan bağımsız dilimlerden oluşur; slices paketi Sort, BinarySearch, Contains, Index, Reverse, Insert, Delete, Equal ve Clone ile günlük işleri kısaltır.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.