Go dilinde struct, her birinin adı ve tipi olan alanlardan oluşan bileşik bir tiptir. type Kullanici struct { Ad string; Yas int } ile tanımlanır, Kullanici{Ad: "Ayşe", Yas: 31} gibi bir bileşik literal (composite literal) ile oluşturulur ve alanlara nokta ile erişilir. Go'da sınıf ve kalıtım yoktur; struct'lar metotlar, arayüzler ve gömme (embedding) ile birleşerek aynı ihtiyaçları karşılar. Struct bir değer tipidir: atandığında ya da fonksiyona verildiğinde tüm alanları kopyalanır.

Bu yazıda struct tanımlamayı, konumsal ve anahtarlı literalleri, sıfır değerleri, işaretçi üzerinden alan erişimini, anonim struct'ları, gömülü alanları ve terfi eden (promoted) alanları, Go 1.27 ile gelen alan seçici anahtarları, karşılaştırılabilirliği, alan etiketlerini ve bellek düzenini Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Metot tanımlama ayrıntıları için Go metotlar ve alıcı tipleri yazısına, serinin tamamı için Go öğrenme rehberi sayfasına bakabilirsiniz.

Struct tanımlama ve bileşik literaller

Struct tanımlama ve bileşik literaller

Spesifikasyon struct'ı, alan adı verilmiş elemanlar dizisi olarak tanımlar; bir struct içindeki boş olmayan alan adları benzersiz olmalıdır. Bir struct değeri oluşturmanın üç yolu vardır: değişkeni tanımlayıp sıfır değerini kullanmak, konumsal literal yazmak ya da anahtarlı (alan adlı) literal yazmak.

go
package main

import "fmt"

type Kullanici struct {
	Ad    string
	Yas   int
	Aktif bool
}

func main() {
	var bos Kullanici // tüm alanlar sıfır değer
	fmt.Printf("%+v\n", bos)

	a := Kullanici{"Ayşe", 31, true}        // konumsal: tüm alanlar, sırayla
	b := Kullanici{Ad: "Mert", Aktif: true} // anahtarlı: Yas sıfır kalır
	fmt.Printf("%+v\n%+v\n", a, b)

	p := &b
	p.Yas = 28 // (*p).Yas yerine kısa yazım
	fmt.Println(b.Yas)

	nokta := struct{ X, Y int }{3, 4} // anonim struct
	fmt.Println(nokta, nokta.X*nokta.Y)
}

// Çıktı:
// {Ad: Yas:0 Aktif:false}
// {Ad:Ayşe Yas:31 Aktif:true}
// {Ad:Mert Yas:0 Aktif:true}
// 28
// {3 4} 12

Anahtarsız literalde her alan için bir değer, tanım sırasıyla verilmelidir. Anahtarlı literalde ise alanlar istediğiniz sırada yazılabilir ve verilmeyen alanlar sıfır değer alır. Effective Go, eleman listesi tamamen boş olan bir literalin tipin sıfır değerini ürettiğini ve new(File) ile &File{} ifadelerinin eşdeğer olduğunu belirtir. Başka paketlerin struct'larında konumsal literal kullanmak risklidir: paketin yeni bir alan eklemesi kodunuzu derlenmez hale getirir. Bu yüzden dışarıdan gelen tiplerde anahtarlı literal tercih edilir.

%+v fiili alan adlarıyla birlikte yazdırır ve hata ayıklamada kullanışlıdır; tüm fiiller için Go fmt ve Printf yazısına bakın. İşaretçi üzerinden alan erişiminde (*p).Yas yazmanıza gerek yoktur: Tour of Go'nun anlattığı gibi dil p.Yas kısaltmasına izin verir. Spesifikasyonun seçici kuralı da bunu tanımlar: x bir işaretçi tipindeyse ve (*x).f geçerli bir alan seçiciyse x.f onun kısaltmasıdır. İşaretçi nil ise bu alanı okumak ya da yazmak çalışma anında panik üretir.

