5- Tidbits | Go Öğreniyorum

6 Nis 2020 · 10 dk okuma süresi

Bu bölümde Go’nun, neredeyse diğer tüm dillerden farklı özelliklerine değineceğiz.

Error Handling

Go, hataları, istisnalarla(exceptions) değil geri dönüş değerleri vasıtasıyla karşılıyor. Örnek olarak, string tipinde değer alıp tam sayıya çeviren strconv.Atoi fonksiyonunu baz alalım:

package main

import (
  "fmt"
  "os"
  "strconv"
)

func main() {
  if len(os.Args) != 2 {
    os.Exit(1)
  }

  n, err := strconv.Atoi(os.Args[1])
  if err != nil {
    fmt.Println("not a valid number")
  } else {
    fmt.Println(n)
  }
}

Kendi hata tiplerinizi oluşturabilirsiniz; bunun için tek şart, dahili olarak gelen error arayüzünün kontratına uymaktır:

type error interface {
  Error() string
}

Kendi hata mesajlarınızı, errors paketini içe aktararak New fonksiyonu ile kullanmamız yaygın bir yöntemdir:

import (
  "errors"
)

func process(count int) error {
  if count < 1 {
    return errors.New("Invalid count")
  }
  ...
  return nil
}

Go’nun standart kütüphanesinde, hata değişkenlerini kullanmanın yaygın bir yöntemi vardır. Örneğin, io paketi içerisinde, aşağıdaki şekilde tanımlanmış bir EOF hata değişkeni vardır:

var EOF = errors.New("EOF")

Bu değişken, paket dışından erişilebilen (ilk harfi büyük) bir paket değişkenidir. Dosyadan veya STDIN‘den okuma yaparken, çeşitli fonksiyonlar bu hatayı döndürebilir. Bağlamsal olarak mantıklı geldiği durumlarda, siz de bu hatayı kullanabilirsiniz. Ayrıca kullanıcı olarak da bu hatayı kullanabiliriz:

package main

import (
  "fmt"
  "io"
)

func main() {
  var input int
  _, err := fmt.Scan(&input)
  if err == io.EOF {
    fmt.Println("no more input!")
  }
}

Final notu olarak; Go, panic ve recover fonksiyonlarına sahiptir. panic fonksiyonu istisnayı açığa vururken, recover fonksiyonu bir nevi catch gibi davranır.

Defer

Go’nun çöp toplayıcısı (garbage collector) olmasına rağmen, bazı kaynaklar açıkça serbest bırakmamızı gerektirir. Örneğin, dosyalarla işimiz bittikten sonra, onları Close fonksiyonu ile kapatmalıyız. Bu tür kodlar her zaman tehlikelidir. Birincisi, bir fonksiyon yazarken 10 satır önce tanımladığımız şeyi Close ile kapatmayı unutmak kolaydır; ikincisi ise bir fonksiyonun birden fazla return ifadesi bulundurabilecek olmasıdır. Go’nun bu duruma çözümü defer anahtar kelimesidir.

package main

import (
  "fmt"
  "os"
)

func main() {
  file, err := os.Open("a_file_to_read")
  if err != nil {
    fmt.Println(err)
    return
  }
  defer file.Close()
  // dosya okuma kodları buraya yazılır
}

Eğer üstteki kodu çalıştırmayı denerseniz, muhtemelen hata alırsınız (dosya mevcut değil). Buradaki amaç defer ifadesinin nasıl çalıştığını göstermektir. Burada defer ile belirttiğiniz şey her ne olursa olsun, fonksiyonun bitişinde çalıştırılır. Böylece, serbest bırakılması gereken kaynağı tanımladığımız yere yakın bir yerde belirtmiş ve birden fazla return ifadesine rağmen bu işlemin gözden kaçmasını engellemiş olursunuz.

Go fmt

Go’da yazılan birçok program aynı formatlama kurallarını takip eder, yani girinti olarak bir tab karakteri kullanır ve parantezler ifadeleriyle birlikte aynı satırda bulunurlar.

Biliyorum, sizin de alıştığınız kendi stilleriniz var ve buna bağlı kalmak istiyorsunuz, ancak sizi bu kurallara uymaya büyük oranda alıştıracak go fmt komutu var. Kullanımı kolay ve tutarlıdır.

Bir proje içerisindeyken, hem geçerli projeye hem de alt projelerin tümüne kuralları bu komutla uygulayabilirsiniz:

