Go dilinde işaretçi (pointer), bir değişkenin bellek adresini tutan değerdir. *T tipi, T tipinde bir değişkeni gösteren işaretçidir; &x ifadesi x değişkeninin adresini üretir, *p ifadesi ise işaretçinin gösterdiği değişkene erişir. İşaretçinin sıfır değeri nildir ve nil bir işaretçiyi dolaylamak çalışma anında panik üretir. C'den farklı olarak Go'da işaretçi aritmetiği yoktur ve yerel bir değişkenin adresini döndürmek güvenlidir.

Bu yazıda işaretçi tanımlamayı ve kullanmayı, fonksiyonlara işaretçi geçirmeyi, new fonksiyonunu ve Go 1.26 ile gelen new(ifade) yazımını, nil işaretçileri, struct işaretçilerini, işaretçi alıcılı metotlara kısa bir girişi, kaçış analizini (escape analysis) ve sık yapılan hataları Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Serinin tamamı için Go öğrenme rehberi sayfasına bakabilirsiniz.

& ve * operatörleri

Spesifikasyona göre T tipindeki bir x operandı için &x adres işlemi *T tipinde bir işaretçi üretir. Operand adreslenebilir olmalıdır: bir değişken, bir işaretçi dolaylaması, bir dilim indeksleme işlemi, adreslenebilir bir struct'ın alan seçicisi ya da adreslenebilir bir dizinin indekslenmesi. Bu kurala bir istisna vardır: bileşik literallerin (&Nokta{1, 2}) adresi de alınabilir. *T tipindeki bir x için *x, x işaretçisinin gösterdiği T tipindeki değişkeni ifade eder.

go
package main

import "fmt"

func main() {
	sayi := 42
	p := &sayi // p'nin tipi *int

	fmt.Println(*p) // p üzerinden okuma
	*p = 21         // p üzerinden yazma
	fmt.Println(sayi)

	var bos *int
	fmt.Println(bos == nil, bos)

	q := p
	fmt.Println(p == q, *q)
}

// Çıktı:
// 42
// 21
// true <nil>
// true 21

Tour of Go bu işleme "dolaylama" (dereferencing veya indirecting) der. *p = 21 ataması sayi değişkeninin kendisini değiştirir, çünkü p onun adresini tutar. Spesifikasyona göre iki işaretçi aynı değişkeni gösteriyorsa ya da ikisi de nil ise eşittir; örnekteki p == q bu yüzden true döndürür. Bir işaretçiyi kopyalamak (q := p) gösterilen veriyi kopyalamaz, yalnızca adresi kopyalar.

Fonksiyonlara işaretçi geçirmek

Go SSS, C ailesindeki tüm dillerde olduğu gibi Go'da da her şeyin değerle geçirildiğini vurgular: fonksiyon kendisine verilen değerin kopyasını alır. Bir int geçirmek int değerini, bir işaretçi geçirmek işaretçiyi kopyalar, ama işaretçinin gösterdiği veriyi kopyalamaz. Bir fonksiyonun çağıranın değişkenini değiştirebilmesi için ona değişkenin adresini vermeniz gerekir:

go
package main

import "fmt"

type Sayac struct{ N int }

func degerle(s Sayac) { s.N++ }

func isaretciyle(s *Sayac) { s.N++ }

func sifirla(p *int) {
	*p = 0
}

func main() {
	s := Sayac{}
	degerle(s)
	fmt.Println(s.N)

	isaretciyle(&s)
	isaretciyle(&s)
	fmt.Println(s.N)

	x := 99
	sifirla(&x)
	fmt.Println(x)
}

// Çıktı:
// 0
// 2
// 0

degerle fonksiyonu struct'ın kopyası üzerinde çalıştığı için s.N sıfır kalır. isaretciyle fonksiyonu ise adres üzerinden çağıranın struct'ını değiştirir. Dilimler ve map'ler işaretçi gibi davranan tanımlayıcılar olduğundan, elemanlarını değiştirmek için onlara işaretçi geçirmeniz gerekmez; ancak bir dilimin uzunluğunu fonksiyon içinde değiştirmek isterseniz yine işaretçi ya da dönüş değeri gerekir. Bu ayrım Go array ve slice farkı yazısında ayrıntılı olarak anlatılıyor.