Anonim struct'lar, tek seferlik bir veri yapısı için tip adı tanımlamadan kullanılır; test tablolarında ve geçici JSON gövdelerinde sık görülür. Effective Go, yerel bir değişkenin adresini döndürmenin C'den farklı olarak tamamen güvenli olduğunu, değişkenin depolama alanının fonksiyon döndükten sonra da yaşadığını belirtir; bu yüzden kurucu fonksiyonlar genellikle return &T{...} biçiminde yazılır.

Gömme (embedding) ve terfi eden alanlar

Bir alanı yalnızca tip adıyla, alan adı vermeden tanımlarsanız bu bir gömülü alan olur. Spesifikasyona göre gömülü alan T ya da *T biçiminde bir tip adı olmalıdır ve nitelenmemiş tip adı alan adı görevi görür. Gömülü alanın alanları ve metotları dış struct'a terfi eder: m.Adres.Sehir yerine m.Sehir yazabilirsiniz.

go
package main

import "fmt"

type Adres struct {
	Sehir string
}

func (a Adres) Etiket() string { return "Şehir: " + a.Sehir }

type Musteri struct {
	Ad string
	Adres
}

type Sube struct {
	Sehir string // gömülü alandaki Sehir'i gölgeler
	Adres
}

func main() {
	m := Musteri{Ad: "Can", Adres: Adres{Sehir: "Bursa"}}
	fmt.Println(m.Sehir, m.Adres.Sehir) // terfi eden alan
	fmt.Println(m.Etiket())             // terfi eden metot

	s := Sube{Sehir: "Merkez", Adres: Adres{Sehir: "İzmir"}}
	fmt.Println(s.Sehir, s.Adres.Sehir, s.Etiket())
}

// Çıktı:
// Bursa Bursa
// Şehir: Bursa
// Merkez İzmir Şehir: İzmir

Effective Go, gömmenin alt sınıflamadan önemli bir farkını vurgular: gömülü tipin metotları dış tipin metotları olur, ama çağrıldıklarında alıcı (receiver) dış tip değil iç tiptir. m.Etiket() çağrısı aslında m.Adres.Etiket() çağrısıdır ve Etiket metodu Musteri hakkında hiçbir şey bilmez.

İsim çakışmalarında kural basittir: daha sığ derinlikteki bir alan ya da metot, daha derindeki aynı adlı öğeyi gölgeler. Sube struct'ındaki Sehir alanı derinlik sıfırda olduğu için s.Sehir onu seçer; gömülü Adres içindeki alana ancak s.Adres.Sehir ile ulaşırsınız. Spesifikasyona göre aynı derinlikte birden fazla aday varsa seçici geçersizdir. Effective Go, aynı derinlikte çakışan bir ad tip tanımının dışında programda hiç kullanılmıyorsa bunun hata sayılmadığını da ekler; bu kural, dışarıdan gömülen tiplere sonradan alan eklendiğinde kodunuzun kırılmasını önler. Gömülü tiplerin metotlarının metot kümelerine nasıl katıldığı ve arayüz uygulaması için Go metotlar ve Go interface yazılarına bakın.

Go 1.27: struct literalinde alan seçici anahtarlar

Go 1.27: struct literalinde alan seçici anahtarlar

Go 1.27 sürüm notlarına göre bir struct literalindeki anahtar artık yalnızca struct'ın üst düzey alan adı değil, struct tipi için geçerli herhangi bir alan seçici olabilir. Pratikte bu, terfi eden bir alanı gömülü struct'ı ayrıca yazmadan doğrudan anahtar olarak kullanabileceğiniz anlamına gelir:

go
package main

import "fmt"

type Nesne struct{ Ad, Renk string }

type Nokta struct {
	Nesne
	X, Y int
}

