4- Code Organization and Interfaces | Go Öğreniyorum

29 Mar 2020 · 13 dk okuma süresi

Artık, kodumuzu nasıl organize etmemiz gerektiğine bakmanın zamanı geldi.

Packages

Daha karmaşık kütüphaneleri ve sistemleri kontrol altında tutabilmek için paketler (packages) hakkında bilgi sahibi olmamız gerekir. Go’da, paket isimleri Go’nun çalışma alanının dizin yapısını takip eder. Eğer bir alışveriş sistemi oluşturuyorsak, muhtemelen paket ismi olarak “shopping” paket ismini kullanmak isteyeceğiz ve kaynak kod dosyalarımızı da $GOPATH/src/shopping/ dizini içerisinde barındıracağız.

Elbette bütün dosyalarımızı bu klasörün içerisine koymak istemeyiz. Örneğin, veritabanı işlemlerini içeren bazı dosyaları, kendisine ait bir klasöre koyup izole etmek isteyebiliriz. Bunu yapmak için de $GOPATH/src/shopping/db şeklinde bir alt klasör oluştururuz. Bu veritabanı dosyaların bulunduğu klasör basitçe db olsa da bu pakete shopping paketi veya bir başka paketten erişmek için shopping/db şeklindeki ifadeyle içe aktarmamız gerekir.

Özetle; pakete isim verirken, package ifadesinin yanına hiyerarşiden bağımsız olarak sadece ismini (ör: “shopping”, “db”) veriyoruz, ancak paketi çağırırken hiyerarşik olarak tüm yolu veriyoruz.

Hadi şimdi biraz örnek yapalım. Örneğimiz için shopping adında bir klasör oluşturun ve ardından bu klasörün içerisine de db adında yeni bir alt klasör oluşturun. Şimdi ise shopping/db klasörü içerisine db.go adında yeni bir dosya oluşturun ve alttaki kodları yazın:

package db

type Item struct {
  Price float64
}

func LoadItem(id int) *Item {
  return &Item{
    Price: 9.001,
  }
}

Klasörün ismiyle paketin isminin aynı olmasına dikkat edin. Ayrıca, bu örnekte gerçek bir veritabanı bağlantısı elbette yok, burada amaç sadece kodları nasıl organize ettiğimizi göstermektir.

Şimdi, shopping klasörü altında pricecheck.go adında bir dosya oluşturun. Bu dosyanın içeriği de şöyle olsun:

package shopping

import (
  "shopping/db"
)

func PriceCheck(itemId int) (float64, bool) {
  item := db.LoadItem(itemId)
  if item == nil {
    return 0, false
  }
  return item.Price, true
}

Geçerli olarak shopping klasörü/paketi içerisinde olduğumuz için, shopping/db ifadesinin şu anki klasörü baz alarak, sıradan bir şekilde içe aktarma yaptığını düşünebilirsiniz. Ancak, içe aktarma için baz alınan dizin $GOPATH/src/shopping/db şeklindedir. Yani, eğer $GOPATH/src/test klasörü içerisinde db isminde bir paketiniz varsa, geçerli klasör içerisinden test/db ifadesiyle bu paketi içe aktarabilirsiniz.

Eğer amacınız bir paket oluşturmaksa, şimdiye kadar gördüklerimizle bunu yapabilirsiniz. Eğer amacınız çalıştırılabilir bir dosya oluşturmaksa, hâlâ bir main metoduna ihtiyacınız olacaktır. Şimdi ise üstte oluşturduğumuz paketleri kullanmak için bir çalıştırılabilir uygulama oluşturalım. Bunun için; shopping klasörü altında, main adında bir alt klasör oluşturalım. Bu alt klasörün içerisinde de main.go adında bir dosya oluşturup içine bu kodları yazalım:

package main

import (
  "shopping"
  "fmt"
)

func main() {
  fmt.Println(shopping.PriceCheck(4343))
}

Kodu çalıştırmak için, shopping klasörüne giderek şu komutu çalıştırabilirsiniz:

go run main/main.go

Cyclical Imports

Daha karmaşık sistemler yazmaya başladığınızda, döngüsel içe aktarma durumuna düşebilirsiniz. Bu durum; A paketi B paketini içe aktarırken, B paketinin de A paketini (doğrudan veya dolaylı olarak) içe aktarmasıyla olur. Bu durum derleyecinin izin vermediği bir durumdur.

Shopping projesinin yapısını bahsi geçen hatayı alacak şekilde değiştirelim.