go fmt ./...

Kodunuzun girintisini ayarlamanın yanısıra, ifadeleri hizalar ve içe aktarılan (import) ifadelerini alfabetik olarak sıralar.

Initialized If

Go, koşul çalıştırılmadan hemen önce, atama ifadesi yazmanızı sağlayan, biraz modifiye edilmiş bir if yapısını destekler:

if x := 10; count > x {
 ...
}

Bu çok saçma bir örnek. Daha gerçekçi bir örnek görmek isteyebilirsiniz:

if err := process(); err != nil {
  return err
}

Burada tanımlanan değerler, sadece if yapısının içerisinden ve ona bağlı else if veya else ifadeleri içerisinden erişilebilir.

Empty Interface and Conversions

Çoğu nesne yönelimli dillerde, object olarak isimlendirilen baz sınıflar, tüm sınıfların üst sınıfıdır. Go’da kalıtım yoktur, dolayısıyla böyle bir üst sınıf da yoktur. Go’nun bu göreve üstlenen elemanı, herhangi bir metoda sahip olmayan interface{} yapısıdır. Arayüzler dolaylı olarak implemente edildiği için tüm tipler boş bir arayüzün sözleşmesine uyabilir.

Örneğin, add isminde aşağıdaki gibi bir fonksiyon oluşturabiliriz:

func add(a interface{}, b interface{}) interface{} {
  ...
}

Bir arayüzü, belirli bir tipe dönüştürmek için .(TİP) ifadesini kullanabiliriz:

return a.(int) + b.(int)

Unutmayın, bu örnekte, eğer arayüzün altındaki tip int değilse hata oluşacaktır.

Ayrıca etkili bir switch yapısını da böylelikle kullanabilirsiniz:

switch a.(type) {
  case int:
    fmt.Printf("a bir int ve değeri: %d\n", a)
  case bool, string:
    // ...
  default:
    // ...
}

Boş arayüzleri düşündüğünüzden fazla görecek ve kullanacaksınız. Hiç kuşkusuz ki bu tip kodlar çok temiz değildir. Tip dönüşümleri tehlikelidir ve çirkin kodlar meydana getirir. Ancak, statik tipli dillerde bazen tek seçeneğiniz budur.

Strings and Byte Arrays

Dizgiler(strings) ve baytlar yakından ilişkilidir. Birini diğerine kolayca dönüştürebilirsiniz:

stra := "the spice must flow"
byts := []byte(stra)
strb := string(byts)

Aslında, bu dönüştürme yöntemi çeşitli türlerde de yaygındır. Bazı fonksiyonlar açıkça bir int32 veya int64 ya da bunların imzasız (unsigned) şeklini bekler. Kendinizi aşağıdaki gibi bir dönüştürme yaparken bulabilirsiniz:

int64(count)

Tekrardan sıklıkla kullanacağınız bayt ve dizgi(string) dönüşümüne dönelim. Unutmayın ki []byte(X) veya string(X) dönüşümlerini kullandığınızda, verinin kopyasını oluşturuyorsunuz. Bu gereklidir, çünkü dizgiler sabittir(immutable).

Dizgiler runes adındaki unicode kodlardan meydana gelir. Bir dizginin uzunluğunu alırsanız, muhtemelen beklediğiniz çıktıyı alamayacaksınız. Örneğin, aşağıdaki kodun çıktısı 3 olacaktır:

fmt.Println(len("椒"))

Eğer range ile bir dizginin üzerinde gezinirseniz, byte değil runes tipini alırsınız. Elbette dizgiyi []byte tipine dönüştürürseniz doğru veriyi alacaksınız.

Function Type

Fonksiyonlar; değişken tipi, parametre ve geri dönüş tipi olarak her yerde kullanılabilecek birinci sınıf tiplerdir:

type Add func(a int, b int) int

Fonksiyonların aşağıdaki örnekteki gibi kullanılması, arayüzlerde elde ettiğimiz gibi kodun belirli uygulamalardan soyutlatmasına yardımcı olabilir:

package main

import (
  "fmt"
)

type Add func(a int, b int) int

func main() {
  fmt.Println(process(func(a int, b int) int{
      return a + b
  }))
}

func process(adder Add) int {
  return adder(1, 2)
}

Kitap kaynağı: https://github.com/karlseguin/the-little-go-book