Go öğreniyorum - Gün 7 : Struct ve Interface Kavramları

`Introducing Go` kitabından bugün öğrendiklerim

Go ile geliştirdiğimiz programları, sadece kendisiyle yerleşik olarak gelen veri tipleri ile geliştirmek çok sıkıcı olabilirdi :)

Chapter 7

Structs, Interfaces

Karenin ve dairenin alanları hesaplayan bir program yazsaydık;

Koordinat sistemi ve alan hesabı
package main

import (
    "fmt"
    "math"
)

func mesafe(x1, y1, x2, y2 float64) float64 {
    a := x2 - x1
    b := y2 - y1
    return math.Sqrt(a*a + b*b)
}

func dortgenAlan(x1, y1, x2, y2 float64) float64 {
    l := mesafe(x1, y1, x1, y2)
    w := mesafe(x1, y1, x2, y1)
    return l * w
}

func daireAlan(x, y, r float64) float64 {
    return math.Pi * r * r
}

func main() {
    var rx1, ry1 float64 = 20, 20
    var rx2, ry2 float64 = 30, 15
    var cx, cy, cr float64 = 0, 0, 5

    fmt.Println(dortgenAlan(rx1, ry1, rx2, ry2))
    fmt.Println(daireAlan(cx, cy, cr))
}

şeklinde bir program yazmamız gerekecekti. Oysa aynı işi yapan, daha rahat kontrol edilebilen bir yöntem daha var:

Structs

Şimdi daire tipini oluşturalım:

type Circle struct {
    x float64
    y float64
    r float64
}

type anahtar kelimesiyle yeni bir tip ürettiğimizi belirtiyoruz. Circle bu tipin adıdır ve süslü parantezler içinde de bu yeni tipin field’ları yani alanları / özellikleri tanımlanır. Field yani alanlar gruplandırılmış bir değişkenler setidir. Her alanın adı ve tipi bulunur ve aynı fonksiyonların içindeki gibi;

type Circle struct {
    x, y, r float64
}

Initialization yani instance üretmek için; değişken tanımlar gibi var c Circle yani c tip olarak Circle struct’ından türesin diyoruz. Default olarak sıfır 0 değerini alır. Tipine göre olası default değerler;

  • int’ler: 0
  • float’lar: 0.0
  • string’ler: ""
  • pointer’lar: nil

Eğer c := new(Circle) dersek, hafızada field’lar için boş yer rezerve eder ve 0 değerini atarız, daha sonra da geriye struct’ın pointer’ını yani *Circle döneriz. Genelde new pek kullanılmazmış. Mutlaka initial value verilirmiş: c := Circle{x: 0, y: 10, r: 2} gibi…

İsterseniz; c := Circle{0, 10, 2} gibi de atama yapabilirsiniz. Pointer gerektiğinde de c := &Circle{0, 10, 2} yapabilirsiniz.

Fields

Nokta işareti ile, aynı diğer dillerdeki (dot notation) class’ların method ya da property’lerine erişir gibi alanlara erişmek mümkün:

fmt.Println(c.x, c.y, c.r)
c.x = 10
c.y = 20

Şimdi dairenin alanını hesap eden programı aşağıdaki gibi düzenleyelim:

package main

import (
    "fmt"
    "math"
)

type Daire struct {
    x, y, r float64
}

func daireAlan(d Daire) float64 {
    return math.Pi * d.r * d.r
}

func main() {
    d := Daire{0, 0, 5}
    fmt.Println(daireAlan(d))
}

Go’da argümanlar her zaman kopyalanır. Eğer daireAlan fonksiyonu içindeki alanlarda değişiklik yapmak istersek Pointer tekniğini kullanmamız gerekir:

func daireAlan(d *Daire) float64 {
    return math.Pi * d.r * d.r
}

func main() {
    d := Daire{0, 0, 5}
    fmt.Println(daireAlan(&d))
}

Methods

Şimdi, Daire struct’ımıza method ekleyelim:

func (d *Daire) alan() float64 {
    return math.Pi * d.r * d.r
}

func ile fonksiyona adı (alan) arasında kalan kısma RECEIVER deniyor. Receiver aslında bir parametre gibi. Adı ve tipi var : d *Daire. Bu oluşturduğumuz fonksiyonu aynı diğer alanlarla iş yapar gibi . ile çağıracağız: fmt.Println(d.alan()) gibi…