func main() {
	// Go 1.27: anahtar, terfi eden bir alan da olabilir.
	n := Nokta{Ad: "köşe", X: 1, Y: 2}
	fmt.Printf("%+v\n", n)

	// Go 1.27 öncesi yazım hâlâ geçerli:
	m := Nokta{Nesne: Nesne{Ad: "köşe"}, X: 1, Y: 2}
	fmt.Println(n == m)
}

// Çıktı:
// {Nesne:{Ad:köşe Renk:} X:1 Y:2}
// true

Spesifikasyonun güncel struct literal kuralları bu özelliğe bazı sınırlar koyar. Hedef alana ulaşmak için geçilen gömülü alanların tipi işaretçi olmamalıdır; yani *Nesne gömülü olsaydı Ad anahtarı kullanılamazdı. Ayrıca aynı gömülü struct başka bir anahtarla da belirtilmişse, onun içindeki terfi eden alan anahtar olarak kullanılamaz. Örtüşen anahtarlar derleme hatası verir:

go
package main

import "fmt"

type Nesne struct{ Ad, Renk string }

type Nokta struct {
	Nesne
	X int
}

func main() {
	n := Nokta{Nesne: Nesne{Ad: "a"}, Ad: "b"}
	fmt.Println(n)
}
text
# command-line-arguments
./main.go:13:36: cannot specify promoted field Ad and enclosing embedded field Nesne

Özellik dil sürümüne bağlıdır. Modülünüzün go.mod dosyasında go 1.26 yazıyorsa Go 1.27.3 derleyicisi bile bu yazımı reddeder ve şu mesajı verir (denediğimiz modülde):

text
./main.go:14:13: use of promoted field Nesne.Ad in struct literal of type Nokta requires go1.27 or later (-lang was set to go1.26; check go.mod)

go yönergesinin dil sürümünü nasıl belirlediği Go mod modül yönetimi yazısında, sürümün diğer yenilikleri Go 1.27 yenilikleri yazısında anlatılıyor.

Karşılaştırma, kopyalama ve map anahtarı

Spesifikasyona göre bir struct tipi, tüm alan tipleri karşılaştırılabiliyorsa karşılaştırılabilirdir. İki struct değeri, boş olmayan karşılık gelen alanları eşitse eşittir; alanlar kaynak sırasıyla karşılaştırılır ve ilk farkta durulur. Karşılaştırılabilir struct'lar map anahtarı olarak kullanılabilir. Atama ise struct'ı kopyalar, ama kopya sığdır: struct içindeki dilim, map ya da işaretçi alanları aynı veriyi göstermeye devam eder.

go
package main

import "fmt"

type Para struct {
	Tutar int64
	Birim string
}

type Sepet struct {
	Urunler []string
}

func main() {
	a := Para{100, "TRY"}
	b := Para{100, "TRY"}
	fmt.Println(a == b, a != Para{100, "EUR"})

	toplam := map[Para]int{a: 1}
	toplam[b]++
	fmt.Println(toplam[Para{100, "TRY"}])

	s1 := Sepet{Urunler: []string{"çay"}}
	s2 := s1             // struct kopyalanır, ama dilim aynı diziyi paylaşır
	s2.Urunler[0] = "su" // s1 de etkilenir
	fmt.Println(s1.Urunler, s2.Urunler)
}

// Çıktı:
// true true
// 2
// [su] [su]

s2 := s1 satırı yeni bir Sepet değeri oluşturur; ancak Urunler alanı bir dilim başlığı olduğu için iki struct aynı alttaki diziyi paylaşır ve s2.Urunler[0] ataması s1 tarafında da görünür. Derin kopya gerekiyorsa dilim ve map alanlarını ayrıca kopyalamanız gerekir (slices.Clone, maps.Clone). Dilim içeren bir struct ise == ile karşılaştırılamaz:

go
package main

type Sepet struct {
	Urunler []string
}

func main() {
	a, b := Sepet{}, Sepet{}
	_ = a == b
}
text
# command-line-arguments
./main.go:9:6: invalid operation: a == b (struct containing []string cannot be compared)

