Ana içeriğe geç

Bölüm 05/01: Fonksiyonlar

Konular içinde bu noktaya gelene kadar kabaca fonksiyonları kullandık. Hem kendimiz yazdık hem de built-in paket’lerden gelen (fmt.Println gibi) pek çok fonksiyonu da çağırdık.

Belirli bir işlevi yerine getiren ve çağrıldığında belirli bir işlemi gerçekleştiren, sıklıkla çağırana geri sonuç/sonuçlar dönen kod bloklarıdır fonksiyonlar. Go’daki fonksiyonların karakteristik özellikleri neler?

  • first class citizen yani fonksiyon tip olabilir, başka bir fonksiyona argüman olarak geçilebilir.
  • Anonim olabilirler (closures)
  • Slice ya da Map’in elemanı, key’i value’su olabilirler
  • Bir struct’ın alanı (field’ı) olabilirler
  • Channel’larda send/receive parametresi olabilirler
  • Fonksiyon içinde anonim fonksiyonlar olabilir
  • Sadece paket kapsamında yaşarlar (package scope)

Signature

Fonksiyon imzası (function signature) denen şey aşağıdakiler gibidir:

func Do(a string, b int) string {}           // function signature
func Done(x string, y int) (a string) {}     // function signature

Argümanlar

Fonksiyonlar argümanları (by default) pass by value ile alırlar. Eğer fonksiyon argümanları pointer olarak alırsa bu durumda pass by reference olurlar.

Fonksiyonun aldığı ve döndüğü parametreler, go’nun type safety yaklaşımından dolayı, mutlaka tanımlı tipler olmalı. Yani diğer dinamik dillerdeki (python, ruby, javascript) gibi fonksiyon kafasına göre tipi belli olmayan bir argüman alamaz. Son yıllarda güvenli tip tanımı ruby, python, javascript gibi dillerede gelmeye başladı.

Otomatik olarak pass by value olarak giden tipler:

  • sayısallar (numerics)
  • bool
  • array’ler
  • struct’lar

pass by reference olanlar;

  • pointer
  • string’ler (immutable)
  • slice’lar
  • map’ler
  • channel’lar

Variadics ile, yani N tane argüman geçme/alma işleri ... ile olur:

https://go.dev/play/p/YPbLB5nstXZ

package main

import "fmt"

func greet(names ...string) {
    for _, name := range names {
        fmt.Println("hello", name, "!")
    }
}

func main() {
    greet("vigo") // hello vigo !

    greet("vigo", "erhan")
    // hello vigo !
    // hello erhan !

    users := []string{"turbo", "max", "move"}
    greet(users...)
    // hello turbo !
    // hello max !
    // hello move !
}

func greet(names ...string) N tane string alır, bu names’e atanır, names artık bir string slice yani []string olur. greet(users...) bu durumda da sona eklenen ... ile verilen slice fonksiyona greet(users[0], users[1], users[2], ...) gibi pas edilir.


Return Values

Fonksiyon duruma göre;

  • hiçbir şey dönmeye bilir.
  • bir sonuç dönebilir.
  • N tane sonuç dönebilir.

Error konusunda da değineceğiz ama sırası gelmişken bahsedelim, go’da fonksiyon genelde dönmesi gereken şeyi ve hatayı döner. Hata (error) go’da önemli bir konudur, hatta;

Errors are values

yani error de bi değerdir, bu bakımdan da işlenmesi gerekir. Early exit yaklaşımıyla, fonksiyon hata döndüğü an ya exit (çıkış) yapılır ya da o hata ciddi bir şekilde değerlendirilir. Kodun akışı hemen kesilmelidir.

Bu bakımdan, go’nun kaynak koduna da baktığınızda, neredeyse tüm paket fonksiyonları geriye sonuç + error döner:

func Print(a ...any) (n int, err error)
func Printf(format string, a ...any) (n int, err error)
func Println(a ...any) (n int, err error)

// ve dahası

