Go öğreniyorum - Gün 6 : Fonksiyonlar - 2

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

Fonksiyonlar konusuna devam ediyoruz.

Chapter 6

Functions

Variadic Functions

Fonksiyona geçilen son parametre için belirlenmiş özel bir durumdur:

package main

import "fmt"

func add_numbers(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    fmt.Println(add_numbers(1, 2, 3))
}

add_numbers fonksiyonu tek parametre alıyor ve aslında ilk ve son parametre. Aslında ... ile birden fazla parametre geçtik fonksiyona. İşte bu tür parametreye variadic parametre deniyor. (bence) adedi belirtilmemiş parametre(ler) olarak da ifade edebiliriz. Bana python ve ruby’deki *args, JavaScript ES2015’deki rest parameters’ı çağrıştırdı.

... yani ellipsis şu anlama geliyor: 0 ya da daha fazla sayıda parametre. add_numbers fonksiyonuna istediğimiz kadar parametre geçebilirdik. Aslında aynı mantık Println fonksiyonunda da var.

Parametre olarak slice da geçebiliriz:

func main() {
    numbers := []int{5, 8, 11, 22, 1}
    fmt.Println(add_numbers(numbers...))
}

Closure

Bu, diğer pek çok dilden de aşina olduğumuz Anonim fonksiyon. Fonksiyon içinde fonksiyon oluşturmak mümkün:

package main

import "fmt"

func main() {
    // burası bir closure
    add := func(x, y int) int {
        return x + y
    }
    fmt.Println(add(1, 6)) // 7
}

add aynı zamanda bir yerel değişken. Bu scope içinde diğer yerel (local) değişkenlere erişebiliyor:

func main() {
    inital_adder := 20
    add := func(x, y int) int {
        inital_adder++ // 1 arttır ve tekrar değeri kendine ata
        return x + y + inital_adder
    }
    fmt.Println(add(1, 6)) // 28
    fmt.Println(add(1, 6)) // 29
    fmt.Println(add(1, 6)) // 30
}

Eğer bir fonksiyon, return value olarak başka bir fonksiyon dönerse bu da closure olur:

package main

import "fmt"

func çiftSayıÜreteci() func() uint {
    i := uint(0)
    return func() (ret uint) {
        ret = i
        i += 2
        return
    }
}

func main() {
    sonrakiÇiftSayı := çiftSayıÜreteci()
    fmt.Println(sonrakiÇiftSayı()) // 0
    fmt.Println(sonrakiÇiftSayı()) // 2
    fmt.Println(sonrakiÇiftSayı()) // 4
}

çiftSayıÜreteci fonksiyonu gerçekten de çift sayı üreten bir anonim fonksiyon dönüyor geriye. Her çağrıldığında local i değişkenine 2 ekliyor.

Recursion

Fonksiyon kendi kendini çağırabilir. Basit bir faktöriyel hesabı:

package main

import "fmt"

func factorial(x uint) uint {
    if x == 0 {
        return 1
    }
    return x * factorial(x-1)
}

func main() {
    fmt.Println(factorial(3)) // 6
}
  1. Parametre 3, 0’a eşit mi? değil.
  2. 3 * factorial(3 - 1) yani factorial(2)
    1. Parametre 2, 0’a eşit mi? değil.
    2. 2 * factorial(2 - 1) yani factorial(1)
      1. Parametre 1, 0’a eşit mi? değil.
      2. 1 * factorial(1 - 1) yani factorial(0)
        1. Parametre 0, 0’a eşit mi? evet.
        2. geriye 1 dön

Yani 3 * 2 * 1 = 6 şeklinde.

Closure ve Recursion yaklaşımları fonksiyonel programlama temellerindendir.

defer

Fonksiyon işini bitirdikten sonra, başka bir fonksiyonu tetikleme / çağırma işine defer denir. Sözlük anlamı olarak ötelemek, sonraya bırakmak olarak tanımlanmış.

package main

import "fmt"

func ilk() {
    fmt.Println("İlk çağrılan")
}

func ikinci() {
    fmt.Println("İkinci çağrılan")
}

func main() {
    defer ikinci()  // bu hareketle, ikinci()’yi ilk()’den sonra çağırdık
    ilk()
}

// İlk çağrılan
// İkinci çağrılan

