in Go

Go Programlama Dilinde Idiomatic Errors

Go Programlama Dilin’de en eziyetli konulardan birisi de hata yönetimi veya hata yakalama’dır. Çünkü mimari gereği hiyerarşik bir yapıya sahip olmadığından her fonksiyonda error tipinde bir değer dönmek gerekiyor. Bu da kod tekrarına ve her yerde if err != nil koşulu ile dolmasına neden oluyor. Ben bu görüntüye hep takıyorum :)

Go’da hatalar dilin kendisi (built-in) ile gelen yani literatürde Predeclared identifiers diye geçen error tipi ile ele alınır.  Hata yakalamak için basitçe error tipinin boş olup olmadığı kontrol edilir. Aşağıdaki klasik bir örnektir.

if err != nil{
    panic(err)
}

Go direkt kendi içinde kapsamlı bir errors paketi barındırmıyor fakat ihtiyaçlara göre belirlenmiş üçüncü parti bir “errors” paketi mevcut https://github.com/pkg/errors Developer’ın elini bir nebze güçlendiriyor.

İsimlendirme

Go’da her bir kavramın olduğu gibi Error objelerininde bir isimlendirme teamülü var.

Error işlevi gören bir nesneniz varsa sonu “Error” ile bitmeli. Error ile ilgili bir değişkeniniz varsa da başı “Err” ile başlamalı.

Örneğin bir error tipi için isimlendirme aşağıdaki gibidir.

type CreateError struct {
  Msg, Help string  
}

veya bir error değişkeni tanımlarken ismi aşağıdaki gibi olmalıdır.

var ErrConnection = errors.New("bağlantı sağlanamadı")

Bu arada yukarıdaki tanımlamadaki New fonksiyonuna string geçerken tüm mesajın küçük harf olması gerekiyor. Bu da isimlendirme kurallarından bir tanesi.

Hata mesajının sonuna nokta konulmaz. Başına veya sonuna başka hatalar eklenebilir veya ayrı açıklamalar gelebileceği hep akılda tutulur.

Error Interface

Go’da dil ile gelen bir errors paketi mevcuttur. Bu paket içinde tek fonksiyonlu bir interface barındırır. Bu Interface’i herhangi bir type’a uyguladığınızda sınıfınız doğal bir error nesnesiymiş gibi hareket eder.

Söyle;

type error interface {
    Error() string
}

Aşağıdaki örnekte kendi oluşturduğun bir tipe Error interface’ini uyarlama şeklini bulabilirsin.

type ConnectionError struct {
    reason string
}

func NewConnectionError(reason string) *ConnectionError {
    return &ConnectionError{
        reason: reason,
    }
}

func (c *ConnectionError) Error() string {
    return c.reason
}

ConnectionError isminde bir tipe Error() fonksiyonunu ekleyerek interface’i uygulamış olduk (satisfy interface temiz olay!). Artık bu hata tipini uygulamamızın her yerinde kullanabilirsin. İhtiyaçlara göre genişleteceğimiz bir sarmalama bu işle başlıyor.

Konu ile ilgili güzel bir örnek: https://golang.org/src/os/error.go

Kendi tipimizi aşağıdaki şekilde kullanabiliyoruz.

func connect(host string) (Server, error) {
   conn, err := NewServer(host).Connect()
   if err != nil {
           return conn, NewConnectionError("uzak sunucuya bağlanılamadı")
   }

   return conn, nil
}

Dikkatinizi çekmiştir. Geri dönüş tipini NewConnectionError olarak verebiliyoruz. Go compiler’ı burada arıza yapmaz.  Çünkü Error için belirlenmiş interface’i uyguladığını bilir.

Kodlama sırasında kullanımıda aşağıdaki gibi olabilir rahatlıkla.

_, err := connect("127.0.0.1")

if err != nil{
 fmt.Printf("Hata meydana geldi: %v\n",err)
}

Adamına Göre Yaklaşımı