Arayüz tipli alanlar özel bir durum yaratır: spesifikasyona göre aynı dinamik tipe sahip iki arayüz değerinin karşılaştırılması, o tip karşılaştırılamıyorsa çalışma anında paniğe yol açar ve bu kural arayüz alanı içeren struct'lar için de geçerlidir. Yani böyle bir struct derlenir ama karşılaştırma çalışma anında başarısız olabilir.

Alan etiketleri (struct tags)

Bir alan bildiriminin ardından gelen isteğe bağlı string literali o alanın etiketidir. Spesifikasyon etiketlerin yansıma (reflection) arayüzüyle görünür olduğunu, struct tip kimliğine katıldığını, bunun dışında dil tarafından yok sayıldığını söyler. reflect.StructTag belgesine göre geleneksel biçim, boşlukla ayrılmış anahtar:"değer" çiftleridir. encoding/json gibi paketler bu etiketleri okuyarak alan adlarını ve seçenekleri belirler:

go
package main

import (
	"encoding/json"
	"fmt"
	"reflect"
)

type Urun struct {
	Ad    string  `json:"ad"`
	Fiyat float64 `json:"fiyat,omitempty"`
	gizli string  // küçük harf: dışa aktarılmaz, JSON'a girmez
}

func main() {
	alan, _ := reflect.TypeFor[Urun]().FieldByName("Ad")
	fmt.Println(alan.Tag.Get("json"))

	b, _ := json.Marshal(Urun{Ad: "Kalem", gizli: "x"})
	fmt.Println(string(b))
}

// Çıktı:
// ad
// {"ad":"Kalem"}

Tag.Get("json") etiketin json anahtarına karşılık gelen değeri döndürür. json.Marshal çıktısında alan adı etiketteki "ad" olarak yazıldı, omitempty seçeneği sıfır değerli Fiyat alanını atladı, küçük harfle başlayan gizli alanı ise dışa aktarılmadığı için hiç kodlanmadı. Dışa aktarma kuralının paket düzeyindeki anlamı Go paket yapısı ve import yazısında, JSON etiket seçeneklerinin tamamı ise Go encoding/json yazısında. Etiketler string literali olduğundan yazım hatalarını derleyici yakalamaz; go vet aracının denetimleri için Go fmt, vet ve doc araçları yazısına bakın.

Boş struct ve bellek düzeni

Alanı olmayan struct{} tipinin boyutu spesifikasyona göre sıfırdır; bu yüzden küme olarak kullanılan map'lerde ve yalnızca sinyal taşıyan kanallarda değer tipi olarak sık kullanılır. Alanların bellekteki sırası ise hizalama (alignment) nedeniyle struct boyutunu etkileyebilir. unsafe.Sizeof ile bunu görebilirsiniz:

go
package main

import (
	"fmt"
	"unsafe"
)

type Bayrak struct{}

type Kayit struct {
	A bool
	B int64
	C bool
}

type Sikisik struct {
	B int64
	A bool
	C bool
}

func main() {
	fmt.Println(unsafe.Sizeof(Bayrak{}))
	fmt.Println(unsafe.Sizeof(Kayit{}), unsafe.Sizeof(Sikisik{}))
}

// Go 1.27.3 windows/amd64 ile alınan çıktı
// (boyutlar mimariye bağlıdır):
// 0
// 24 16

Aynı üç alan iki farklı sırayla yazıldığında boyut değişti, çünkü bu platformda int64 alanı 8 baytlık sınıra hizalanır ve bool alanlarının arasına dolgu (padding) eklenir. Spesifikasyon yalnızca asgari hizalama kurallarını garanti eder; bir struct'ın hizalaması, alanlarının hizalamalarının en büyüğüdür. Boyutlar mimariye ve derleyiciye bağlıdır; çıktı yukarıdaki platformda alınmıştır. Çoğu programda bu ayrıntı önemli değildir; milyonlarca eleman tutan dilimlerde bellek kullanımını düşürmek gerektiğinde değerlendirilir. unsafe paketinin kullanımı taşınabilirlik garantilerinin dışındadır.

