Go, C-Like syntax (C benzeri syntax) ve garbage collection özelliğine sahip derlenen, statik tipli bir dildir. Peki bunlar ne anlama geliyor? Adım adım bu özelliklere değinelim.
Derleme (compilation) işlemi yazdığınız kaynak kodun, Go’da olduğu gibi assembly’ye veya C# ve Java’da olduğu gibi başka bir aracı bir dile dönüştürülme işlemidir.
Derleme işlemi yavaş olduğu için derlenen dillerle çalışmak hoş olmayabilir. Kodun derlenmesi dakikalar ve saatler alıyorsa hızlı geliştirme yapmanız zordur. Derleme hızı, Go’nun en önemli tasarım hedeflerinden biridir. Bu, büyük projelerde çalışan insanlar için olduğu kadar, yorumlayıcı dillerdeki hızlı geri bildirime alışkın insanlar için de iyi bir haberdir.
Derlenen diller daha hızlı çalışmaya eğilimlidir ve çalıştırılabilir dosyalar (executable files) ek bağımlılıklara ihtiyaç duymadan çalışabilir (en azından C, C++ ve Go gibi doğrudan assembly’e derlenen diller için bu durum böyledir).
Statik tipli (statically typed) demek değişkenin belirli bir tipte (int, string, bool, []byte gibi) olması demektir. Değişkenin tipini, değişken tanımlanırken belirtebiliriz veya derleyicinin tipi algılamasını sağlayabiliriz. (örneklerde buna değineceğiz).
Statik tip hakkında söylenebilecek daha çok şey vardır, ancak, kodlar üzerinden bunun daha anlaşılabilir olacağına inanıyorum. Eğer dinamik tipli dillere aşinaysanız statik tipli dilleri hantal bulabilirsiniz. Haksız sayılmazsınız, ancak statik tipli dillerin de avantajları mevcut, özellikle de statik tipi derleme ile bağdaştırdığınızda.
Bir dilin C-Like syntax (C benzeri syntax) olması demek; C, C++, Java, JavaScript ve C# gibi dillere aşinaysanız en azından Go’ya alışmanız zor olmayacak demektir. Örneğin; &&
ifadesi “mantıksal VE” operatörü, ==
ifadesi eşitlik kontrol operatörü, {
ve }
karakterleri başlangıç ve bitiş kapsam belirleyicileridir, ayrıca dizilerin indisi 0’dan başlar.
C-Like syntax dillerde ifadeler noktalı virgülle bitirilir ve koşulların etrafında parantezler bulunur. Go, bu iki özelliği de zorunlu kılmaz ancak, önceliği kontrol etmek için yine de parantezler kullanılır. Örneğin, bir if
ifadesi şöyle görünür:
if name == "Leto" {
print("the spice must flow")
}
Ve daha karmaşık koşullarda parantezler kullanışlıdır:
if (name == "Goku" && power > 9000) || (name == "gohan" && power < 4000) {
print("super Saiyan")
}
Bunun ötesinde, Go sadece syntax açısından değil, aynı zamanda amaç açısından da C’ye, C# ve Java’dan daha yakındır. Bu, dilin öğrendikçe fark edeceğiniz anlaşılırlığına ve belirginliğine yansır.
Bazı değişkenlerin ömrünü belirlemek kolaydır. Örneğin bir fonksiyonun içerisinde tanımlanan değişken fonksiyon çalıştıktan sonra ömrü son bulur. Bunun dışındaki durumlar o kadar da net değildir, en azından derleyici için. Örneğin; bir fonksiyon tarafından döndürülen değişkenlerin ya da başka değişkenler veya objeler tarafından başvurulan değişkenlerin ömrünü belirlemek zor olabilir. Garbage collection olmadan, değişkenin artık gerekmediği durumda, değişkenin bulunduğu hafıza alanını serbest bırakmak geliştiricinin işidir. Örneğin, bu işi C’de free(str);
komutuyla yapmak mümkündür.
Garbage collection’a sahip diller (örneğin; Ruby, Python, Java, JavaScript, C#, Go) değişkenleri izleyip, artık ihtiyaç duyulmadıkları durumlarda hafızadan silebilirler. Garbage collection yükü arttırır, ama aynı zamanda bazı yıkıcı problemleri de ortadan kaldırır.
Yolculuğumuza basit bir program oluşturup bunu nasıl derleyip çalıştıracağımızı öğrenerek başlayalım. Favori metin editörünüzü açın ve aşağıdaki kodları yazın:
package main
func main() {
println("it's over 9000!")
}
Dosyayı main.go ismiyle kaydedin. Şu an için istediğiniz yere kaydedebilirsiniz; bu tür önemsiz örnekler için Go’nun çalışma alanını (workspace) kullanmamız gerekmiyor.
Daha sonra shell/komut istemini açın ve geçerli dizini dosyanın bulunduğu klasöre değiştirin. Ben kendi bilgisayarımda cd ~/code
komutunu çalıştırdım.
Son olarak, aşağıdaki komut ile programı çalıştırın:
go run main.go
Her şey yolunda gittiyse, it’s over 9000! yazısını görmelisiniz.
Fakat, derleme adımları nerede? go run
komutu kodunuzu derleyip çalıştıran kullanışlı bir komuttur. Bu komut, programı oluşturmak için geçici bir dizin kullanır, çalıştırır ve kendini temizler. Geçici dosyaların yerini alttaki komutu çalıştırarak görebilirsiniz:
go run --work main.go
Kodu derlemek için go build
komutunu kullanın:
go build main.go
Bu komut çalıştırılabilir bir main dosyası oluşturacaktır. Linux/macOS üzerinde çalıştırılabilir dosyanın başına ./main
şeklinde ./
ön ekini eklemeyi unutmayın.
Geliştirme sürecinde go run
veya go build
komutlarını kullanabilirsiniz. Ancak kodunuzu ürün ortamına çıkarırken go build
komutunu kullanarak binary dosyasını oluşturup çalıştırmak isteyeceksiniz.
Üstte yazdığımız kodun gayet anlaşılır olduğunu düşünüyorum. Bir fonksiyon oluşturduk ve dahili println
fonksiyonu ile string
tipindeki ifadeyi ekrana yazdırdık. go run
, kodumuzun içerisinde sadece bir fonksiyon olduğu için mi programı nereden başlatacağını bildi? Hayır. Go’da programın başlangıç noktası main
paketi içerisindeki main
fonksiyonu olmak zorundadır.
Sonraki bölümlerde paketler hakkında daha fazlasına değineceğiz. Şimdilik, Go’nun temellerini öğrenirken, kodumuzu daima main
paketi içerisinde yazacağız.
Eğer isterseniz kodu ve paket ismini değiştirin. Ardından kodu go run
komutu ile çalıştırın, hata almanız gerekiyor. Şimdi ise paket ismini tekrardan main
olarak değiştirin ama bu sefer fonksiyon ismini farklı bir şey yapın. Bu sefer farklı bir hata görmeniz gerekiyor. Eğer bir kütüphane oluşturuyorsanız, herhangi bir başlangıç noktasına (entry point) ihtiyaç yoktur. Bu durumlarda go build
ile derleme yapılabilir.
Go, println
gibi herhangi bir referans göstermeden kullanabileceğiniz bazı dahili fonksiyonlara sahiptir. Go’nun standart kütüphaneleri ve üçüncü parti kütüphanelerini kullanmadan çok ileri gidemeyiz. Go’da, import
anahtar kelimesiyle projede kullanacağımız paketleri bildiririz.
Şimdi programımızı biraz değiştirelim:
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) != 2 {
os.Exit(1)
}
fmt.Println("It's over", os.Args[1])
}
Bu kodu aşağıdaki şekilde çalıştırabilirsiniz:
go run main.go 9000
Şu anda fmt
ve os
olmak üzere Go’nun 2 tane standart paketini kullanıyoruz. Ayrıca len
adında dahili bir Go fonksiyonunu da öğrenmiş olduk. len
fonksiyonu; string’lerin boyutunu, dictionary’deki eleman sayısını veya örnekte kullandığımız gibi dizideki eleman sayısını verir. Yukarıdaki örnekte neden 2 tane argüman beklediğimizi merak ediyorsanız; bunun sebebi ilk argümanın (0 numaralı index) daima o anda çalıştırılan programın yolunu tutmasıdır. İsterseniz yukarıdaki örneği değiştirerek ilk argümanı ekrana yazdırabilirsiniz.
Muhtemelen fonksiyon isimlerinin önünde paket isimlerini kullandığımızı fark ettiniz, örneğin fmt.Println
. Bu diğer birçok dilden farklıdır. Sonraki bölümlerde paketler hakkında daha fazlasını öğreneceğiz. Şimdilik bir paketin nasıl import edilip kullanıldığını bilmek iyi bir başlangıç olacaktır.
Go, paketlerin import edilmesi konusunda otoriterdir. Eğer import ettiğiniz bir paketi kullanmazsanız, Go, kodunuzu derlemeyecektir. Şimdi aşağıdaki kodu çalıştırmayı deneyin:
package main
import (
"fmt"
"os"
)
func main() {
}
fmt
ve os
paketlerinin kullanılmadığına dair 2 tane hata mesajı almış olmalısınız. Bu can sıkıcı olabilir mi? Kesinlikle. Zamanla buna alışacaksınız (ama yine de can sıkıcı olacak). Go, bu konuda katıdır, çünkü kullanılmayan ancak import edilmiş paketler derleme işlemini yavaşlatabilir.
Bir diğer önemli husus ise Go’nun standart paketlerinin çok iyi dökümante edilmiş olmasıdır. Kullandığımız Println
fonksiyonu hakkında daha fazla bilgiye ulaşmak için https://golang.org/pkg/fmt/#Println adresine gidebilirsiniz. Bölümlerin başlığına tıklayarak kaynak kodunu görebilirsiniz.
Eğer internet bağlantınız olmadan herhangi bir noktada takılırsanız, aşağıdaki komutu kullanarak dökümantasyonu lokalde erişilebilir hâle getirebilirsiniz:
godoc -http=:6060
ve tarayıcı ile http://localhost:6060 üzerinden de erişebilirsiniz.
Bir değişken tanımlayıp içine değer atamanın x = 4 şeklinde olduğunu söyleyerek değişkenler konusunu bitirebilsek güzel olurdu. Ne yazık ki, bu konu Go’da biraz daha karmaşık. Konuya basit örneklerle başlayacağız. Structures dersinde yapıları oluşturup kullanmaya göz atarken bu konuyu biraz daha açacağız. Yine de tam anlamıyla alışmanız muhtemelen biraz zaman alacaktır.
Belki, “bu konu ne kadar karmaşık olabilir” diye düşünüyor olabilirsiniz. Hadi, biraz örneklere göz atarak başlayalım.
Değişken bildirimi ve atama ile başa çıkmanın en belirgin yöntemi aynı zamanda en karmaşık olanıdır:
package main
import (
"fmt"
)
func main() {
var power int
power = 9000
fmt.Printf("It's over %d\n", power)
}
Üstteki kod bloğunda int
tipinde power isimli bir değişken tanımladık. Varsayılan olarak Go bu değişkene sıfır değerini atar. Tam sayılar için 0
, boolean’lar için false
, string’ler için ""
değeri atanır. Üstteki kodta, power
isminde değişken bildiriminin ardından değişkene 9000 değerini atadık. İlk iki kod satırını şu şekilde birleştirebiliriz:
var power int = 9000
Hâlâ kodun uzun olduğunu söyleyebiliriz. Go, değişken tipini algılayabilecek kullanışlı bir :=
operatörüne sahiptir:
power := 9000
Bu yöntem kullanışlıdır ve aynı şekilde fonksiyonlar için de geçerlidir:
func main(){
power := getPower()
}
func getPower() int {
return 9001
}
Şunu unutmamanız gerekir ki, :=
operatörü değişken tanımlamak ve tanımlanan değişkene değer atamak için kullanılır, o nedenle aynı değişken için bu operatörü iki kere kullanamayız. Alttaki kod bloğunu çalıştırmayı denediğinizde hata alacaksınız:
func main() {
power := 9000
fmt.Printf("It's over %d\n", power)
// COMPILER ERROR:
// no new variables on left side of :=
power := 9001
fmt.Printf("It's also over %d\n", power)
}
Derleyici, “no new variables on left side of :=” hatasını verecektir. Bu hata, değişkeni ilk tanımladığımızda :=
operatörünü kullandığımız durumların ardından atama yaparken =
operatörünü kullanmamız gerektiği anlamına gelir. Yani; bir değişken için birden fazla deklarasyon yapamayacağımız gibi, :=
operatörünü de aynı değişken için birden fazla kez kullanamayız.
Aslında bu durum oldukça mantıklı, ancak, hangi operatörü ne zaman kullanacağımızı hatırlamak hafızamıza biraz yük olacaktır.
Hata mesajlarını dikkatli okursanız, değişkenler anlamına gelen variable(s) ifadesi, yani çoğul bir ifade kullanılmış. Bunun sebebi; Go’nun hem =
hem de :=
operatörü için çoklu atama yapmaya olanak sağlamasıdır:
func main() {
name, power := "Goku", 9000
fmt.Printf("%s's power is over %d\n", name, power)
}
Değişkenlerden en az birisi yeni olduğu takdirde :=
operatörü kullanılabilir:
func main() {
power := 1000
fmt.Printf("default power is %d\n", power)
name, power := "Goku", 9000
fmt.Printf("%s's power is over %d\n", name, power)
}
power
ismindeki değişken :=
operatörü ile iki kere kullanılmasına rağmen; derleyici, ikinci kullanım için hata vermeyecektir, name
değişkeninin yeni olduğunu görecektir ve :=
operatörüne izin verecektir. Ancak power
değişkeninin tipini değiştiremezsiniz. Tipi ilk ifadede açıkça tam sayı olarak deklare edildiği için, ikinci ifadede sadece tam sayı tipinde değer atanabilir.
Şu an için bu bölümde öğreneceğiniz son şey; Go, import ifadesinde olduğu gibi burada da kullanılmayan değişkenlere izin vermeyecektir. Örneğin:
func main() {
name, power := "Goku", 1000
fmt.Printf("default power is %d\n", power)
}
kodu derlenmeyecektir, çünkü power
isminde değişken tanımlandı ama kullanılmadı. import ifadesinde olduğu gibi bu da can sıkıcı olarak görülebilir, ancak kodun temizliği ve okunabilirliği açısından bunun faydalı olduğunu düşünüyorum.
Atamalar ve deklarasyonlarla ilgili daha öğrenilecek şeyler var. Şu an için; varsayılan değere sahip değişken tanımlarken var NAME TYPE
, tanımlama ve atama yaparken NAME := VALUE
, tanımlaması yapılmış değişkene değer atarken NAME = VALUE
ifadesini kullanacağınızı bilmeniz yeterli.
Fonksiyonların birden fazla değer döndürebildiğini öğrenmenin tam zamanı. Aşağıdaki üç fonksiyonu inceleyin; birincisi geri dönüş değeri olmayan bir fonksiyon, ikincisi bir tane geri dönüş değeri olan fonksiyon, üçüncüsü ise iki tane geri dönüş değeri olan bir fonksiyon:
func log(message string) {
}
func add(a int, b int) int {
}
func power(name string) (int, bool) {
}
Üçüncü fonksiyonu şu şekilde kullanırız:
value, exists := power("goku")
if exists == false {
// handle this error case
}
Bazen geri dönüş değerlerinden sadece biriyle ilgilenirsiniz. Bu durumlarda, ihtiyacınız olmayan değeri _
karakterine atarsınız:
_, exists := power("goku")
if exists == false {
// handle this error case
}
_
karakteri bir isimlendirmeden daha fazlasıdır; bu karakter özel bir karakterdir ve buna aslında atama yapılmaz. Bu karakteri döndürülen tipten bağımsız olarak tekrar tekrar kullanabilirsiniz.
Son olarak, fonksiyon bildirimlerinde karşılaşmanız muhtemel bir şey var. Eğer parametreler aynı tipteyse, daha kısa bir söz dizimi kullanabilirsiniz:
func add(a, b int) int {
}
Birden fazla değer döndürmek sık kullanacağınız bir şeydir. Aynı şekilde herhangi geri dönüş değerini görmezden gelmek için _
karakterini de sıklıkla kullanacaksınız. Adlandırılmış geri dönüş değerleri ve daha az karmaşık görünen parametre tanımlaması ise çok yaygın kullanıma sahip değildir. Yine de er ya da geç bunları kullanacaksınız, bu sebeple bunları bilmeniz önemlidir.
Go Öğreniyorum serisi; Karl Seguin tarafından hazırlanan The Little Go Book kitabını kendi cümlelerimle Türkçe’ye çevirerek oluşturduğum bir seridir. Bu seriyi oluşturmamdaki hedefim, Go dilini öğrenmektir. Bir dili öğrenmek için; önce bir dökümanı bitirip ardından o dil ile proje geliştirmenin etkili bir öğrenme yöntemi olduğunu düşünüyorum. Bu sebeple, bu seriyi oluşturmaya karar verdim.
Kitap içeriğinden kırptığım noktalar olabileceği gibi, kitapta olmayan bazı bilgileri de dahil etmiş olabilirim. Çevirilerde anlamı ve anlaşılabilirliği korumak adına başlıkları, kod bloklarını ve bazı ifadeleri elimden geldiğince İngilizce bırakmaya özen gösterdim.
Kitap kaynağı: https://github.com/karlseguin/the-little-go-book