Go’da try-catch-finally gibi fırlatılan hatanın tipine göre koşullar üreten bir mekanizma yok fakat bunun Go tarzında yapabileceğin bir yol var. Aşağıdaki koda bir bakalım.

var (
    ErrConnection = errors.New("bağlantı sağlanamadı")
    ErrTimeout = errors.New("istek zaman aşımına uğradı")
    ErrInvalidHost = errors.New("uzak sunucu ismi geçersiz")
)

func main() {

    result, err := NewConnection("127.0.0.1")

    if err != nil {

        switch err {
        case ErrConnection:
            //Bağlantı sağlanamadığında yapılacaklar
        case ErrTimeout:
            //Bağlantı zaman aşımına uğradığında yapılacaklar.
        case ErrInvalidHost:
            //Host ismi yanlış olduğunda yapılacaklar.
        default:
            //Hatanın ne olduğu belirlenemediğinde yapılacaklar.
        }
    }

    fmt.Println(result)

}

Olayın püf noktası, kendi hata tiplerimizi oluşturup karşılaştırmak. Kendi hata tiplerimiz dediğim. Yukarıdaki kod’da errors.New ile oluşturulmuş hata değerleri. Eğer belirli hatalara karşı, belirli işler yaptırmak istiyorsak idiomatik yakşalım böyle.

Örnekte NewConnection fonksiyonundan dönen error tipine göre hangi kod blokunun çalıştırılacağını belirleyen bir switch koşulu var. Bu yöntem ile fırlatılan hatayı tipine göre ayırabiliyoruz.

Bu desenin literatürde bir karşılığı vadır ama henüz bilmiyorum. Error Marking gibi bir şey olabilir sanırım. Bilen varsa yoruma yazsın ;)

Gerçek Hayat

Go’nun sunduğu framework olanakları ile temel işlemleri kısmende olsa aşabiliyorsunuz fakat gerçek hayatta geliştirdiğiniz uygulamaların her zaman özgün ihtiyaçları olmuştur.

Gerçek hayat bizi karmaşı yapılar tasarlamamıza iter o nedenle KISS, DRY gibi yaklaşımları cepte tutmak önemli. Bu bölümde MaestroPanel‘in error tipinin tasarlarken nasıl bir yaklaşım sergiledik biraz ona değinmek istiyorum.

Öncelikle;

  • Bir hata mesajından beklentimiz nedir?
  • Hangi alanlar olması gerekiyor?
  • Hataları gören, okuyan ve anlayan kim?
  • Hata mesajı çözümü de sunmalı mı?
  • İyi bir hata mesajı nasıl olmalı?

gibi soruları sorduk kendimize.

Bu sorulara cevap vermeye çalışmak olayı kavramamız ve ön göremediğimiz koşulları tespit etmemize yardımcı oldu. Güzel ve pratik bir teknik başlangıç için.

İlk etapta error tipimizin üzerinde ihtiyacımıza karşılık gelen property’leri belirlememiz gerekti. Hatalar hem son kullanıcıya bilgi vermeli hem de sistem yöneticisine. Hatta beklenmedik bir durumda developer’a da bilgi vermesi elzem.

Yani hata ile karşılaşan her seviye kullanıcı yabancılık çekmeden ne yapacağını bilmesi hedefimiz oldu.

Tip aşağıdaki gibi çıktı. (Sonradan gelen edit: Buraya stack-trace’i de eklemek lazım)

type Error struct {
    Err error
    ErrID string
    ErrMsg string
    Resolve string
    Src ErrorSrc
    Op string
}