Ne zaman işaretçi geçirmeli? Go SSS metot alıcıları için şu ölçütleri sıralar ve aynı düşünce fonksiyon parametreleri için de geçerlidir: değerin değiştirilmesi gerekiyorsa işaretçi şarttır; değer büyükse (ör. büyük bir struct) işaretçi daha ucuz olabilir; temel tipler, dilimler ve küçük struct'lar için değer geçirmek ucuzdur ve daha açıktır. İşaretçi kullanmak, nil kontrolünü ve paylaşılan verinin kimin tarafından değiştirildiğini takip etme sorumluluğunu da beraberinde getirir.

new, new(ifade) ve bileşik literaller

new, new(ifade) ve bileşik literaller: new(T), new(ifade), &T{...}, Mevcut değişkenin adresi

Spesifikasyona göre yerleşik new fonksiyonu yeni, başlatılmış bir değişken oluşturur ve onu gösteren bir işaretçi döndürür. Argüman bir tip T ise new(T) sıfır değerli bir T değişkeni ayırır. Go 1.26 sürüm notlarına göre new artık operand olarak bir ifade de kabul eder ve bu ifade değişkenin başlangıç değerini belirler. Spesifikasyon bu durumda değer tipsiz bir sabitse önce varsayılan tipine çevrildiğini ve nil ön tanımlı tanımlayıcısının new argümanı olamayacağını belirtir:

go
package main

import "fmt"

type Ayar struct {
	Port   int
	Zaman  *int // nil: ayarlanmamış
	Etiket string
}

func yeniAyar(port int) *Ayar {
	a := Ayar{Port: port}
	return &a // yerel değişkenin adresini döndürmek güvenlidir
}

func main() {
	p := new(int) // sıfır değerli yeni int
	fmt.Println(*p)

	q := new(42) // Go 1.26: başlangıç değeriyle
	r := new(int64(300))
	fmt.Printf("%d %T %d\n", *q, r, *r)

	a := yeniAyar(8080)
	a.Zaman = new(30)
	fmt.Println(a.Port, *a.Zaman)

	b := &Ayar{} // new(Ayar) ile aynı
	fmt.Println(b.Zaman == nil)
}

// Çıktı:
// 0
// 42 *int64 300
// 8080 30
// true

new(42) bir *int döndürür, çünkü tipsiz 42 sabitinin varsayılan tipi intdir. Farklı bir tip istiyorsanız dönüşüm yazarsınız: new(int64(300)). Go 1.26 sürüm notları bu özelliğin özellikle encoding/json veya protocol buffers gibi, isteğe bağlı değerleri işaretçi alanlarla temsil eden serileştirme paketleriyle çalışırken faydalı olduğunu, isteğe bağlı bir alanın tek bir ifadeyle doldurulabildiğini vurgular. Örnekteki a.Zaman = new(30) satırı, eskiden önce geçici bir değişken tanımlayıp adresini almayı gerektiren işi tek ifadede yapıyor.

Effective Go, Go'da new fonksiyonunun belleği başlatmadığını, yalnızca sıfırladığını belirtir; bu yüzden sıfır değeri doğrudan kullanılabilen tipler tasarlamak önemlidir. sync.Mutex ve bytes.Buffer bunun bilinen örnekleridir. Pratikte struct işaretçileri için new yerine çoğunlukla &T{...} bileşik literali kullanılır; Effective Go new(File) ile &File{} ifadelerinin eşdeğer olduğunu söyler. yeniAyar fonksiyonundaki gibi yerel bir değişkenin adresini döndürmek de güvenlidir: değişken ona referans olduğu sürece yaşar.