Hemen bir örnek yapalım:

https://go.dev/play/p/O-5Cuz4k0Dp

package main

import (
    "fmt"
    "log"
)

type users map[string]struct{}

func greetFromMap(u users, name string) (string, error) {
    if _, ok := u[name]; !ok {
        return "", fmt.Errorf("%s not found in map", name)
    }
    return "hello " + name, nil
}

func main() {
    u := users{
        "vigo": {},
    }

    g, err := greetFromMap(u, "vigo")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(g) // hello vigo

    g, err = greetFromMap(u, "lego")
    if err != nil {
        log.Fatal(err)
        // 2023/08/06 13:15:10 lego not found in map
        // exit status 1
    }

    fmt.Println(g)
}

Naked Returns ya da Named Returns

İyi bir pratik olmamakla birlikte, bazen ismilendirilmiş (named) ya da çıplak (naked) geri dönüş değerleri kullanılabilir (return values). Bu tam olarak ne demek? Hemen örneğe bakalım:

https://go.dev/play/p/L1CVHUT19VY

package main

import "fmt"

func sum(a, b int) (result int) {
    result = a + b // buradaki result, (result int)'deki result
    return         // geri dönen şey ne? aaa pardon (result int)'deki result
}

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

Fonksiyon imzasına bakınca func sum(a, b int) (result int) şunu anlıyoruz; Go, fonksiyonu işlemeye başlarken result diye int tipinde bir değişken ataması yapacak, sonra bu fonksiyonun içinde bir yerlerde (function body) birisi result’ı set edecek (değer atayacak) en sonda da o atanan değer geri dönecek (return).

İmza esnasında atanan result artık isimlendirilmiş yani named oluyor. Fonksiyonun sonunda neyin döndüğü bilinmeyen return ifadesi de naked oluyor. Fonksiyonun ne döndüğünü anlamak için sürekli imzaya bakıp takip etmek gerekiyor.

Konu ile ilgili güzel bir makale.


Recursivity

Türkçeye çevirmeye çalışınca Özyineleme, Özyinelemeli fonksiyonlar gibi bir çeviri buldum internette. Kendi kendini çağırabilme durumuna Recursivity deniyor. Bu tür fonksiyonlar da doğal olarak Recursive Functions oluyorlar.

Go bu durumu destekliyor; en klişe örnekle devam edelim; faktöriyel hesabı:

https://go.dev/play/p/l6pS9__0mXp

package main

import "fmt"

func fact(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1) // kendini çağırdı.
}

func main() {
    fmt.Println(fact(3)) // 6
}

Closure

Bir fonksiyon içindeki başka bir fonksiyonun, dışarıdaki yerel değişkenleri de kullanması, kendi tanımlandığı kapsamın dışındaki değişkenlere erişebilme yeteneği yani closes over yapması durumudur. İçerideki fonksiyon dışarıdaki değişkenleri referans olarak alıp kullanır.

https://go.dev/play/p/TT0gtxq6L7U

package main

import "fmt"

func fib() func() int {
    a, b := 0, 1

    // bu fonksiyon a ve b yi kullanabilir
    return func() int {
        a, b = b, a+b
        return b
    }
}

func main() {
    f := fib()

    for x := f(); x < 100; x = f() {
        fmt.Println(x)
    }
}

// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// 55
// 89

Başka bir örnek;

https://go.dev/play/p/gn6O1adOQF-

package main

import "fmt"

func scope() func() int {
    outer_var := 2
    foo := func() int { return outer_var } // 2
    return foo                             // foo fonksiyon olarak döndü, birinin çağırması lazım, çağırana 2 döner
}

func main() {
    // aslında "add" isimli bir fonksiyon tanımı
    add := func(a, b int) int {
        return a + b
    }

    fmt.Println(add(3, 4)) // 7

    sc := scope()
    // geriye fonksiyon döner,
    // bu fonksiyon da geriye int döner

    fmt.Println(sc()) // 2
}