defer ikinci(), sona attı fonksiyon çağrılmasını. Örneğin bir dosyayı açtığımızda daha sonra mutlaka kapatmak için defer kullanıyoruz:

f,_ := os.Open(filename)
// :
// :
defer f.Close()

Faydaları:

  1. Eğer fonksiyondan birden fazla şey dönerse (if / else olsaydı arada), hepsinden önce Close() çalışır.
  2. Runtime panic oluşsa bile defer edilen fonksiyonar çalışır.

panic ve recover

Runtime error oluşturmak istediğimizde panic fonksiyonunu çağırıyoruz. Bu hataları da recover ile yakalıyoruz (handle ediyoruz). recover, panic’i durdurur, panic’e geçilen parametreleri geride döner:

package main

import "fmt"

func main() {
    panic("Panik!")
    str := recover() // bu çalışmaz
    fmt.Println(str)
}

Bu örnekte recover çalışamaz. panic olduğu an execution durur yani o noktadan itibaren kod çalışmaz. Bu bakımdan defer gerekiyor:

package main

import "fmt"

func main() {
    defer func() {
        str := recover()
        fmt.Println(str) // Panik!
    }()
    panic("Panik!")
}

panic genelde programın (kodun) yapabileceği hataları işaret eder. 3 elemanlı bir array’den 4.’yü isterken ya da map’i initialize etmeyi unuttuğumuzda panic oluşur.

Pointers

Değişkenin hafızada durduğu yeri refere eden şeye Pointer denir. x := 5 ifadesinde, x’in değeri olan 5 yerine, x’in hafızada rezerve ettiği / durduğu yeri gösterir pointer.

Argüman alan bir fonksiyon çağırdığımızda, argüman, fonksiyona kopyalanır:

package main

import "fmt"

func zero(x int) {
    x = 0
}

func main() {
    x := 5
    zero(x)
    fmt.Println(x) // x halen 5
}

Eğer parametre olarak x’in pointer’ını geçseydik ve zero fonksiyonu da pointer karşılasaydı:

package main

import "fmt"

func zero(xPointer *int) {
    *xPointer = 0
}

func main() {
    x := 5
    zero(&x)
    fmt.Println(x) // x artık 0 oldu
}

zero’ya değişkenin pointer’ı yani hafızadaki adresi geçiliyor. Yaptığımız iş ilgili adresin içini değiştirmek.

* ve & operatörleri

Pointer * ile gösterilir. func zero(xPointer *int) bu deklarasyonda, xPointer aslında int’i işaret eder, yani int’in pointer’ı dır. Aynı zamanda pointer değişkenlerini de-refere (dereference) etmek için kullanılır. Yani pointer’ın işaret ettiği şeyin değerini almak için.

*xPointer = 0 atamasında, int 0’ı hafızada xPointer’ın işaret ettiği yere koy. main()’de x := 5 derken, x için hafızada bir adres rezerve edilip içine 5 koyuluyor:

+---+---+---+---+---+---+---+---+
|   | A | B | C | D | E | F | G |
+---+---+---+---+---+---+---+---+
| A | 5 |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
| B |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+
| C |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+

x = 5 => AA’ya 5 koy
&x dediğimiz şey ise x’in değeri değil, AA :)

& ise değişkenin adresini bulmak için kullanılır. &x bize *int yani int’i işaret edeni döner. x artık int’dir. zero()’daki xPointer ile &x aynı yeri işaret eder.

new

Pointer’ı almak için yerleşik (built-in) fonksiyondur.

package main

import "fmt"

func one(xPointer *int) {
    *xPointer = 1
}

func main() {
    xPointer := new(int)
    one(xPointer)
    fmt.Println(*xPointer) // 1
}

xPointer := new(int) ile hafızada bir int’i işaret eden adresi xPointer’a atadık. Daha sonra one() ile o adresin içini 1 yaptık. Daha sonra o adresin içini print ettik.

new argüman olarak tip alır, yeterli hafıza tahsisini (allocation) yapar ve değerin sığacağı kadarlık yerin pointer’ını geri döner. Diğer dillerde new ile birşey tanımlayınca mutlaka bu tanımlanan şeyi yoketmemiz gerekir. Go’da garbage collection işi çok iyi ve otomatik olduğu için bu tarz destruct/destroy işlerini düşünmemize gerek kalmaz.