Item tanımını shopping/db/db.go dosyasından shopping/pricecheck.go dosyasına taşıyalım. Yeni hâliyle pricecheck.go dosyası şöyle görünecektir:

package shopping

import (
  "shopping/db"
)

type Item struct {
  Price float64
}

func PriceCheck(itemId int) (float64, bool) {
  item := db.LoadItem(itemId)
  if item == nil {
    return 0, false
  }
  return item.Price, true
}

Bu kodu çalıştırırsanız, 'db/db.go içerisinde, Item öğesinin tanımlanmamış olmasından kaynaklı birkaç hata alacaksınız. Bu makul bir hatadır, çünkü Item öğesi shopping paketi içerisine taşındığı için, artık db paketi içerisinde mevcut değil. shopping/db/db.go dosyasının içeriğini şu şekilde değiştirmeliyiz:

package db

import (
  "shopping"
)

func LoadItem(id int) *shopping.Item {
  return &shopping.Item{
    Price: 9.001,
  }
}

Şimdi, bu kodu çalıştırmayı denediğinizde, korkunç bir import cycle not not allowed hatasını alacaksınız. Bu hatayı, ortak yapıları barındıran bir başka paket oluşturarak çözeceğiz. Dizin yapınız şu şekilde olmalıdır:

$GOPATH/src
  - shopping
    pricecheck.go
    - db
      db.go
    - models
      item.go
    - main
      main.go

pricecheck.go dosyası hâlâ shopping/db paketini içe aktarıyor olacak. Ancak, db.go dosyası shopping yerine shopping/models paketini içe aktarıyor olacak ve dolayısıyla döngü hatası oluşmasını engelleyecek. Ortak yapımız olan Item yapısını shopping/models/item.go içerisine taşıdığımız için, shopping/db/db.go paketinin, Item yapısını models paketi içerisinden referans almasını sağlamalıyız:

package db

import (
  "shopping/models"
)

func LoadItem(id int) *models.Item {
  return &models.Item{
    Price: 9.001,
  }
}

Genellikle, models dışında utilities klasörü gibi çeşitli ortak olarak kullanmak isteyeceğiniz paketler olacaktır. Bu ortak paketleri kullanırken dikkat etmeniz gereken önemli bir husus; bu paketlerin, shopping paketinden veya alt paketinden herhangi bir paketi içe aktarmaması gerektiğidir. Alttaki bölümlerde, bu tarz bağımlılık problemlerini çözmede bize yardımcı olabilecek arazyüzler (interfaces) konusuna değineceğiz.

Visibility

Go, hangi tiplerin veya fonksiyonların paket dışından erişilebilir olacağını tanımlamak için basit bir kural kullanır. Tipin veya fonksiyonun ismi büyük harf ile başlıyorsa, paket dışarısından erişilebilir; eğer küçük harf ile başlıyorsa, paket dışından erişilemez.

Bu aynı zamanda yapılar (structures) için de geçerlidir. Eğer bir yapının ismi küçük harf ile başlıyorsa, sadece bulunduğu paket içerisinden erişilebilir.

Örneğin, eğer items.go dosyası bu şekilde bir fonksiyona sahip olsaydı:

func NewItem() *Item {
  // ...
}

Bu fonksiyonu models.NewItem() şeklinde çağırabilirdik. Eğer fonksiyon newItem olarak isimlendirilseydi, paket dışarısından fonksiyona erişemezdik.

Dilerseniz, shopping içerisinde çeşitli fonksiyon, tip ve alan isimlerini değiştirin. Örneğin, Item tipinin Price alanını, price olarak yeniden adlandırarak alacağınız hatayı inceleyin.

Package Management

Şimdiye kadar, Go komutunun alt komutları olan run ve build alt komutlarını kullandık. Go komutunun sahip olduğu, üçüncü parti kütüphaneleri indirmeye yarayan diğer bir alt komutu da get komutudur. go get alt komutu çeşitli protokolleri destekliyor ancak, biz örneğimizde Github üzerinden kütüphane indireceğiz. Bu nedenle, bilgisayarınızda Git‘in kurulu olması gerekmektedir.

Git‘in kurulu olduğundan emin olun ve ardından, shell veya komut istemini kullanarak şu komutu çalıştırın:

go get github.com/mattn/go-sqlite3

go get uzaktaki dosyaları alır ve sizin çalışma alanınızda depolar. $GOPATH/src dizinine giderek, dizini kontrol edin. Oluşturmuş olduğumuz shopping projesine ek olarak, github.com klasörünü göreceksiniz. Bu klasörün de içinde de go-sqlite3 klasörünü barındıran mattn klasörünü göreceksiniz.

