Go; C++, Java, Ruby ve C# gibi nesne yönelimli(object-oriented, kısaca OO) değildir. Objeler ve kalıtım mekanizması yoktur. Bu sebeple, OO ile ilişkili polymorphism ve overloading gibi konseptler de yoktur.
Go, metotlarla ilişkilendirilebilecek yapılara(structures) sahiptir. Go ayrıca, basit ama etkili bir yazım biçimini destekler. Genel olarak daha basit bir kod yapısı sunar, ancak, OO’ın sunduğu bazı özelliklerden mahrum kalacağınız durumlar da olacaktır.
Her ne kadar Go, OO olmasa da Go’daki yapılar ile diğer birçok OO dillerdeki sınıflar(class) arasında birçok benzerlik göreceksiniz: Aşağıda, Saiyan
isminde bir yapı(structure) örneklendirilmiştir:
type Saiyan struct {
Name string
Power int
}
Sınıflarda olduğu gibi yapılara da metot eklemek mümkün elbette, yapılara nasıl metot eklendiğini göreceğiz, ancak öncelikle, deklarasyon (declarations) konusuna geri dönmeliyiz.
Go Öğreniyorum serisinin ilk bölümündeki Variables and Declarations konusunu işlerken, sadece integer ve string gibi dahili tiplere değindik. Şimdi ise yapılara değiniyoruz ve bu konuyu işaretçileri de (pointers) dahil edecek şekilde genişletiyoruz.
Üstteki yapımızdan bir değer yaratmanın en basit yolu şudur:
goku := Saiyan{
Name: "Goku",
Power: 9000,
}
Not: Üstteki yapıda sondaki ‘,’ karakteri gereklidir. Sondaki virgül olmazsa derleyici hata verecektir. Özellikle bu kuralın aksini zorunlu kılan bir dil veya format kullandıysanız, bu kuralı takdir edersiniz.
Bütün alanları(fields) hatta alanlardan herhangi birini dahi belirtmemiz gerekmez. Alttaki iki kullanım da geçerlidir:
goku := Saiyan{}
// veya
goku := Saiyan{Name: "Goku"}
goku.Power = 9000
Atama yapılmayan değişkenlerde olduğu gibi bu alanları da değer atamadan bırakabiliriz.
Ayrıca, deklarasyon sırasını baz alarak alan isimlerini yazmadan da atama yapabilirsiniz ama kod açıklığı için bunu sadece az alana sahip yapılarda kullanmalısınız:
goku := Saiyan{"Goku", 9000}
Üstteki örneklerin tümü goku isminde değişken tanımlayıp ona bir değer atamaktan ibarettir.
Çoğu zaman, bir değişkenin doğrudan bir değeri tutmasını değil, değerimizi tutan değişkenin adresini tutmasını isteriz. İşaretçi(pointer) bir değişkenin bellekteki adresidir, yani değişkenin değerinin bulunduğu adrestir. Kısacası; bir evde bulunmakla, bir eve gidecek adrese sahip olmak arasındaki fark gibidir.
Peki neden doğrudan bir değeri değil de o değere ulaştıracak işaretçiyi kullanmak istiyoruz? Çünkü Go, argümanları metotlara bir kopya olarak gönderir. Bu durumun ne anlama geldiğini biliyor musunuz? Sizce alttaki kod ne çıktı verir?
func main() {
goku := Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s Saiyan) {
s.Power += 10000
}
Cevap 9000 olur, 19000 değil. Peki neden? Çünkü Super
isimli metot, bizim orjinal goku
isimli değişkenimizin kopyasını değiştirdi, böylece Super
metodunun içerisinde yapılan değişiklikler, çağrının yapıldığı yere yansımadı. Bu örneğin beklendiği gibi çalışmasını sağlamak için metoda parametre olarak değişkenin işaretçisini göndermeliyiz:
func main() {
goku := &Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s *Saiyan) {
s.Power += 10000
}
İki tane değişiklik yaptık. İlk değişiklik &
operatörü (address of operator olarak bilinir) ile değişkenin adresini almak oldu. Ardından, Super
ismindeki metodun parametresinin beklediği tipi değiştirdik. İlk başta metodumuz Saiyan
tipinde bir değişken beklerken şimdi ise *Saiyan
tipindeki bir değişkenin adresini bekliyor. *X
demek X
tipindeki değişkenin adresi demektir. Aralarında her ne kadar açık bir benzerlik bulunsa da Saiyan
ve *Saiyan
iki farklı tiplerdir.
Unutmayın ki Super
isimli metoda goku
değişkeninin değerini hâlâ bir kopya olarak gönderiyoruz. Ancak bu sefer goku
değişkeninin değeri bir adrestir. Yani; kopya olarak gönderdiğimiz adres, orijinal adresle aynı olduğu için parametrenin kopya olarak gitmesi sorun teşkil etmiyor. Örnekle ifade edecek olursak; bir restorantın adresini kopyaladığınızı düşünün, bu kopya sizi nereye götürür? Yine restorantın olduğu yere götürür.
Peki kopya olarak gönderdiğimiz adres değişkeninin değerini değiştirirsek ne olur? Unutmayın ki böyle bir şeye ihtiyaç duymanız pek olası değildir, ancak üstte yazdığımız örneği doğrulamak adına deneyebiliriz:
func main() {
goku := &Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s *Saiyan) {
s = &Saiyan{"Gohan", 1000}
}
Üstteki kod bloğu, tekrardan, 9000 çıktısını üretir. Bu davranış; Ruby, Python, Java C ve C# için de geçerlidir.
İşaretçiyi kopyalamanın kompleks bir yapıyı kopyalamaktan daha maliyetsiz olduğunu rahatlıkla söyleyebiliriz. 64-bit bir cihazda, bir işaretçi 64 bit büyüklüğündedir. Çok fazla alana sahip bir yapıdan kopyalar üretmek maliyetli olabilir. Super
isimli metodun goku
değişkeninin kopyasını mı değiştirmesini istersiniz yoksa goku
değişkeninin kendisini mi? İşaretçileri değerli kılan şey işte budur, değerleri paylaşmanıza olanak sağlamasıdır.
Tüm bu anlatılanlar demek değildir ki her zaman işaretçileri kullanmak isteyeceğiz. Bu dersin sonunda, -yapılar ile neler yapabileceğimizi biraz daha işledikten sonra- “Pointers versus Values” başlığı altında, işaretçileri mi değerleri mi kullanmalıyız sorusunu tekrar inceleyeceğiz.
Bir metodu bir yapı ile ilişkilendirebiliriz:
type Saiyan struct {
Name string
Power int
}
func (s *Saiyan) Super() {
s.Power += 10000
}
Üstteki kodda, Saiyan
‘ın Super
ismindeki metodun alıcısı olduğunu belirtiyoruz. Böylelikle, Super
ismindeki metodu şu şekilde çağırıyoruz:
goku := &Saiyan{"Goku", 9001}
goku.Super()
fmt.Println(goku.Power) // 19001 çıktısını verecektir
Yapılar yapıcılara(constructors) sahip değillerdir. Bunun yerine, istenen istenen tipteki bir örneği(instance) döndüren bir fonksiyon oluşturursunuz:
func NewSaiyan(name string, power int) *Saiyan {
return &Saiyan{
Name: name,
Power: power,
}
}
Fonksiyonumuz işaretçi döndürmek zorunda değildir, bu kullanım da tamamen geçerlidir:
func NewSaiyan(name string, power int) Saiyan {
return Saiyan{
Name: name,
Power: power,
}
}
Go, yapıcı metotlara sahip olmamasına rağmen, bir tip için gereken belleği ayırmak için kullanılan, dahili bir new
fonksiyonuna sahiptir. new(X)
ile &X{}
aynı işi yapmaktadır.
goku := new(Saiyan)
// veya
goku := &Saiyan{}
Hangisini kullanacağınız size kalmıştır, ancak; initialize edilecek alanlar olduğunda çoğu geliştirici, daha okunaklı olması açısından ikinci kullanımı tercih eder:
goku := new(Saiyan)
goku.name = "goku"
goku.power = 9001
//vs
goku := &Saiyan {
name: "goku",
power: 9000,
}
Hangi yaklaşımı tercih ederseniz edin, bir önceki başlıkta gördüğümüz deseni kullandığınız takdirde, kodunuzun geri kalanında ne şekilde allocation yaptığınızla ilgilenmek zorunda kalmayacaksınız.
Şimdiye kadar gördüğümüz örneklerde Saiyan
; string
ve int
tiplerinde olmak üzere sırasıyla Name
ve Power
isminde iki alana sahipti. Alanlar; yapı(struct) tipinde olabileceği gibi henüz keşfetmediğimiz array, map, interface ve fonksiyon gibi herhangi bir tipte de olabilir.
Örneğin, Saiyan
yapısını şöyle genişletebiliriz:
type Saiyan struct {
Name string
Power int
Father *Saiyan
}
ve bu şekilde de initialize
edebiliriz:
gohan := &Saiyan{
Name: "Gohan",
Power: 1000,
Father: &Saiyan {
Name: "Goku",
Power: 9001,
Father: nil,
},
}
Go, bir yapıyı bir diğerine eklemeyi sağlayan bileşim(composition) desteği sunar. Bazı dillerde bunlara özellik(trait) veya karışım(mixin) deniliyor. Bileşim (composition) mekanizmasına sahip olmayan diller bunu uzun yoldan yapabilirler. Örneğin Java’da:
public class Person {
private String name;
public String getName() {
return this.name;
}
}
public class Saiyan {
// Saiyan is said to have a person
private Person person;
// we forward the call to person
public String getName() {
return this.person.getName();
}
...
}
Bu yöntem biraz can sıkıcı olabilir. Person
sınıfının her metodu, Saiyan
içerisinde tekrardan yazılmaya ihtiyaç duyuluyor. Go, bu hantal durumu ortadan kaldırıyor:
type Person struct {
Name string
}
func (p *Person) Introduce() {
fmt.Printf("Hi, I'm %s\n", p.Name)
}
type Saiyan struct {
*Person
Power int
}
// and to use it:
goku := &Saiyan{
Person: &Person{"Goku"},
Power: 9001,
}
goku.Introduce()
Saiyan
yapısı *Person
tipinde bir alana sahiptir. Dikkat ederseniz, bu tip için bir isim belirtmedik. Dolaylı tipin (composed type) alan ve fonksiyonlarına dolaylı olarak erişebiliriz. Ancak, Go derleyicisi ilgili alanlar için bir isim vermiştir. Alttaki örnekle konuyu daha anlaşılır hâle getirelim:
goku := &Saiyan{
Person: &Person{"Goku"},
}
fmt.Println(goku.Name)
fmt.Println(goku.Person.Name)
Yukarıdaki her iki çıktı da “Goku” olacaktır.
Şunu da not etmek gerekir: Şayet Saiyan
sınıfında da Name
isminde bir alan olsaydı, goku.Name
ifadesi, Saiyan
yapısının Name
alanını verecekti.
Peki, sizce bileşim (composition) kalıtımdan (inheritance) daha mı iyi? Çoğu kişi bunun daha iyi bir yöntem olduğunu düşünüyor. Kalıtımda, sınıfınız üst sınıfa sıkı sıkıya bağlıdır, bir süre sonra yapının işlevinden daha çok hiyerarşiye odaklanırsınız.
Aşırı yükleme (overloading) yapılara özgü olmasa da ele almaya değer. Özetle, Go aşırı yüklemeyi desteklemiyor. Bundan dolayı; Load
, LoadById
ve LoadByName
gibi birçok fonksiyon görecek ve yazacaksınız.
Ancak, üstte kalın puntoyla bahsettiğim notta, her nasıl Saiyan
yapısındaki Name
bileşim tipindeki Person
yapısının üstüne yazdıysa (overwrite), aynı durum fonksiyonlar için de geçerli. Yani anlayacağınız, bileşim(composition) tipi sadece bir derleyici hilesi olduğundan, “overwrite” özelliğini Go’da kullanabiliyoruz. Örneğin, Saiyan
yapısı kendi Introduce
fonksiyonuna sahip olabilir:
func (s *Saiyan) Introduce() {
fmt.Printf("Hi, I'm %s. Ya!\n", s.Name)
}
Bileşim hâlindeki yapıya s.Person.Introduce()
şeklinde yine erişim sağlayabiliriz.
Go kodu yazarken, kendinize “pointer mı yoksa value mu kullanmalıyım?” diye sormanız normaldir. Size iki iyi haberim var. Birincisi, aşağıdakilerin tümü için cevap aynıdır:
İkinci iyi haber ise; eğer pointer mı yoksa value mu kullanacağınıza karar veremediyseniz pointer kullanın.
Daha önce gördüğümüz gibi; parametreyi value olarak göndermek, değerin fonksiyondan değiştirilmesini önlerken Pointer olarak göndermek, değerim doğrudan fonksiyon içerisinden değiştirilmesine olanak tanır. Bazen value olarak göndermek isteyebilirsiniz, ancak çoğu zaman bunu istemeyeceksiniz.
Eğer verinin değiştirilmesini istemiyor olsanız bile, verinin klonlanma maliyenin önüne geçmek için pointer kullanmak avantajlı olacaktır. Bunun tersi olarak da çok basit veri yapısına sahip olduğunuzu düşünün:
type Point struct {
X int
Y int
}
Bu tür durumlarda, X
ve Y
alanına, pointer ile dolaylı yoldan erişmek yerine doğrudan erişmek, yapının klonlanma maliyetini telafi edecektir. Tabii bunlar oldukça ince hesaplardır. Binlerce hatta on binlerce Point
yapısını aynı anda kullanmadan bu farkı göremeyebilirsiniz.
Kitap kaynağı: https://github.com/karlseguin/the-little-go-book