YazımSonuçNot
new(T)Sıfır değerli T değişkenine *THer tip için
new(ifade)İfadenin değeriyle başlatılmış değişkene işaretçiGo 1.26 ve sonrası
&T{...}Literalle başlatılmış değişkene *TStruct, dizi, dilim, map literalleri
&xMevcut değişkenin adresix adreslenebilir olmalı
make(T, ...)Başlatılmış dilim, map veya kanal (işaretçi değil)Yalnızca bu üç tip

make ile new karışıklığına dikkat edin. Effective Go'ya göre make yalnızca dilim, map ve kanal oluşturur ve *T değil, başlatılmış bir T döndürür. new([]int) ise nil bir dilimi gösteren işaretçi verir ve bu nadiren işe yarar.

nil işaretçi ve çalışma zamanı paniği

nil işaretçi ve çalışma zamanı paniği

Spesifikasyona göre x nil ise *x ifadesini değerlendirmeye çalışmak çalışma anında panik üretir. Struct işaretçisinde alan erişimi de aynı kurala tabidir: işaretçi nil ise alanı okumak ya da yazmak panikler.

go
package main

import "fmt"

type Dugum struct {
	Deger int
	Sonra *Dugum
}

func main() {
	var d *Dugum
	fmt.Println(d == nil)
	fmt.Println(d.Deger)
}
text
true
panic: runtime error: invalid memory address or nil pointer dereference

Panik mesajının ardından gelen satırlar işletim sistemine göre değişir; Windows'ta bir sinyal satırı ve yığın izi yazdırılır. Bu hatanın en sık nedenleri başlatılmamış struct işaretçisi alanları, hata kontrolü yapılmadan kullanılan dönüş değerleri (v, err := f() sonrası err kontrol edilmeden v kullanmak) ve map'ten okunan nil işaretçi değerleridir.

nil her zaman hata anlamına gelmez. İşaretçi alıcılı bir metot nil alıcıyla çağrılabilir; metot alana erişmeden önce nil kontrolü yaparsa bu güvenli ve kullanışlı bir kalıptır. Bağlı liste gibi özyinelemeli yapılarda nil işaretçi "liste sonu" anlamına gelir:

go
package main

import "fmt"

type Dugum struct {
	Deger int
	Sonra *Dugum
}

func (d *Dugum) Uzunluk() int {
	if d == nil { // nil alıcı geçerlidir
		return 0
	}
	return 1 + d.Sonra.Uzunluk()
}

func main() {
	liste := &Dugum{1, &Dugum{2, &Dugum{3, nil}}}
	var degerler []int
	for n := liste; n != nil; n = n.Sonra {
		degerler = append(degerler, n.Deger)
	}
	fmt.Println(degerler)
	fmt.Println(liste.Uzunluk())

	var bos *Dugum
	fmt.Println(bos.Uzunluk())
}

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

bos.Uzunluk() çağrısı paniklemez, çünkü metot alıcıyı dolaylamadan önce d == nil kontrolü yapar. Arayüz değerlerindeki nil davranışı ise farklıdır ve ayrı bir tuzaktır: nil bir işaretçiyi taşıyan arayüz değeri nil değildir. Bu konu Go interface yazısında anlatılıyor.

Struct alanları, dizi elemanları ve çift işaretçi

Adreslenebilirlik kuralı sayesinde bir dizinin elemanının, bir dilim elemanının ya da adreslenebilir bir struct'ın alanının adresini alabilirsiniz. İşaretçiye işaretçi de tanımlanabilir, ancak günlük kodda nadiren gerekir:

go
package main

import "fmt"

func main() {
	x, y := 1, 1
	a, b := &x, &y
	fmt.Println(a == b, *a == *b)

	dizi := [3]int{1, 2, 3}
	e := &dizi[1] // eleman adresi
	*e = 20

	type Nokta struct{ X, Y int }
	n := Nokta{1, 2}
	px := &n.X // alan adresi
	*px = 10
	fmt.Println(dizi, n)

	var pp **int = &a // işaretçiye işaretçi
	**pp = 5
	fmt.Println(x)
}

// Çıktı:
// false true
// [1 20 3] {10 2}
// 5