Şimdiye kadar sadece, bu paketleri çalışma alanımıza nasıl indireceğimizi gördük. Yeni indirdiğimiz go-sqlite3 paketini projemize bu şekilde dahil ediyoruz:

import (
  "github.com/mattn/go-sqlite3"
)

Biliyorum, bu klasik bir URL’ye benziyor, ama aslında bu ifade sadece, $GOPATH/src/github.com/mattn/go-sqlite3 dizini içerisinde bulmayı beklediği sqlite3 paketini içe aktarır.

Dependency Management

go get komutunun doğası gereği birkaç özelliği vardır. Eğer bir projede go get komutunu çalıştırırsanız, tüm dosyaların içindeki üçüncü parti import ifadelerini tarar ve indirir. Bir bakıma, kaynak kodlarımız Gemfile veya package.json olarak kullanılır.

go get -u komutu ile paketler güncellenir. Dilerseniz, go get -u TAM_PAKET_ADI şeklinde sadece belirli bir paket güncellenir.

Yine de go get komutunu yetersiz bulabilirsiniz. Mesela, versiyon belirtmenin bir yolu mevcut değil, daima varsayılan olarak master/head/trunk/default baz alınır. Bu durum; bir projede, aynı kütüphanenin farklı versiyonlarına ihtiyaç duyduğunuzda daha büyük bir problem hâline gelir.

Bunu çözmek için, üçüncü parti bir bağımlılık yönetici aracı kullanabilirsiniz. Hâlâ çok yeni olmalarına rağmen; goop ve godep araçları gelecek vaat ediyor. Diğer araçların da bulunduğu listeye de go-wiki üzerinden ulaşabilirsiniz.

Interfaces

Arayüzler (Interfaces), sınıfların elemanlarının bildirildiği ancak implementasyonlarının yapılmadığı tiplerdir. Arayüzleri, sınıfların taslağı gibi düşünebilirsiniz. Örneğin:

type Logger interface {
  Log(message string)
}

Bunun hangi amaca hizmet edebileceğini merak ediyor olabilirsiniz. Arabirimler, kodumuzu belirli gruplara soyutlamamıza yardımcı olur. Örneğin, çeşitli amaçlarla loglama yapan tiplerimiz olabilir:

type SqlLogger struct { ... }
type ConsoleLogger struct { ... }
type FileLogger struct { ... }

Arayüzleri kullanarak kodlama yapmak, kodu bozmadan, kolayca değiştirmemizi ve test etmemizi sağlar.

Peki arayüzleri nasıl kullanabilirsiniz? Herhangi bir başka tip gibi kullanılabilir, örneğin yapıların bir alanı olabilir:

type Server struct {
  logger Logger
}

veya bir fonksiyonun parametresi (veya geri dönüş değeri):

func process(logger Logger) {
  logger.Log("Hello!")
}

C# veya Java gibi bir dilde, bir sınıf arayüzü baz alacaksa bunu açıkça belirtmeliyiz:

public class ConsoleLogger : Logger {
  public void Logger(string message) {
    Console.WriteLine(message)
  }
}

Go’da bu dolaylı olarak gerçekleşir. Eğer yapınız; geri dönüş değeri olmayan, string tipinde parametresi olan bir Log isminde fonksiyona sahipse Logger olarak kullanılabilir:

type ConsoleLogger struct {}
func (l ConsoleLogger) Log(message string) {
  fmt.Println(message)
}

Bu, küçük ve odaksal arayüzler oluşturmaya teşvik eder. Go’nun standart kütüphanesi arayüzlerle doludur. io paketinin; io.Reader, io.Writer ve io.Closer gibi popüler paketleri buna örnektir. Eğer Close() fonksiyonunu çalıştıracak, tek bir parametresi olan fonksiyon yazıyorsanız, kesinlike io.Closer kullanmalısınız.

Arayüzler, ayrıca bileşim desteği sunar ve arayüzler bir başka arayüzün de parçası olabilir. Örneğin, io.ReadCloser, io.Closer gibi io.Reader arayüzünden oluşan bir arayüzdür.

Son olarak, arayüzler döngüsel içe aktarmaları (cyclical imports) engellemek için de yaygın olarak kullanılır. Çünkü, arayüzlerin implementasyonları olmadığı için sınırlı bağımlılıkları olacaktır.


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