package main

import (
    "fmt"
    "math"
)

type Daire struct {
    x, y, r float64
}

func (d *Daire) alan() float64 {
    return math.Pi * d.r * d.r
}

func main() {
    d := Daire{0, 0, 5}
    fmt.Println(d.alan())
}

Bu sayede daireAlan(&d) çağırmadaki gibi & işaretine ihtiyaç kalmadı. Go, bunu anlıyor ve kendisi otomatik olarak pointer’ı geçiyor çünkü alan() fonksiyonu yanlızca Daire struct’ından türeyen tipler tarafından çağrılabilir durumda artık!

Bana bu yöntem JavaScript’deki Object Oriented programlamayı hatırlattı.

Embedded Types

Struct’lar arasında ilişki kurmak için kullanılır:

type İnsan struct {
    Adı string
}

func (i *İnsan) Konuş() {
    fmt.Println("Selam, benim adım", i.Adı)
}

İnsan tipimiz var. Adı adında string türünde bir alanımız ve Konuş() methodumuz. Şimdi Robot tipmiz olsa;

type Robot struct {
    İnsan İnsan
    Model string
}

Robot da İnsan’dan türemiş ve ek olarak Model’i var string tipinde. Bu örnekte Robot tipinin İnsan’ı var diyoruz aslında. Oysa demek istediğimiz, Robot da bir İnsan:

type Robot struct {
    İnsan
    Model string
}

toparlarsak:

package main

import (
    "fmt"
)

type İnsan struct {
    Adı string
}

func (i *İnsan) Konuş() {
    fmt.Println("Selam, benim adım", i.Adı)
}

type Robot struct {
    İnsan
    Model string
}

func main() {
    r := new(Robot)
    r.Adı = "vigo"
    r.Model = "T1000"
    r.İnsan.Konuş()

    r.Konuş() // direkt olarak
}

Interfaces

Struct gibi düşünülebilir, farkı; struct’da alanları (field) deklare ederken, interface’de method’ları belirtiyoruz. Yani bir tür şema / blueprint!

type Şekil interface {
    alan() float64
}

type kelimesinden sonra interface’in adı gelir. Süslü parantezler içinde de method’ları yazıyoruz. Method’un mutlaka dönüş tipi belirtilmeli.

package main

import (
    "fmt"
    "math"
)

type Şekil interface {
    alan() float64
}

type Daire struct {
    x, y, r float64
}

type Dörtgen struct {
    en, boy float64
}

func (d *Daire) alan() float64 {
    return math.Pi * d.r * d.r
}

func (d *Dörtgen) alan() float64 {
    return d.en * d.boy
}

func tümAlanlar(şekiller ...Şekil) float64 {
    var alan float64
    for _, s := range şekiller {
        alan += s.alan()
    }
    return alan
}

func main() {
    d1 := Daire{0, 0, 5}
    d2 := Daire{0, 0, 15}
    k1 := Dörtgen{10, 20}
    k2 := Dörtgen{20, 2}
    fmt.Println(tümAlanlar(&d1, &d2, &k1, &k2))
}

Bazen interface’ler field olarak da kullanılabilir:

type Şekiller struct {
    şekiller []Şekil
}

Şimdi, farklı farklı şekillerin alanlarını hesaplayacağız, yeterki alan() method’u olsun:

package main

import (
    "fmt"
    "math"
)

type Dörtgen struct {
    en, boy float64
}

type Daire struct {
    x, y, r float64
}

func (d *Dörtgen) alan() float64 {
    return d.en * d.boy
}

func (d *Daire) alan() float64 {
    return math.Pi * d.r * d.r
}

type Şekil interface {
    alan() float64
}

type Şekiller struct {
    şekiller []Şekil
}

func (ş *Şekiller) alan() float64 {
    var a float64
    for _, ş := range ş.şekiller {
        a += ş.alan()
    }
    return a
}

func main() {
    t := Şekiller{
        şekiller: []Şekil{
            &Dörtgen{5, 10},
            &Daire{0, 0, 8},
        },
    }
    fmt.Println(t.alan())
}

Interface’ler büyük ve kompleks projelerde sık sık kullanılır. Keza ileriki konularda göreceğimiz package yani paketleme işlemi yardımıyla farklı farklı interface’leri fonksiyon’ları ve değişkenleri kombine bir şekilde kullanabiliyoruz.