Artık, kodumuzu nasıl organize etmemiz gerektiğine bakmanın zamanı geldi.
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
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.
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.
Ş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.
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.
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