Açıklarsak;

  • Err özelliği yakalanan hata mesajını içeriyor.
  • ErrID özelliği hata ile ilgili bir knowledge base merkezine geçilecek ID’yi tutar (Fırlatılan her bilinçli hata için yardım ve çözüm sayfası vermeyi düşünüyoruz)
  • ErrMsg özelliği hatanın herkes tarafından anlaşılabileceği bir versiyonunu taşır.
  • Resolve özelliği hatanın nasıl çözüleceğini kullanıcıya taşır.
  • Src özelliği ise bir Enum tipinde bir kategorik sistemi temsil ediyor. Daha çok hatanın kaynağının ne olduğunu destek ekibinin daha kolay bulması açısından yönlendirme amaçlı. Örneğin: Source, Permission ise. Bu hatanın bir şekilde izinlerle olduğu anlaşılır.
  • Op özelliği hangi faaliyeti gerçekleştirirken bu hata meydana geldi gibi sorulara yanıtlar verir. Örneğin: Nginx.AddVhosts veya MySQL.CreateDatabase gibi değerler alabilir. Buda hangi operasyon sırasında hata meydana geldiğini hızlıca anlaşılmasını sağlar.

Bu yapı daha değişecektir. Belki kullanıcılar URL’ye hiç tıklamayacaklar, belki Resolve alanında verilen çözüm hiç dikkate alınmayacak. Bunları ürünü çıkarttıktan sonra ölçümleyip göreceğiz. Tek kesin olan şey bu type değişecek kesinlikle (felsefeye gel)

Tipi aktif kodlama aşamasında kullanmak için bir constructor’ı olması gerekiyor. Tüm projede bu constructor’ı kullanarak hataları oluşturacağımız için kısa bir şey olması avantaj.

Ek kısa ve akılda kalıcı kelime “New” ama daha da kısası Error’un E’si olarak belirlemek daha mantıklı geldi. Yazması daha kısa. New rezerv kalıyor.

En nihayetinde projede hata yakalarken veya kendi hatamızı üretirken kullanacağımız yapı aşağıdaki gibi oldu.

err := errors.E("Nginx.AddVhost", Src.Exists,"domain.com nginx üzerinde zaten tanımlı","err-7001","Bu hatayı, mevcut alan adını silerek çözebilirsiniz")

Dikkat ederseniz E fonksiyonunda bir nesneyi create etme yok, parametre sırası yok, parametre ismi vererek değer tanımlama yok. Sadece ufak teamüler var.
Sadece errros.E ile kendi Custom Error tipimizi ayaklandırdık ve hata nesnesini üretip kullanılabilir hale getirdik.

Bunu yapabilmek için E fonksiyonuna biraz Go ameleliği uyguladık ama pratik oldu. Aşağıdaki fonksiyona bir bak.

func E(args ...interface{}) error {}

E fonksiyonunu variadic olarak belirledik (… yapınca C#’daki params‘a tekabul ediyor) getirdik ve içeride tipe göre ayırıp ilgili property’lere tanımladık. Tabi 4 property string tipinde geldiğinden tipe göre ayrıştırma yapamıyorsun (Burada bir code smell var sanki! Göreceğiz.)

Onu da şu şekilde çözdük.

  • Op: Operasyon parametresinin formatı “Paket.Aksiyon” şeklinde olmalı.
  • ErrMsg: Mesaj alanı sadece küçük harflerden oluşmalı.
  • Resolve: yani çözüm için yönlendirme alanı büyük harfle başlamalı.
  • ErrID yani hata numarası err-N+ (err-7001 gibi buda http://kb.mp.com/err-7001’e gönderilecek) formatında olmalı.

İşte bunlar hep “Go Way!” :)

Paketi implemente ettikten sonra kodlamaya başladık. Şimdilik pratik gidiyor. Değişiklikler yapıp ekleyip çıkartıyoruz. Muhtemelen bu yazıyı yayınladıktan sonra da bir çok yeri değişecek. Fakat ana fikri hep aynı tutmaya çalışacağım.

Olayı biraz daha kavrayınca jenerik bir paket şeklinde yayınlarım artık.

Bonus

Mesaj

Whatever you do, always check your errors!

EOF

Yorum Bırak

Comment

Webmentions

  • Go Programlama Dili Üzerine – Oğuzhan Blog

    […] Go Programlama Dilinde Idiomatic Errors […]