İlk satırdaki sonuç işaretçi karşılaştırması ile değer karşılaştırması arasındaki farkı gösterir: x ve y aynı değeri taşısa da farklı değişkenlerdir, bu yüzden a == b false, *a == *b true olur. Map elemanlarının adresini alamazsınız; ayrıntı Go map kullanımı yazısında. Dilim elemanının adresini aldıktan sonra append yeni dizi ayırırsa işaretçinin eski diziyi göstermeye devam ettiğini de unutmayın; örneği Go slice append ve copy yazısında.

İşaretçi alıcılı metotlara kısa bir bakış

Metot alıcısı da bir parametre gibi davranır. İşaretçi alıcılı (func (s *Sayac) Arttir()) bir metot alıcıyı değiştirebilir; değer alıcılı bir metot kopya üzerinde çalışır. Effective Go'ya göre değer metotları hem işaretçi hem değer üzerinden çağrılabilir; işaretçi metotları ise yalnızca işaretçiler üzerinden çağrılabilir. Ancak değer adreslenebilir ise derleyici s.Arttir() çağrısını otomatik olarak (&s).Arttir() biçimine çevirir:

go
package main

import "fmt"

type Sayac struct{ n int }

func (s *Sayac) Arttir() { s.n++ }

func (s Sayac) Deger() int { return s.n }

func main() {
	var s Sayac
	s.Arttir() // derleyici (&s).Arttir() yapar
	s.Arttir()

	p := &s
	fmt.Println(p.Deger(), s.Deger()) // değer alıcı her ikisinde de çalışır
}

// Çıktı:
// 2 2

Bu kolaylık, değişkenler için çalışır; map elemanları ya da fonksiyon dönüş değerleri gibi adreslenemeyen değerler için çalışmaz. Metot kümeleri, hangi tipin hangi arayüzü uyguladığı ve alıcı tipi seçimi Go metotlar ve alıcı tipleri yazısının konusu.

Kaçış analizi: yığın mı heap mi?

Go SSS, bir değişkenin yığında (stack) mı heap'te mi tutulduğunu doğruluk açısından bilmenize gerek olmadığını söyler: Go'da her değişken ona referans olduğu sürece yaşar ve uygulamanın seçtiği depolama yeri dilin anlamını etkilemez. Performans açısından ise fark vardır. Derleyici, fonksiyon döndükten sonra referans verilmeyeceğini kanıtlayabildiği yerel değişkenleri yığında tutar; kanıtlayamazsa sarkan işaretçi hatalarını önlemek için değişkeni çöp toplayıcılı heap'e taşır. SSS'e göre adresi alınan bir değişken heap adayıdır, ama temel bir kaçış analizi bazı durumlarda değişkenin yığında kalabileceğini tespit eder.

go
package main

func yerel() int {
	x := 1
	return x
}

func kacan() *int {
	y := 2
	return &y
}

func main() {
	_ = yerel()
	_ = kacan()
}

Go GC rehberi, derleyicinin kaçış analizi kararlarını görmek için -gcflags=-m hata ayıklama bayrağını gösterir (rehberde ayrıntılı çıktı için -m=3 kullanılır). Yukarıdaki dosyayı bu bayrakla derlediğimizde:

bash
go build -gcflags=-m main.go
text
# command-line-arguments
./main.go:3:6: can inline yerel
./main.go:8:6: can inline kacan
./main.go:13:6: can inline main
./main.go:14:11: inlining call to yerel
./main.go:15:11: inlining call to kacan
./main.go:9:2: moved to heap: y

moved to heap: y satırı, adresi döndürülen y değişkeninin heap'e taşındığını söylüyor; değeri döndürülen x için böyle bir satır yok. Bu çıktı derleyici sürümüne göre değişebilir. Kaçış analizi bir optimizasyon konusudur: önce doğru ve okunaklı kod yazın, ancak profil çıkarma (profiling) bir sıcak nokta gösterdiğinde bu ayrıntıya bakın.

İşaretçi aritmetiği yok