Anonim Fonksiyonlar

Adı, imzası olmayan fonksiyonlar anonim fonksiyonlardır:

// anonim fonksiyon
func() {
    fmt.Println("anonymous")
}()

Vur-kaç (fire and forget) durumlarında, hızlıca çalıştırıp çöpe atacağımız fonksiyonlara ihtiyaç duyduğumuzda, go routine’lerle çalışırken sıklıkla bu tür ifadelere kullanacağız.

Tip ya da Argüman olarak Fonksiyon

Birinci sınıf vatandaş olduğu için, fonksiyonları type definition mantığında kullanabiliyoruz:

https://go.dev/play/p/tiNguGxxvNW

package main

import "fmt"

type GreeterFunc func(string) string

func greet(f GreeterFunc, name string) string {
    return f(name)
}

func main() {
    func1 := func(name string) string {
        return "func1 - " + name
    }

    func2 := func(name string) string {
        return "func2 - " + name
    }

    fmt.Println(greet(func1, "vigo"))  // func1 - vigo
    fmt.Println(greet(func2, "erhan")) // func2 - erhan
}

greet fonksiyonuna string alıp, string dönen herhangi bir fonksiyonu parametre olarak geçebiliriz. type GreeterFunc func(string) string artık GreeterFunc diye bir type’ımız var, tipi ne? bir fonksiyon, nasıl bir fonksiyon? string alıp, string dönen bir fonksiyon.


Defer

Defer kelimesi; tehir etmek geciktirmek anlamındadır. Go’da da aynı mantıkla çalışır, Fonksiyon işini bitirip geri dönmeden önce (return etmeden önce) defer edilenleri çalıştırır ve çıkar.

https://go.dev/play/p/kAYIa_7Qm0U

package main

import "fmt"

func greet(n string) {
    defer func() {
        fmt.Println("exit - greet") // 2. exit - greet
    }()

    fmt.Println("hello ", n)
}

func main() {
    defer func() {
        fmt.Println("exit - main") // 4. exit - main
    }()

    greet("vigo") // 1. hello  vigo

    fmt.Println("after greet") // 3. after greet
}

defer bize pek çok durumda esneklik sağlar. Bir dosya açtık, sonra otomatik kapatmak istiyoruz:

https://go.dev/play/p/rHwN9oMOBa-

package main

import (
    "log"
    "os"
)

func createTempFile() {
    f, err := os.Create("/tmp/foo.txt") // dosyayı oluştur
    if err != nil {
        log.Fatal(err)
    }

    defer f.Close() // fonksiyondan çıkarken dosyayı kapat!
}

func main() {
    createTempFile()
}

createTempFile exit (return) etmeden önce file’ı kapatacaktır. defer çağırılana kadar os.Create işlemini bekler.

defer kullanırken dikkat edilmesi gereken husus şu; kapsam içindeki değişkenlerin değerleri kopyalanır, bu da bazen yanlış sonuç almamıza neden olur:

https://go.dev/play/p/TUsycKzYeDP

package main

import "fmt"

func main() {
    a := 1
    defer fmt.Println("defer a", a) // a'nın değeri 1 kopyalandı

    a = 100             // a artık 100
    fmt.Println("a", a) // a 1000
    // eğer defer a = 100'den sonra tanımlansaydı
    // defer fmt.Println("defer a", a) // 100
}

// a 100
// defer a 1

Fonksiyon return etmeden önce inner-scope yani fonksiyon kapsamı içinde bir değişkeni de bozabilir:

https://go.dev/play/p/Hpbsy_WGRT0

package main

import "fmt"

func do() (a int) {
    // a -> named return
    // şu an allocate edildi, zero-value aldı: 0

    defer func() { a = 100 }() // en son çalışır ve 100 döner

    a = 1  // a'nın değeri değişti; 1 oldu
    return // naked return;
    // defer en son çalıştığı için a’yı bozar...
}

func main() {
    fmt.Println(do()) // 100
}