Struct özellikleri özeti

KonuKuralNot
Sıfır değerHer alan kendi sıfır değerini alırEk başlatma gerekmeden kullanılabilir
Konumsal literalTüm alanlar, tanım sırasıylaDış paket tiplerinde kırılgan
Anahtarlı literalAlan adı: değer, sıra serbestVerilmeyen alan sıfır kalır
Gömülü alanTip adı alan adı olurAlan ve metotlar terfi eder
Terfi eden alan anahtarıGo 1.27 ve sonrasıİşaretçi gömmeden geçilemez
== karşılaştırmasıTüm alanlar karşılaştırılabilirseDilim, map, fonksiyon alanı engeller
AtamaAlanlar kopyalanır (sığ)Dilim ve map alanları paylaşılır
EtiketAlandan sonra string literalreflect ile okunur

Sık yapılan hatalar

Sık yapılan hatalar: Değer kopyasını değiştirmek, Sığ kopyayı derin sanmak, Dış paket struct'ında konumsal literal, Küçük harfli alanın JSON'a girmesini beklemek
  • Değer kopyasını değiştirmek: struct'ı fonksiyona değerle verip alanını değiştirmek çağıranı etkilemez; işaretçi verin ya da yeni değeri döndürün.
  • Sığ kopyayı derin sanmak: struct atamasında dilim ve map alanları paylaşılmaya devam eder.
  • Dış paket struct'ında konumsal literal: pakete alan eklendiğinde kod kırılır; anahtarlı literal kullanın.
  • Küçük harfli alanın JSON'a girmesini beklemek: dışa aktarılmayan alanlar kodlama paketleri tarafından görülmez.
  • nil struct işaretçisinde alan okumak: var p *T sonrası p.Alan panikler.
  • Terfi eden alanı eski dil sürümünde anahtar yapmak: go.mod içindeki go yönergesi 1.27'den düşükse derleme hatası alırsınız.
  • Map içindeki struct alanını doğrudan değiştirmek: m[k].Alan = v derlenmez; Go map yazısındaki çözümlere bakın.

Sık Sorulan Sorular

Go'da struct bir sınıf mıdır?

Hayır. Go'da sınıf ve kalıtım yoktur. Struct yalnızca veri alanlarını tanımlar; davranış metotlarla, soyutlama arayüzlerle, kod yeniden kullanımı gömme ile sağlanır. Gömme bir alt sınıflama değildir: terfi eden metodun alıcısı her zaman iç tiptir.

Struct'ı değer olarak mı işaretçi olarak mı geçirmeliyim?

Fonksiyonun struct'ı değiştirmesi gerekiyorsa işaretçi geçirin. Go SSS, büyük struct'lar için işaretçinin daha ucuz olabileceğini, küçük struct'larda ise değer geçirmenin verimli ve açık olduğunu belirtir. Ayrıntılı tartışma için Go pointer yazısına bakın.

İki struct'ı nasıl karşılaştırırım?

Tüm alanlar karşılaştırılabiliyorsa doğrudan == kullanın. Dilim veya map alanı varsa == derlenmez; alanları kendiniz karşılaştıran bir fonksiyon yazmanız ya da slices.Equal ve maps.Equal gibi yardımcıları kullanmanız gerekir.

Anonim struct ne zaman kullanılır?

Bir veri yapısına yalnızca tek bir yerde ihtiyaç varsa: tablo tabanlı testlerde test durumlarını listelemek, geçici bir JSON gövdesi oluşturmak ya da yerel bir gruplama yapmak için. Aynı yapı birden fazla yerde kullanılıyorsa adlandırılmış bir tip tanımlamak daha okunaklıdır. Tablo testleri için Go test yazma yazısına bakabilirsiniz.

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 metotlar ve alıcı tipleri · Go interface · Go pointer · Go encoding/json · Go 1.27 yenilikleri