Tour of Go, C'den farklı olarak Go'da işaretçi aritmetiği olmadığını belirtir. Go SSS bunun nedenini güvenlik olarak açıklar: işaretçi aritmetiği olmadan, yanlışlıkla geçersiz bir adres türetip başarılı gibi görünen bir dil oluşturmak mümkün olmaz. SSS ayrıca dizi indeksleriyle yazılan döngülerin günümüz derleyicilerinde işaretçi aritmetiği kadar verimli olabildiğini ve aritmetik olmamasının çöp toplayıcının gerçekleştirimini sadeleştirdiğini ekler. Bir işaretçiyi artırmaya çalışmak derleme hatası verir:

go
package main

func main() {
	x := 1
	p := &x
	p++
}
text
# command-line-arguments
./main.go:6:2: invalid operation: p++ (non-numeric type *int)

Düşük seviyeli bellek işlemleri için unsafe paketi vardır, ancak bu paket Go'nun tip güvenliği ve taşınabilirlik garantilerinin dışındadır ve günlük uygulama kodunda gerekmez.

Sık yapılan hatalar

Sık yapılan hatalar: nil işaretçiyi dolaylamak, Gereksiz işaretçi kullanmak, Dilim ve map için işaretçi geçirmek, make ile new karıştırmak
  • nil işaretçiyi dolaylamak: var p *T sonrası p.Alan veya *p panikler. Başlatmayı ve hata kontrolünü atlamayın.
  • Gereksiz işaretçi kullanmak: küçük, değişmeyen değerler için işaretçi geçirmek kodu karmaşıklaştırır ve nil riskini artırır.
  • Dilim ve map için işaretçi geçirmek: elemanları değiştirmek için *[]T veya *map[K]V gerekmez; yalnızca dilim başlığını değiştirmek için gerekir.
  • make ile new karıştırmak: new(map[string]int) nil bir map'i gösteren işaretçi verir; map için make kullanın.
  • Döngüde eleman işaretçisi biriktirmek: range değişkeninin adresi elemanın değil kopyanın adresidir; &s[i] kullanın.
  • Arayüze nil işaretçi koyup nil kontrolü yapmak: arayüz değeri nil olmaz; interface yazısındaki açıklamaya bakın.
  • new(nil) yazmak: spesifikasyon nil argümanını yasaklar; işaretçi tipini açıkça verin: new(*int).

Panik ve derleyici mesajlarının toplu listesi için Go sık karşılaşılan hatalar yazısına bakabilirsiniz.

Sık Sorulan Sorular

Go'da yerel değişkenin adresini döndürmek güvenli mi?

Evet. Effective Go'nun belirttiği gibi C'den farklı olarak Go'da yerel bir değişkenin adresini döndürmek tamamen güvenlidir; değişkenin depolama alanı fonksiyon döndükten sonra da yaşar. Derleyici gerekiyorsa değişkeni heap'e taşır.

new(42) ne işe yarar?

Go 1.26 ve sonrasında new bir ifade alabilir: new(42), değeri 42 olan yeni bir int değişkeni oluşturup onun adresini döndürür. İsteğe bağlı alanları işaretçiyle temsil eden struct'ları doldururken geçici değişken tanımlama ihtiyacını ortadan kaldırır. Modülünüzün go yönergesi 1.26 veya üzeri olmalıdır.

İşaretçi ile referans aynı şey mi?

Go'da "referans tipi" diye ayrı bir dil kavramı yoktur; her şey değerle geçirilir. Dilim, map ve kanal değerleri içlerinde alttaki veriyi gösteren işaretçiler taşıdıkları için referans gibi davranır. Go SSS, bu tasarımın tarihini ve dizilerin neden değer olarak kaldığını ayrıca anlatır.

Bir değişkenin heap'e kaçtığını nasıl görürüm?

go build -gcflags=-m ile derleyin; çıktıda moved to heap satırları kaçan değişkenleri gösterir. Bu bilgi performans çalışmaları içindir; programın doğruluğu depolama yerine bağlı değildir.

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 struct yapıları · Go metotlar ve alıcı tipleri · Go interface · Go 1.26 yenilikleri · Go defer, panic ve recover