Sıfırdan “Go”!

Sercan Çakır
İyi Programlama
Published in
31 min readApr 5, 2020

Go programlama dili, Google mühendisleri Robert Griesemer, Rob Pike ve Ken Thompson tarafından 2007 yılında geliştirilmeye başlandı. İlk kararlı sürüm Mart 2012'de açık kaynak olarak yayınlandı.

Tüm kaynak kodlarına Github üzerinden erişebilir hatta gelişim için katkıda bulunabilirsiniz.

Go’nun yeni markası: https://blog.golang.org/go-brand

Go’nun öne çıkan başlıca özellikleri:

  • Açık kaynaklı
  • Statik olarak yazılmıştır
  • Yüksek performans
  • Hızlı derlenme (milyarlarca satırlık proje saniyeler içinde derlenebilir)
  • Garbage collector (çöp toplayıcı) desteği
  • Dahili paket yöneticisi
  • Eşzamanlılık (dil seviyesinde)
  • Her şeyin belgelendiği ayrıntılı dokümantasyonlar
  • ve dahası

Bir Go dosyası temel olarak;

  • fonksiyonlar (methodlar)
  • değişkenler
  • türler
  • yorumlar
  • sabit değişkenlerden oluşur.

Önsöz

Bu yazı, sizleri Go’ya ısındırmak ve hızlı başlangıç yapmanıza yardımcı olmak için oluşturuldu. Isınma turundan sonra Go’yu anlamak için golang.org‘daki dokümanları mutlaka okuyun.

Öğrenmeyi etkili hale getirmek için örnekleri kopyalamak yerine el alışkanlığı kazanmak için yazmaya çalışın. Kodları değiştirin, çıktıları gözlemleyin ve değişimi anlamaya çalışın.

Bölüm içinde (veya sonunda) referans olarak verilen bağlantıları mutlaka inceleyin. Bu kaynaklar, konuyla alakalı daha fazla bilgi ve örneğe ulaşmanız için size yardımcı olacaktır.

Örnek kodları Playground‘da çalıştırabilirsiniz.

Önce alışkanlıklarından vazgeç!

Ezberlemek yerine, bir işin Go’da nasıl yapıldığını öğren, dilin felsefesini anla.

Kurulum

Çalışacağınız ortamda kodları derlemek için öncelikle Go derleyicisini yüklemeniz gerekiyor. Downloads sayfasından işletim sisteminize uygun derleyiciyi indirin ve yükleyin.

Dilerseniz kurulum sayfasındaki yönergeleri takip edebilirsiniz.

Kurulum tamamladıktan sonra test etmek için terminale (CL: komut satırı) aşağıdaki komutu yazın:

go version

Bu komut yüklü olan Go versiyonunu ekrana yazdıracak.

Go aracı tarafından desteklenen tüm komutları görüntülemek için go help komutunu kullanabilirsiniz.

IDE

Go kodlarını herhangi bir IDE (tümleşik geliştirme ortamı) üzerinde yazabilirsiniz. Aşağıdaki önerilerden herhangi birini seçebilirsiniz.

Tercihinize göre IDE için bazı eklentileri kurmakta özgürsünüz.

Çalışma Dizini

Derleyici kurulumundan sonra, Go yükleyicisi ihtiyacına uygun bir kaç dizin oluşturur. Programların geliştirilmesi, derlenmesi ve yürütülmesi için bu dizinler önemlidir. Başlangıçta bilmemiz gereken 2 önemli dizini tanıyalım:

$GOROOT : Go’nun kurulduğu dizin

$GOPATH : geliştirilen projelerin yer aldığı dizin

Kurulum esnasında otomatik tanımlandığı için $GOROOT üzerinde değişiklik yapmaya gerek yoktur.

$GOPATH dizinini tanımlamak/değiştirmek için wiki sayfası veya kurulum belgesi sayfasındaki talimatları (işletim sistemine uygun) takip edebilirsiniz. Başlangıçta değişiklik yapmadan varsayılan $GOPATH dizinini kullanabilirsiniz. Birden fazla $GOPPATH değişkeni tanımlanabilir. Geliştirilecek uygulamaya ait her şey $GOPATH dizininin (çalışma dizini) altında olması gerektiğini unutmayın.

$GOPATH varsayılan olarak MAC sistemler için ~/go olarak tanımlıdır.

Terminal üzerinde go env komutunu yazarak tüm ortam değişkenlerini listeleyebilirsiniz.

İlk Go Programı

Bir programlama dili öğrenirken, gelenek haline gelen “merhaba dünya” örneği ile başlamaya ne dersiniz?

Çalışma dizininde yeni bir Go projesi oluşturun. Proje dizinin içine main.go dosyası oluşturun. Bu dosyanın içine aşağıdaki kodları yazın ve kaydedin:

package main

import "fmt"

func main() {
fmt.Println("merhaba dünya")
}

Go programları paketlerden oluşur. Bu nedenle her Go dosyası mutlaka bir paket adına sahip olmalıdır. Dosyanın en üstüne package sözcüğünden sonra paket adı yazılır. Yürütülebilir (library olmayan) programlar için main paketi başlangıç noktasıdır.

Ortak bir amaca yönelik oluşturulan yeniden kullanılabilir paketlere library (kütüphane) diyebiliriz. Örneğin; kendisine verilen sayıların ortalamasını hesaplayan bir paket oluşturup Github ya da benzeri kaynaklara bu paketi yükleyebilirsiniz. İhtiyaç duyduğunuz her projede bu paketi kullanarak aynı kodları tekrar tekrar yazmaktan kurtulursunuz.

İş kuralı (business rule) içermediği sürece paketlerinizi açık kaynak olarak yayınlayarak, herkesin kullanımına da açabilirsiniz.

Sonraki satırda import sözcüğü ile fmt paketi içe aktarıldı. İçe aktarılan fmt paketinin dışarıdan erişilebilir (exported) tüm değişkenler, fonksiyonlar ve metodlar artık bu program içinde kullanılabilir.

Değişken, fonksiyon ya da method isimlendirmeleri büyük harf ile başlıyorsa bu ifadeler diğer paketler tarafından erişilebilir (public) olur. Örneğin; Ortalama, OrtalamaHesapla, …

Ancak ifade isimleri küçük harf ile başlıyorsa diğer paketler tarafından erişlemez (protected) olur. Sadece aynı paket içindeki diğer dosyalar tarafından erişilebilir. Örneğin; ortalama, ortalamaHesapla , …

Go’da ana giriş noktası main fonksiyonudur. Bu fonksiyon parametre almaz ve geriye herhangi bir değer döndürmez. Son olarak, fonksiyon gövdesinde fmt paketinden Println işlevi (method) ile ekrana istediğimiz mesajı yazdırıyoruz.

Fonksiyonlar ve işlevler (methods) birbirlerine benzerler ancak ifade biçimleri ve kullanım amaçları bakımından birbirlerinden farklıdır. İlerleyen bölümlerde her ikisini de ayrı ayrı inceleyeceğiz.

Bu programı çalıştırarak sonucu görelim. Go programlarını çalıştırmak için farklı yollar mevcuttur. İlk olarak; yazdığımız kodu derleyip, derlenmiş programı yürütmek için 2 farklı komut kullanacağız. Programı derlemek için terminal’i açın ve projenin olduğu dizine gidin. Çalışma dizini üzerindeyken aşağıdaki komutu yazın:

go build ./main.go

Bu komut, yürütülebilir bir ikili (binary) dosya oluşturur. Programı yürütmek için derlenmiş binary dosyayı çalıştırın:

./main

Geliştirme ortamında (local) her seferinde programı derlemek ve derlenmiş dosyayı çalıştırmak yerine daha basit bir yolu tercih edebilirsiniz. Yukarıdaki işlemlerin yerine, derleme adımını soyutlayan run komutunu kullanabiliriz. Aşağıdaki komut; soyut olarak programı derleyip, derlenmiş programı çalıştıracaktır:

go run ./main.go

Go derlenebilir bir dil olduğu için; Go kodlarında herhangi bir değişiklik yapıldığında, değişikliğin yürütülen (çalışan) programa yansıması için programın durdurulup yeniden (derlenerek) çalıştırılması gerekmektedir. Bu Go’ya özel bir durum değil, derlenebilir tüm diller için gerekliliktir.

I/O ve formatlama işlemlerinin gerçekleştirildiği fmt paketi hakkında daha fazla bilgi için paket belgelerine mutlaka göz atın.

Yorumlar

Bazen, programın herhangi bir yerine daha sonra hatırlamak için yorum veya açıklama eklemek ya da fonksiyon/method kullanımını göstermek için basit örnekler yazmak isteyebilirsiniz.

Yorumlar (comments), diğer dillerde de olduğu gibi Go tarafından da desteklenir. Tüm yorum satırları derlenme sırasında göz ardı edilir. Yorum satırları arasına ne isterseniz yazabilirsiniz.

// karakterleri ile başlayan satır, yorum satırı olarak kabul edilir:

// buraya istediğiniz açıklamayı yazabilirsiniz

/* ve */ karakterleri arasındaki her şey blok (çok satırlı) yorum olarak kabul edilir:

/*
buraya
istediğiniz
açıklamayı yazabilirsiniz
*/

Genellikle blok yorumlar bir şeyi uzun uzun ifade etmek için tercih edilir. Bir kaç satırlık açıklama için blok yorum yerine, satır yorumları tekrarlayarak ifade edebilirsiniz:

// buraya
// istediğiniz
// açıklamayı yazabilirsiniz

Yorumları kullanarak kodlarınız için kullanım klavuzu (dokümantasyon) hazırlayabilirsiniz. Mesela, geliştirdiğiniz bir paketi belgeleyerek diğer geliştiriciler için kullanım kolaylığı sağlayabilirsiniz.

Dokümantasyon oluşturmak için bir kaç kurala sadık kalmak gerekiyor:

  • Usule uygun kullanım için Effective Go açıklamalarını mutlaka okuyun.
  • Dokümantasyon oluşturmak için bilinmesi gerekenler Documenting Go Code blog yazısında anlatılmış, incelemenizi öneririm.
  • Yorumları, dokümantasyon belgelerine dönüştüren komut aracını tanımak için godoc paketi için hazırlanan belgeleri inceleyebilirsiniz.

Go’da paket dokümantasyonları, kodların hemen üzerindeki yorumlar ile oluşturulur. Bu özellik, kodlarına doküman yazmak (belgelemek) isteyen bir kişi için inanılmaz kolaylık sağlar.

Yazılım dünyasında dokümantasyonlar çok önemlidir. Daha iyi ifadeler için Github üzerindeki Go paketlerine ait kod yorumlarını mutlaka inceleyin.

Değişkenler

Go statik olarak yazıldığı için, derlenme sırasında değişkenlerin türlerini bilmek veya tahmin etmek ister. Bu nedenle, bir değişken tanımlanırken türü belirtilmeli veya Go’nun tahmin etmesi için açık bir değerle başlatılmalıdır.

Değişkenler, adından da anlaşılabileceği gibi istenildiği zaman değeri değiştirilebilir. Tek kural, yeni değer değişkenin kabul ettiği türde olmalıdır.

Go’da değişken tanımlamak için var deyimi kullanılır. Hemen sonrasında değişkenin adı ve türü belirtilir.

var name  string
var email string
var age int

Değişkenleri tek tek tanımlamak yerine gruplayarak da tanımlayabilirsiniz. Go tercihi size bırakıyor, özgürsünüz.

var (
name string
email string
age int
)

Türü aynı olan değişkenleri tek seferde bildirebilirsiniz. Derlenme sırasında Go kendi anlayacağı formata dönüştürür.

var name, email string
var age int

veya;

var (
name, email string
age int
)

Bir değişken tanımlanırken ilk değerle başlatılabilir. Sabit bir değer olmadığı için başlatılan değer daha sonra aynı türde farklı değerle değiştirilebilir.

var (
name string = "John Doe"
email string = "john@doe.com"
age int = 34
)
// değişkenler, türü aynı olan herhangi bir veri ile
// istenildiği zaman güncellenebilir
age = 27
// her ne olursa olsun, bir değişken farklı türde
// veriyi asla kabul etmez
// age = "on iki"

Değişken eğer başlatıcı değere sahipse, değişken tanımlanırken tür bildirimi atlanabilir. Go, derlenme zamanında başlatıcı değerden tür çıkarımı yaparak (inferred typing) değişkene tür bildirimini yapabilir.

var (
name = "John Doe"
email = "john@doe.com"
age = 34
)

veya;

var name, email, age = "John Doe", "john@doe.com", 34

Değişken başlatıcı değere sahipse := atama operatörü ile var anahtar sözcüğü atılabilir. Bu bildirim şekli sadece fonksiyonların gövdelerinde kullanılabilir, global kapsamda := operatörüne izin verilmez.

import "main"// bu alanda kısa/hızlı değişken bildirimi kullanılamaz,
// sadece "var" deyimi ile değişken bildirilebilir.
int main() {
// fonksiyon gövdelerinde hem "var" deyimi hem de
// kısa bildirim kullanılabilir.
name, email, age := "John Doe", "john@doe.com", 34
// ...
}

Go, derlenme zamanında kısa bildirimi çözümler ve kendi anlayacağı formata dönüştürür.

Go’da bir değişken daha önce tanımlandıysa, aynı kapsam içinde var deyimi ile ikinci kez bildirime (aynı türde olsa dahi) izin verilmez. Ancak kısayol bildirim operatörü ile çoklu değişken bildirimi yaparken, eğer yeni değişken bildirimi yapılıyorsa Go daha önceden tanımlanmış bir değişkeni tolere edebilir. Sıradaki örnekleri inceleyelim.

Aşağıdaki örnek hatalıdır. Her iki değişkende yeniden tanımlandığı için derleyici bu satırın düzeltilmesini ister:

name, age := "John Doe", 34
name, age := "Jane Doe", 30

Aşağıdaki örnekte ise tanımlı değişken, daha sonra bir başka yeni değişken bildirimi ile kullanıldığı için derleyici buna müsade eder. Go, tanımlı bir değişkenin ikinci bildirimini değişken tanımlama yerine değer güncelleme olarak kabul eder.

name, email := "John Doe", "john@doe.com"
name, age := "Jane Doe", 30

Go yukarıdaki kod bloğunu derlenme zamanında çözümleyerek kendi anlayacağı formata dönüştürür. Dönüştürülmüş hali şöyle olacaktır:

var name string = "John Doe"
var email string = "john@doe.com"
name = "Jane Doe"
var age int = 30

Bu bildirim biçiminin sağladığı avantajı ilerleyen bölümlerde “hata yönetimi” konusunu incelerken anlayacaksınız.

Scope

Go dilinde, değişkenler bulundukları scope (kapsam) içinde oluşturulur ve kapsam sonunda öldürülür.

Kısaca, süslü parantezler içinde kalan gövde olarak tanımlayabiliriz kapsamı. Her yeni süslü parantez yeni bir kapsam doğurur.

  • Farklı kapsamlarda, aynı isimde yeni bir değişken oluşturulabilir, hatta aynı isimde fakat türü farklı olabilir.
  • Bir değişken, kapsam dışında kaldığında tanımsızdır.
  • Alt kapsamlar, kendisinden daha üst kapsamda yer alan değişkenlere erişebilir. Ancak tersi mümkün değildir.

Anlamak için devam edelim.

package main

import "fmt"

func main() {
msg := "x"

{
fmt.Println("1:", msg)

msg := "y"

fmt.Println("2:", msg)
}

fmt.Println("3:", msg)
}

Sizce bu program ne yapar? Program çalıştırıldığında aşağıdaki çıktı ekrana yazdırılacaktır:

1: x
2: y
3: x

Yukarıda aslında birbirinden bağımsız 2 farklı değişken tanımladık. Bellekteki yerleri, değerleri birbirinden farklıdır. Sonuçların neden böyle oluştuğunu yorumlayalım:

1: x alt kapsamlar, kendisinden daha üst kapsamdaki değişkenlere erişebilir. Dolayısıyla alt kapsam oluşturulan bu değer kendisinden daha üst katmandaki değişkenin değerini kullandı.

2: yalt kapsamlar, kendisinden daha üst kapsamdaki değişkenleri yeniden oluşturabilir veya güncelleyebilir. Go, kapsam değiştiğinde aynı isimde yeni bir değişken tanımlanmaya izin verdi. Hatta değişkenin türünü dahi değiştirebilirdik. Dilerseniz msg := “y" yerine msg := 5 deneyin.

3: x : üst kapsamlar, alt kapsamda oluşturulmuş olan değişkenlere erişemez. Halen kendi kapsamındaki değere sahip.

Kodları biraz değiştirirsek, alt kapsamların kendisinden daha üst katmandaki değerleri nasıl etkilediğini inceleyebilme fırsatına sahip olabiliriz:

package mainimport "fmt"func main() {
msg:= "x"

{
fmt.Println("1:", msg)
msg = "y"

fmt.Println("2:", msg)
}

fmt.Println("3:", msg)
}

Örnekte tek yaptığımız değişiklik, alt kapsamdaki msg := "y" değerini değişken oluşturma yerine msg = "y" değer güncelleme olarak değiştirdik. Peki sizce sonuç ne olacak?

1: x
2: y
3: y

Sonuçlardan da anlayacağınız üzere, üst kapsamdaki değişkene alt kapsamda eriştiğimiz için güncelleme yetkisine de sahip olduk. Yeni bir değişken tanımlamadığımız için, mevcut değişkeni güncelleyebildik.

Kuralı unutmayın! Değişkenler sadece aynı türde değer ile güncellenebilir. Bu örnekte msg = "y" yerine msg = 5 değişimine izin verilmez. Aynı türde değer atamadığımız için bu hatalı olur.

Kapsam; global, fonksiyon, işlev, if-else ve döngü gövdelerinde de geçerlidir.

for i := 1; i < 5; i++ {
fmt.Println(i)
}

// "i" değişkeni "for" kapsamında tanımlı olduğu için
// bu alanda "i" değişkenine erişilemez

Değişkeni döngü kapsamı yerine daha üst kapsama çıkarmak için:

// "i" değişkenini "for" kapsamından daha üst
// kapsamda tanımlıyoruz
var i int

for i = 1; i < 5; i++ {
fmt.Println(i)
}

// "i" değişkeni artık burada erişilebilir
// hatta değeri "for" kapsamında değişti
fmt.Println("son değer: ", i)

Bu örneği mutlaka deneyin.

Veri Türleri

Go, her şeyin türünü ve değerini bilmek ister. Go, değişkene atanan değeri inceleyerek türünü çıkarım yoluyla keşfedebilir. Ya da türü belirtilmiş ancak değeri atanmamış değişkene varsayılan değeri (varsayılan değer değişken türüne göre farklılık gösterir) atayabilir.

Veri türleri hakkında daha fazlası için Types Specification belgelerini mutlaka inceleyin. Her şey en ince ayrıntısına kadar anlatılmaktadır.

Çok sayıda değişken türü Go tarafından desteklenir. Hatta bazı veri türlerinin farklı kapasiteli halleri mevcuttur. Hızlıca veri türlerine göz atalım:

Numbers

Go’nun sayıları temsil eden birkaç farklı türü vardır. Genellikle sayıları iki farklı türde kabul ederiz: tamsayılar ve kayan nokta sayıları.

Integers (Tamsayılar)

Tamsayı veri tipinin bit sayısına göre değişkenin bellekte kapladığı alan ve kapasitesi farklı olabilir. İşte Go tarafından desteklenen tamsayı veri tipleri:

int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr

var i uint8
fmt.Printf("%d\n", i)

i = 34
fmt.Printf("%d\n", i)

Bu örneğimizde 8 bitlik unsigned sayısal bir değişken tanımı yapıldı, . Bilgisayar biliminde sayısal bir ifade signed ve unsigned olabilir. Eğer değişken unsigned tipinde ise sadece pozitif (sıfır dahil) sayılardan oluşabilir.

Tamsayı tipli değişkene, herhangi bir değer atanmamışsa derleme sırasında varsayılan 0 (sıfır) değeri atanır.

Sayısal veri tipi için iki takma ad türü vardır; uint8 ile byte ,int32 ile rune aynıdır.

Go geliştiricileri, bellek kullanımını ve performansı çok fazla önemser. Bu nedenle ihtiyaca uygun veri türü kullanılır. Değer int8 veri tipi ihtiyacınızı görüyorsa int64 tanımlamanız önerilmez.

Genellikle Go programlarında int veri türü kullanılır.

Kayan Nokta Sayılar (Float Numbers)

Ondalıklı sayıları tutan değişken tipidir. Sayının hassalığına göre float32 veya float64 türünden birine sahip olabilir. Her iki tür için varsayılan olarak 0 (sıfır) değeri atanır.

var f float32
fmt.Printf("%f\n", f)

f = 3.12
fmt.Printf("%f\n", f)

String

Dize değerlerini tutabileceğiniz değişken tipidir. Değer atanmadığı durumda "" (boş dize) değerine sahiptir.

var s string
fmt.Printf("%#v\n", s)

s = "go"
fmt.Printf("%#v\n", s)

Boolean

Yalnızca true veya false değerini tutar. Değeri atanmamış bir değişken varsayılan false değerine sahiptir.

var b bool
fmt.Printf("%t\n", b)

b = true
fmt.Printf("%t\n", b)

Tür Dönüşümü

Go, değişkenlerin türü konusunda fazlaca hassastır. Örneğin; 2 sayısal ifade üzerinde matematiksel işlem yapılacaksa, değişken türlerinin aynı olması beklenir.

Tür dönüşümü T(v) formülü ile özetlenebilir. Yani; v değerini T türüne dönüştürür.

package main

import "fmt"

func main() {
var i int = 42
var f float64 = float64(i)

fmt.Printf("%T(%v)\n", i, i)
fmt.Printf("%T(%v)\n", f, f)
}

Bu örnekte; türü int olan i değişkeni, türü float64 olan f değişkenine kopyalanmadan önce verinin tür dönüşümüne uğradığını söyleyebiliriz.

Tür dönüşümü hakkında daha fazla bilgi ve diğer tür dönüşüm fonksiyonlar için convensions yazısını mutlaka inceleyin.

Sabit Değişkenler

Bazen programda bir defa tanımlanan ve değeri daha sonra değişmemesini istediğimiz ifadelere ihtiyaç duyarız.

Mesela programı versiyonlamak istediğinizde, güncel versiyon numarası bir sabittir. Programa yeni bir özellik eklediğimizde bu değeri bir artırarak sabit tutarız. Neden uygulamanın herhangi bir yerinde değişsin ki?

Sabitler, değişkenlere benzer ancak bazı farklılıklar vardır. Sabitler hakkında bilmeniz gerekenler:

  • var yerine const kullanılır.
  • string, boolean veya numeric türde değer atanabilir.
  • kısayol := operatörü desteklenmez.
  • kesinliği olmayan veriler değer olarak atanamaz.
  • tür bildirilmemişse bağlamın türünü alır.
  • değeri asla değiştirilemez.
  • tanımlanmış ancak kullanılmamış sabitler için derleyici hata vermez.
package main

import "fmt"

const (
Locale string = "tr_TR"
PI = 3.14
PILong = float64(22) / float64(7)
// aşağıdaki ifade sürekli değişen değer ürettiği için
// için sabit bir değişkene atanamaz
// Now = time.Now()
)

func main() {
fmt.Printf("%T(%v)\n", Locale, Locale)
fmt.Printf("%T(%v)\n", PI, PI)
fmt.Printf("%T(%v)\n", PILong, PILong)
}

Operatörler

Go programlama dilinde farklı amaçlarla kullanılan bir takım operatörler bulunur. En çok ihtiyaç duyulan operatörleri hızlıca inceleyelim:

Aritmetik Operatörler

Bu operatörler, sayısal veriler üzerinde matematiksel işlem yapmamıza olanak tanır.

https://golang.org/ref/spec#Arithmetic_operators

Atama Operatörleri

Bir değişkene değer atamak için kullanılan operatörlerdir.

https://golang.org/ref/spec#Assignments

Karşılaştırma Operatörleri

İki veriyi birbiriyle kıyaslayarak bool (mantıksal) bir sonuç döndürür. Eğer karşılaştırma doğru ise true , yanlış ise false değerini döndürür.

https://golang.org/ref/spec#Comparison_operators

Mantıksal Operatörler

Birden fazla mantıksal veriyi zincirleyerek kontrol eder. Kullanılan operatöre göre çıktılanan sonuç farklılık gösterebilir.

https://golang.org/ref/spec#Logical_operators

Adres/İşaretçi Operatörleri

Bir değişkenin bellekteki adresini almak ya da bellek adresinde bulunan değeri öğrenmek için kullanılan operatörlerdir. Yazının devamında işaretçiler (pointers) bölümünde ayrıntılı ele alınacaktır.

https://golang.org/ref/spec#Address_operators

Go, çok daha fazla operatör içerir. Başlangıç için bu listede yer alan operatörleri bilmek yeterlidir. Yine de daha fazlası için operatörler sayfasını inceleyebilirsiniz.

Arrays

Belirli bir uzunlukta (kapasitede) verinin tutulduğu birleşik veri türüdür. Bir array (dizi) oluşturulurken, dizinin kapasitesi önceden belirtilir.

[n]T, T türünde n adet değeri tutan bir dizidir.

var a [5]int

Yukarıdaki ifade 5 adet int türünde veri tutan bir dizi tanımlar. Dizilerin başlangıç indisleri sıfırdır. 2 elemanlı bir dizi 0 ve 1 nolu indislere sahip olur. Kapasiteleri dahilinde, bir dizinin sahip olduğu herhangi bir indisine değer atanabilir veya sahip olduğu indisin değeri güncellenebilir.

Örneğin;

package main

import "fmt"

func main() {
var m [2]int

m[0] = 25

// dizi kapasitesinin yeterli olmadığı indislere
// veri ataması yapılamaz
// m[2] = 5

fmt.Printf("%#v", m)
}

Yukarıdaki örnekte 2 elemanlı bir dizi tanımladık. Dizinin birinci elemanına (0 nolu indis) değer atadık. Ancak ikinci elemanına (1 nolu indis) değer atamadık. Değer ataması yapılmamış dizi elemanları, veri türüne uygun olan varsayılan değer ile doldurulur.

Dilerseniz, dizi tanımlarken dizi elemanlarının sahip olacağı değerleri önceden verebilirsiniz. Dizinin ilk değerlerini tanımlamak için, dizi ifadesinden hemen sonra süslü parantezler içinde ilklendirme değerlerini belirtebilirsiniz.

Aşağıdaki örnekte, dizinin bir kısım elemanları ilklendirilmiştir. İlklendirilmeyen değerlerin, varsayılan değerler ile doldurulduğunu sakın unutmayın.

package main

import "fmt"

func main() {
m := [3]int{25, 5}

fmt.Printf("%#v", m)
}

Dizi elemanlarına erişmek için, erişilmek istenen elemanın indisi köşeli parantezler içinde bildirilmesi yeterlidir:

package main

import "fmt"

func main() {
m := [2]string{"foo", "bar"}

fmt.Println(m[0])

// dizi 2 nolu indise sahip olmadığı için
// aşağıdaki ifade hata fırlatır
// fmt.Println(m[2])
}

Bir dizi veriye sahip olduğunuzu ve kapasite belirtmeden bunu yeni bir diziye atamak istediğinizi varsayalım. Go, bu durumda sizin yerinize kapasite tahmini yapabilir. Bu durumda uzunluğu yazmak yerine ... (üç nokta) operatörünü kullanabilirsiniz.

package main

import "fmt"

func main() {
m := [...]string{"foo", "bar"}

fmt.Printf("%#v", m)
}

Hep tek boyutlu diziler ile çalıştık ama zaman zaman çok boyutlu matrislere de ihtiyaç duyarız. Go, çok boyutlu dizileri destekler. Aşağıdaki örneği inceleyelim:

package main

import "fmt"

func main() {
var a [2][3]string

for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
a[i][j] = fmt.Sprintf("%d-%d", i+1, j+1)
}
}

fmt.Println(a)
}

Yukarıdaki örneği tek boyutlu dizilerde olduğu gibi, daha önceden sahip olduğunuz bir çok boyutlu dizi ile ilklendirebilirsiniz:

package main

import "fmt"

func main() {
a := [3][2]int{
{0, 1},
{2, 3},
{4, 5},
}
fmt.Printf("%#v", a)
}

Çok boyutlu dizilerde bir elemana ulaşmak için tek boyutlu dizilerde olduğu gibi köşeli parantezleri kullanırız. Örneğin; 2 nolu satırın ilk elemanına ulaşmak için:

a[2][0]

ifadesini kullanmamız yeterlidir.

Dizilerle ilgili daha fazla bilgi için örnek uygulamalara ya da Go belgelerine göz atabailirsiniz.

Slices

Dilimler (slices), dizilere daha kullanışlı hale getirmek için dizileri sarar. Dizilere benzer ancak dilimlerin kapasitesi belirsiz olabilir.

[]T, T türünde bir dilimdir.

package main

import "fmt"

func main() {
n := []int{1, 2, 3, 4}

fmt.Printf("%#v", n)
}

Go’da diziler dilimlenerek, yeni bir dilim değişkeni oluşturulabilir. Bir dizinin sınırları belirlenerek yeni bir dilim oluşturmak için:

msg[low:high]

ifadesi kullanılır. Bu ifade ilk elemanı (low) içeren ancak sonuncusu (high) hariç tutan yarı açık bir dilim seçer. İfade biraz karışık, anlaşılmak için örnek üzerinden ilerleyelim.

package main

import "fmt"

func main() {
chars := [6]string{"a", "b", "c", "d", "e", "f"}

var slice []string = chars[1:4]

fmt.Printf("%#v", slice)
}

Bu örnek, dizinin 1–4 numaralı indisleri arasında kalan (1 nolu indeks dahil, 4 nolu indeks dahil değil) değerleri seçecektir. Dizilerin başlangıç indeksinin 0 (sıfır) olduğunu unutmayın.

Dizilerden referans yoluyla dilim oluşturma

Bu görsel, bir önceki yaptığımız işlemin görselleştirilmiş halidir. Diziler, dilimlere ayrılırken referans yoluyla kopyalanır. Yani, dizinin değeri değil, aslında referansı değer olarak aktarılır.

Devam etmek için aşağıdaki kodu çalıştırıp, çıktıyı gözlemleyin.

package main

import "fmt"

func main() {
chars := [6]string{"a", "b", "c", "d", "e", "f"}

var slice []string = chars[1:4]

chars[2] = "x"

fmt.Printf("%#v", slice)
}

Yeni boş bir dilim oluşturmak için make sözcüğünü kullanırız. Go, make sözcüğü için bir uzunluk belirtilmesini bekler.

package main

import "fmt"

func main() {
s := make([]int, 2)

fmt.Printf("%#v:", s)
}

Bu örnekte uzunluğu (len) 2 olan yeni bir dilim oluşturduk. Dilime henüz bir değer atamadığımız için Go başlangıç değeriyle doldurdu.

package main

import "fmt"

func main() {
s := make([]int, 2)

s[0] = 10
s[1] = 20

fmt.Printf("%#v:", s)
}

Dizilerde olduğu gibi, dilimlere de indisleri üzerinden erişebilir veya değer atayabilirsiniz.

Dilimler, uzunluk limiti bakımından kolaylık sağladığından bahsetmiştik. O halde başlangıçta belirlenen uzunluk miktarı yeterli olmadığı durumlarda ne yapacağız?

s[2] = 30

Yukarıdaki ifadeyi örneğimize eklediğimizde, Go dilim uzunluğunu aştığımız için bize kızacak. Peki ne yapabiliriz?

s = append(s, 30)

Dilim uzunluğu genişletmek, dilimin sonuna yeni elemanlar eklemek için append sözcüğünü kullanabiliriz. Go, bizim yerimize dilimi genişletecek ve yeni elemanı dilimin sonuna ekleyecektir.

s = append(s, 30, 40, 50)

Dilerseniz tek bir satırda çok sayıda veriyi dilime ekleyebilirsiniz.

Boş bir dilim oluşturulduğunda ilk değerlerin doldurulduğunu unutmayın. Ayrıca, append sadece yeni değerleri dilimin sonuna ekler. Yani herhangi bir indisteki değeri güncellemez.

İşte bir örnek:

package main

import "fmt"

func main() {
s := make([]int, 2)

s = append(s, 10)

fmt.Printf("%#v:", s)
}

Bir dilimin uzunluğunu öğrenmek için len sözcüğünü kullanabilirsiniz.

l := len(s)

Daha fazlası için Go turuna katılabilir ya da bu konuyu harika özetleyen “slices explained” makalesini okuyabilirsiniz.

Maps

yakında güncellenecek.

Koşullu İfadeler

Programlar, bir durumun doğru veya yanlış olma ihtimaline göre farklı davranışlar sergiler. Mevcut şartları sorgulamak ve programın akışını yönlendirmek için koşullu ifadelerden (kontrol yapılarından) faydalanırız.

“a" değişken değerinin 5'e eşit olup olmadığını kontrol eden basit bir kontrol yapısı

If/Else

Bir durumu “eğer” şartı/şartları ile kontrol etmemizi sağlar.

Örneğin, sayısal tipli bir değişkenin sahip olduğu değeri tek ya da çift olma durumuna göre kontrol etmek istiyoruz. Sayı eğer çift ise ekrana mesaj yazdıracağız.

package main

import "fmt"

func main() {
i := 4

if i%2 == 0 {
fmt.Println("çift")
}
}

// output: çift

Örnekte mod operatörü ile i değişkeni kontrol ediliyor. Eğer bölümden kalan sonuç sıfır (0) ise true değeri oluşturulacak ve if bloğu içindeki şart karşılanacaktır. Koşula uyduğu için if bloğu gövdesindeki kod parçası çalıştırılacak. Eğer sayı tek olsaydı koşul sağlanmadığı için ekrana hiçbir şey yazdırılmayacaktı. Değeri değiştirerek deneyin.

İster sevin, ister sevmeyin ama Go dilinde if sonrasında koşul gövdesi için süslü parantezler kullanılmalı.

Ya her iki durumda da ekrana bir şey yazdırmamız gerekseydi?

package main

import "fmt"

func main() {
i := 3

if i%2 == 0 {
fmt.Println("çift")
} else {
fmt.Println("tek")
}
}

// output: tek

Örnekte, koşul false sonucu üreteceği için if koşulu sağlanmayacak. Program if koşulunu sağlamadığı için else gövdesini çalıştırır. Eğer if koşulu sağlansaydı bu seferde else gövdesi çalışmayacaktı.

Bazen programı çok sayıda koşulla sınamak isteyebiliriz. Yani if ve else haricinde farklı kontrollere de ihtiyacımız olabilir. O halde else if tanışalım.

package main

import "fmt"

func main() {
i := 7

if i < 5 {
fmt.Println("sayı beşten küçük")
} else if i == 6 {
fmt.Println("sayı altıya eşit")
} else if i == 7 {
fmt.Println("sayı yediye eşit")
} else {
fmt.Println("sayı hiçbir şartı karşılamıyor")
}
}

// output: sayı yediye eşit

Örnekte if koşulu sağlanmadığı için bir sonraki sırada yer alan koşul ile sınanır. Bu koşul da doğru değilse, bir sonraki, bir sonraki... Eğer herhangi bir koşula uymazsa else gövdesi tetiklenir. Her şey sıralı yürütülür. Ancak örneğimizde koşulun sağlandığı bir blok var: i==7 durumu.

Koşullu ifadeler bir tane if ve else bloğu içerebilirken, çok sayıda else if bloğuna sahip olabilir. Dürüst olmak gerekirse her şey if ve else bloklarından oluşur. Aslında else if sadece bizlere görsel kolaylık sağlamak için vardır.

if i == 5 {
...
} else if i == 7 {
...
} else {
...
}

eşittir:

if i == 5 {
...
} else {
if i == 7 {
...
} else {
...
}
}

Okunabilirliği azalttığı için, iç içe (nested) kod blokları yazmamaya gayret edin. Sadece böyle çalıştığını bilin, gönül rahatlığıyla else if kontrol bloğunu kullanabilirsiniz.

Daha fazlası için kontrol yapıları belgelerine göz atabilirsiniz.

Switch

if/else kontrol bloğuna güçlü bir alternatif olan switch bloğu, benzer kontrollerin yanında daha güçlü bir arayüz sunar. Yine koşullar yukarıdan aşağı doğru sıralı olarak sınanır. Bir durum (sav, şart) başarılı olduğunda koşul gövdesi yürütülür.

package main

import "fmt"

func main() {
num := 2

switch num {
case 1:
fmt.Println("num: 1")
case 2:
fmt.Println("num: 2")
}
}

Yukarıdaki örnekte, switch deyimi şarta uygulanacak değeri tutar. Daha sonra case deyimleri ile sırayla koşullar sınanır. Kazanan durum gövdesi çalıştırılır.

Aklınıza else geliyor değil mi? Merak etmeyin, hiç bir şart karşılanmadığı durumda çalıştırabileceğiniz default bloğu mevcut. Aynı else gibi çalışır.

package main

import "fmt"

func main() {
num := 200

switch num {
case 1:
fmt.Println("num: 1")
case 2:
fmt.Println("num: 2")
default:
fmt.Println("num: diğer")
}
}

Eğer switch ifadelerinde, default durumu varsa ve tüm şartlar sorgulanmasına rağmen hiç bir şart sağlanmamışsa default gövdesi çalıştırılarak kontrol sonlandırılır. Eğer en az bir tane şart sağlanmışsa default gövdesi çalıştırılmaz.

package main

import (
"fmt"
"time"
)

func main() {
switch hour := time.Now().Hour(); {
case hour < 12:
fmt.Println("günaydın")
case hour < 17:
fmt.Println("tünaydın")
default:
fmt.Println("iyi akşamlar")
}
}

Bu örnekte ise switch kapsamına özel bir değişken tanımlandı. Tanımlı değişkene kapsam içinde erişebilir, gelişmiş kontroller oluşturabilirsiniz.

package main

import (
"fmt"
"time"
)

func main() {
switch time.Now().Hour() {
case 6, 7, 8, 9, 10, 11:
fmt.Println("günaydın")
case 12, 13, 14, 15, 16:
fmt.Println("tünaydın")
default:
fmt.Println("iyi akşamlar")
}
}

Bir başka kullanım örneği ile devam edelim. Kontrol durumlarında bir liste halinde şart belirtebilirsiniz. Eğer şart listesi içindeki herhangi bir değere eşitlik koşulu sağlanırsa, koşul gövdesi çalıştırılır.

Daha fazlası için switch belgelerine veya örneklere göz atabilirsiniz.

Döngüler

Döngü (loop), bir kod bloğunu tekrar tekrar yürütmek için kullanılır. Go, tüm döngü türlerini tek bir for anahtar sözcüğü ile yapabilir.

For Döngüsü

Başlangıcı, bitişi ve artırım/azalım miktarı belli olan döngü türüdür. Döngü, çalışacağı şartları öncesinden bilir.

package main

import "fmt"

func main() {
sum := 0
for i := 1; i < 10; i++ {
sum += i
}
fmt.Println(sum) // döngü kapsamında olmadığı için "i" değişkeni
// bu alan için tanımlı değildir
}

Yukarıdaki program belirli aralıktaki sayıların toplamını ekrana yazdırır. Döngü aşağıdaki durumlarda çalışır:

  1. i := 1 durumu ile başlatılır (init)
  2. i < 10 koşulu (condition) hesaplanır. Koşul sağlanıyorsa döngü gövdesi çalıştırılır, aksi halde döngü tamamlanır.
  3. i++ durumu çalıştırılır (post)
  4. 2. adıma geri dön

While Döngüsü

Önceki bölümde ifade edilen for döngüsü biraz değiştirilerek while döngüsü elde edilebilir.

package main

import "fmt"

func main() {
sum, i := 0, 1
for i < 10 {
sum += i
i++
}
fmt.Println(sum)
}

Aslında while döngüsü sadece şarttan oluşur. Şart sağlandığı sürece tekrar tekrar çalışır. Döngü aşağıdaki durumlarda çalışır:

  1. i < 10 koşulu (condition) hesaplanır. Koşul sağlanıyorsa döngü gövdesi çalıştırılır, aksi halde döngü tamamlanır.
  2. 1. adıma geri dön

For-each Döngüsü

Yinelenilir (tekrarlı) string, array, slice, map veya channel veri türünde değişkenlerin sahip olduğu verileri sırayla işlemek için ideal döngüdür. Mesela;

package main

import "fmt"

func main () {
list := []string{"cat", "dog", "rabbit"}
for key, value := range list {
fmt.Println(key, value)
}
}

Döngü, liste içindeki her elemanı baştan sona doğru sırayla gezer. Son eleman işlendikten sonra imha olur. Liste, range anahtarı ile döngüye tanıtılır. Döngü her elemanı key ve value haritasıyla işler.

Eğer key değerine ihtiyacınız yoksa null identifier (boş tanımlayıcı) ile yok sayabilirsiniz. Boş tanımlayıcı için _ (alt çizgi) karakteri kullanılır.

for _, value := range list {
fmt.Println(value)
}

Sadece key değerine ihtiyacınız varsa, value değeri atmanız yeterli.

for key := range list {
fmt.Println(key)
}

Sonsuz Döngü

Bazen bir döngünün programın çalıştığı süre boyunca çalışması istenebilir. Hiç bir şart altında çalışmayan döngüdür.

package main

import "fmt"

func main() {
for {
fmt.Println(".")
}
}

Bu program çalıştığı süre boyunca ekrana nokta yazacak.

Break/Continue

Neredeyse tüm programlama dillerinde olan break ve continue anahtar sözcükleri, Go tarafından desteklenir.

break: döngüyü kırıp çıkar

continue : mevcut çevrimi atlar ve döngüye devam eder.

package main

import "fmt"

func main() {
sum := 0
for i := 1; ; i++ {
// i değeri 2 ve 3'e tam bölünüyorsa atla
if i%2 == 0 && i%3 == 0 {
continue
}

// sum değeri 15'ten büyükse döngüyü kır
if sum > 15 {
break
}
sum += i
}
fmt.Println(sum)
}

Fonksiyonlar

Bilgisayar biliminde fonksiyon (işlev), matematik bilimindeki fonksiyonlara benzer. Tanımlanan f(x) fonksiyonu, ihtiyaç duyulduğunda tekrar tekrar kullanılabilir.

Fonksiyon prototipi

Yukarıdaki görselde bulunan siyah kutuyu fonksiyon olarak hayal edin. Fonksiyonlar; sıfır veya daha fazla girdi parametresi (inputs) alır ve sıfır veya daha fazla sayıda çıktı (outputs) oluşturur.

Go dilinde fonksiyonlar func anahtar sözcüğü ile tanımlanır. Her fonksiyon, benzersiz bir isime sahiptir. Daha önce, parametresiz ve sonuç döndürmeyen main isimli fonksiyon ile sıkça karşılaşmıştık.

func main() {
// ...
}

Fonksiyonları anlamak için minik bir program yazalım:

package main

import "fmt"

func main() {
n := []int{5,10,15,20}
t := 0

for _, v := range n {
t += v
}

fmt.Println(t)
}

Örnekte n değişkenine atanmış bir dizi sayısal veriyi toplayıp ekrana yazdıran kod bloğu yazdık. Toplama işlemini gerçekleştiren kod bloğunu, ana bloktan kopararak yeni bir fonksiyona dönüştürelim.

func toplam(n []int]) {
t := 0

for _, v := range n {
t += v
}

fmt.Println(t)
}

Yukarıdaki örnekte toplam isimli fonksiyon bir adet girdi istiyor (parametre) ve geriye herhangi bir sonuç döndürmüyor. Bu çıkarımı, fonksiyonun prototip sözleşmesinden anlayabiliriz. Hadi devam edelim.

func name(inputs) outputs {
// body
}

Yukarıdaki ifade, fonksiyonların en basit formülüdür, bir başka deyişle prototip. Oluşturduğumuz toplam fonksiyonunu bu formüle göre açıklamamız istenirse, fonksiyon isminden hemen sonra n []int tipinde bir adet parametre aldığını söyleyebilirsiniz. Yine formüle göre, çıktı hanesinde herhangi bir şey tanımlı olmadığı için geriye değer döndürmediğini söylemek isabetli çıkarım olur.

Fonksiyonu çağırmak için main fonksiyona geri dönerek, fonksiyon gövdesini güncelleyelim:

func main() {
toplam([]int{5,10,15,20})
}

Program temiz görünüyor ancak yeniden kullanılabilir (reusable) değil. Hesaplanan toplam değeri fonksiyon içinde ekrana yazdırmak yerine, geriye çıktı olarak döndürürsek daha iyi olmaz mı? Böylece çıktılanan değer farklı amaçlarda kullanılabilir. Biraz değişiklik yaparak sonuç döndüren fonksiyonları tanıyalım:

func toplam(n []int]) int {
t := 0

for _, v := range n {
t += v
}

return t
}

Değişikliği fark ettiniz mi? Go, fonksiyonlardan geriye sonuç döndürülecekse döndürülecek değerin tipini önceden bilmek ister. Fonksiyondan geriye int tipinde bir adet sonuç döndürüleceği garanti edildi. Gövdenin son satırında ise sonucu ekrana yazdırmak yerine return anahtar sözcüğü ile değer geri döndürüldü.

Şimdi main fonksiyonuna geri dönelim ve değişikliği uygulayalım:

func main() {
sonuc := toplam([]int{5,10,15,20})

fmt.Println(sonuc)
}

Artık toplam fonksiyonundan elde edilen yeni değer farklı amaçlarla kullanılabilir. Sonuç değerini ister ekrana yazdırın, isterseniz bir dosyaya kaydedin.

İşleri biraz daha karmaşıklaştıralım. Çok sayıda parametre alan ve çok sayıda sonuç döndüren program yazmaya ne dersiniz?

package main

import "fmt"

func main() {
o1, o2:= islem(5, 6)

fmt.Printf("o1: %v, o2: %v", o1, o2)
}
func islem(a int, b int) (int, float64) {
x := a * b
y := float64(a) * 0.25

return x, y
}

Daha önce tanımladığımız parametreli fonksiyonlara benzer olarak, kabul edilecek her parametrenin adı ve tipi fonksiyon başlığında bildiriliyor. Birden fazla sonuç döndürüleceği zaman, çıktılanacak verilerin tipleri parantez içinde tanımlanıyor.

Fonksiyondan geriye döndürülecek veri birden fazla ise, döndürülecek veri tipleri fonksiyon başlığında parantez içinde belirtilmesi zorunluluktur. Tek bir adet sonuç döndürüleceği zaman parantezlere gerek yoktur.

func islem (a, b int) (int, float64) {
// ...
}

Go’da, tipleri aynı olan ardışık parametreleri yukarıdaki örnekte olduğu gibi gruplayabilirsiniz.

Şu durumu hayal edin: geriye çok sayıda sonuç döndüren bir fonksiyonumuz var ancak uygulama içinde hepsini kullanmamıza gerek yok. Yani bazı sonuçlar bizim için gereksiz. Bu durumda kullanılmayacak fonksiyon çıktılarını boş tanımlayıcı ile yok saymamız gerekiyor:

package main

import "fmt"

func main() {
_, iki, _ := fn()

fmt.Println(iki)
}

func fn() (int, int, int) {
return 1, 2, 3
}

Unutmayın, Go’da bir değişken tanımlanmışsa mutlaka kullanılması gerekiyor. Bu nedenle fonksiyonlardan elde ettiğimiz ihtiyaç fazlası değerleri boş tanımlayıcı ile yok saymalıyız.

Son olarak, belirsiz (sıfır, bir ya da çok) sayıda parametre kabul eden variadic functions olarak bilinen fonksiyonları hızlıca inceleyelim. Çok sık kullandığımız fmt.Println fonksiyonu aslında variadic fonksiyondur. Go kaynak kodlarında fonksiyon şöyle tanımlanmış:

func Println(a ...interface{}) (n int, err error)

Bu fonksiyonda a isimli parametre,, interface{} türünde (herhangi bir türde) ve belirsiz sayıda girdiyi kabul ediyor. Parametre sayısındaki belirsizlik (3 nokta) ifadesi ile bildiriliyor.

Örnekte kullandığımız toplam fonksiyonunu değiştirerek belirsiz sayıda parametre almasını sağlayalım:

func toplam(n ...int) int {
t := 0

for _, v := range n {
t += v
}

return t
}

Daha önce oluşturduğumuz fonksiyona bağlı kalarak n []int olan parametre bildirimini a ...int olarak güncelledik. Hepsi bu kadar. Bu fonksiyonu çağırmak için:

func main() {
sonuc := toplam(1, 2, 10, 15)

fmt.Println(sonuc)
}

Hata Yönetimi

Go, hataların işlenmesini geliştiriciye bırakıyor. Diğer dillerin aksine, Go her hatanın tek tek ele alınmasını ister. Go’nun iki farklı hata işleme mekanizması vardır:

  • fonksiyonlar geriye türü error olan bir sonuç döndürür
  • panic deyimi ile çalışma zamanı (run-time) istisnası fırlatılır (önerilmez)

Go, geriye çok sayıda sonuç döndürme yeteneğine sahip olduğu için, programlarda oluşan hatalar error arayüzünden türetilmiş bir sonuçla bildirilir.

type error interface {
Error() string
}

Go hata işleme için try/catch mekanizmasına sahip değildir. Bunun yerine fonksiyonlardan döndürülen error sonucu kontrol edilir. Aslında bu özellik diğer dillerdeki çözümlere göre hem daha pratik hem de daha temizdir.

Örneğin; dosya sistemleri ile çalışırken os.Open işlevi bir dosyayı açamadığı durumlarda geriye türü error olan sonuç döndürür. Aşağıda bu işlevin prototipi yer almaktadır:

func Open(name string) (file *File, err error)

Bu işlevi kullandığımızda, işlem hata üretip üretmediğini kontrol etmemiz gerekir. Aşağıdaki örnekte bir hatayı nasıl kontrol edeceğiniz yer almaktadır.

f, err := os.Open("file.txt")if err != nil {
log.Fatal(err)
}
// "f" değişkeni ile bir şeyler yapın

Kendi örneğimizi yazarak konuyu anlamaya çalışalım. Bölme işlemi gerçekleştiren basit bir fonksiyon yazacağız. Eğer bölüm değeri sıfır ise hata oluşturacağız ve programda bu hatayı yakalayacağız.

package mainimport (
"errors"
"fmt"
"log"
)
func bol(sayi, bolen float64) (float64, error) {
if bolen == 0 {
return 0, errors.New("bölen değeri sıfır olamaz")
}
sonuc := sayi / bolen return sonuc, nil
}
func main() {
sonuc, err := bol(5, 0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("sonuc: %v", sonuc)
}

Örnekte, fonksiyonlardan hata döndürme ve hata işleme ile ilgili önerilen kullanım biçimi yer almaktadır. Özel hata oluşturmak için errors paketi içinde yer alan işlevlerden birini kullanmak yeterli.

err := errors.New("hata mesajı")

Fonksiyonlarda hataları sonuç olarak döndürme sırasında dikkat edeceğimiz noktalar:

  • hata durumunda error haricindeki diğer değer parametreleri, türe ait varsayılan değerlerle doldurulabilir
  • hata olmadığı durumlarda ise error dönüş değeri nil değerinde olmalı
  • çoklu sonuç dönen fonksiyonlarda genellikle error türündeki parametre en son sırada olur (best practice)

Daha fazlası için paket belgelerine, blog yazısına ve Effective Go açılamalarına göz atabilirsiniz.

Structs

Go dilinde sınıflar yoktur, zaten nesne yönelimli programlama (object-oriented programming) dili değildir. Sadece OOP yaklaşımının avantajlarını barındırır.

Sınıflar, structs (yapılar) ile taklit edilebilir. Yapı elemanları, property (özellik) olarak kabul edilebilir.

type Rect struct {
width float64
height float64
}

Yukarıdaki örnekte width ve height değişkenlerini barındıran Rect isimli bir yapı oluşturduk. Yapı elemanları şu ana kadar öğrendiğimiz tüm veri tiplerine sahip olabilir. Hatta bir başka yapıyı dahi tip olarak kabul edebilir.

package mainimport "fmt"type Rect struct {
width float64
height float64
}
func main() {
r := Rect{}
fmt.Printf("%+v\n", r)
}

Tanımladığımız Rect yapısından örnek (instance) aldık. Henüz yapı elemanlarına herhangi bir değer yüklemediğimiz için, değişkenler bölümünden de hatırlayacağınız üzere varsayılan değerler otomatik olarak atanır.

package mainimport "fmt"type Rect struct {
width float64
height float64
}
func main() {
r := Rect{}
r.width = 5.0
r.height = 4.0
fmt.Printf("%+v\n", r)
fmt.Printf("width: %.2f, height: %.2f", r.width, r.height)
}

Yapı elamanlarına erişmek için . (nokta) operatörü kullanılır. Bir elemana değer atamak ya da değerini almak için instance alınan değişken üzerinden nokta operatörü yardımıyla elemanlara erişebiliriz.

r := Rect{5.0, 4.0}

Yapı oluştururken, başlangıç değerlerini de tanımlayabiliriz. Yukarıdaki örnekte, yapı tanımındaki sıraya göre değişkenlere değer atanacaktır. Bu durumda width değeri 5.0 ve height değeri 4.0 olacaktır.

r := Rect{
width: 5.0,
height: 4.0,
}

Yapı ilklendirme için en çok tercih edilen yazım şeklidir. Genellikle bir yapıdan örnek alınırken, değişkenlerin değeri açıkça belirtilir.

Methods

Go’da yapılar için tanımlanan özel fonksiyonlardır. Bu fonksiyonlar, yapılara aittir ve sadece yapı örnekleri ile kullanılabilir.

Bir önceki bölümde oluşturduğumuz Rect yapısı için alan (area) değerini hesaplayıp geri döndüren method (işlev) yazalım.

type Rect struct {
width float64
height float64
}
func (r Rect) Area() float64 {
return r.width * r.height
}

Fonksiyonlar için geçerli olan tüm kurallar method tanımı içinde geçerlidir. Fonksiyonların aksine, fonksiyon adının hemen başına hangi yapı için kullanılacağı belirtilir. Bu durumda, Area fonksiyonunun Rect yapısına ait olduğu anlamına gelir.

r := Rect{
width: 5.0,
height: 4.0,
}
fmt.Println("area:", r.Area())

Bir işlevi çağırmak için nokta operatörünü kullanırız. Instance değişken üzerinden işlev çağrımı yapılır.

Örneğimizde, parametre almayan bir işlev tanımladık. Go’da, parametreli ve geriye çoklu değer dönen işlevler de tanımlanabilir.

Pointers

Pointers (işaretçiler), bir değerin bellekteki adresini tutar.

Bir değişkenin bellekteki adresi & (and) operatörü ile alınır. Bellek adresindeki değer ise * (yıldız) operatörü ile görüntülenir.

package mainimport "fmt"func main() {
a := 5
c := &a
fmt.Println(c)
}

Yukarıdaki örnekte c değişkenine a değişkeninin referansı atandı. Programı çalıştırdığınızda ekrana 0x40e020 değerine benzer bir çıktı basılır. Bu aslında a değişkeninin bellekteki adres değeridir.

Referans tutan değişken değerini ekrana yazdırmak için * operatörünü kullanalım:

fmt.Println(*c)

Pointer kullanırken dikkatli olmak gerekir. Normalde bir değişkenin değerini başka bir değişkene atadığınızda değer kopyalaması yapılır. Ancak referans yoluyla atama yapıldığında, değer yerine referans kopyalama yapılır. Böylece orijinal ya da referans değişkeni bir değişme uğrarsa her iki değişken değeride değişmiş olur.

package mainimport "fmt"func main() {
a := 5
b := a
c := &a
// a = 3
*c = 3
fmt.Println(a, b, *c)
}

Yukarıdaki programı çalıştırdığınızda a ve c değişkenlerinin değerinin değiştiğini, ancak b değişkeninin değerinin etkilenmediğini göreceksiniz.

Fonksiyon ve methodlar parametre olarak referans kabul edebilir. Fonksiyonlara bir parametre gönderildiğinde değişkenin bir kopyası oluşturulur. Ancak parametre referans ise yeni bir kopya oluşturmak yerine değişkenin bellekteki adresi gönderilir. Eğer fonksiyon içinde değişken herhangi bir değişime uğrarsa, fonksiyona aktarılan değişken ya da işlevin çağırıldığı yapı (değişken) da değişime uğrar.

package mainimport "fmt"type Rect struct {
width float64
height float64
}
func (r Rect) setWidth(w float64) {
r.width = w
}
func (r *Rect) setHeight(h float64) {
r.height = h
}
func main() {
r := Rect{
width: 5.0,
height: 4.0,
}
r.setWidth(2.0)
r.setHeight(3.0)
fmt.Printf("%+v", r)
}

Yukarıdaki örnekte setWidth değer olarak uyguladığı için, işleve uygulanan yapının bir kopyası oluşturulur. İşlev içinde width değeri değiştirilse de bu çağrıyı gerçekleştiren yapıda bir değişime neden olmaz.

Ancak setHeight referans olarak uygulandığı için, yapının yeni bir kopyası yerine refeansı aktarılır. İşlev gövdesinde height değeri değiştiğinde çağrıyı gerçekleştiren yapıda etkilenir.

Programı çalıştırıp sonucu gözlemleyebilirsiniz.

Interfaces

Interfaces yani arayüzler, OOP dünyasında çok yaygın kullanılır. Go, yapı ve işlevlerle birlikte arayüzleri uygular ve sahte bir OOP desteği sunar.

Nesne yönelimli programlama dillerin aksine, Go’da interface açıkça uygulanmaz. Arayüzün uygulanması için, arayüz içindeki tüm işlevlerin bir yapı üzerinde tanımlanmış olması gerekir. Tüm işlevler oluşturulduğunda, Go arayüz implementasyonunu kabul eder.

Konuyu anlamak için bir örnek yapalım. Şekiller için alan hesabı yapan bir programa ihtiyacınız olduğunu düşünün. Her şekil için alan hesabı formülü farklıdır. Daha önce diktörtgen için alan hesabı yapmıştık ancak bu daire için uyumlu değil. Arayüz uygulayarak hem dikdörtgen, hem daire hem de gelecekteki diğer şekiller için destek veren bir program gerçekleştirelim.

type Shaper interface {
Area() float64
}

Yukarıdaki Shaper arayüzümüz bir adet işlev içermektedir. Go dilinde anlamlı olsun ya da olmasın arayüz isimlerinin er ile bitmesi önerilmektedir. Arayüzler, sıfır, bir ya da çok sayıda işlev içerebilir.

Daha önce oluşturduğumuz Rect yapısı için bu arayüzü uygulayalım.

type Rect struct {
width float64
height float64
}
func (r Rect) Area() float64 {
return r.width * r.height
}

Oluşturduğumuz Rect yapısı artık Shaper arayüzünün koşullarını yerine getirdiği için arayüz başarıyla uygulanmıştır. Dilerseniz test edelim:

func main () {
var r Shaper = Rect{
width: 5.0,
height: 4.0,
}

fmt.Printf("type of r is %T\n", r)
fmt.Printf("value of r is %v\n", r)
fmt.Printf("value of r area is %.2f\n\n", r.Area())
}

Örnekte Shaper tipli yeni bir değişken oluşturmak istediğimizde derleyici bu isteğimize izin verdi. Eğer Rect yapısı, arayüz kurallarını yerine getirmemiş olsaydı derleyici isteğimizi reddecek ve hata fırlatacaktı.

Fonksiyonlar, arayüz tipini parametre tipi olarak kabul edebilir. Bu özellik sayesinde daha esnek (genişlemeye müsait) programlar yazabiliriz.

Yeni bir tane daire yapısı oluşturup arayüzü uygulayalım:

type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}

Ve son olarak hesaplamayı gerçekleştiren Calculator fonksiyonumuzu yazalım. Eğer arayüzler olmasaydı farklı iki türdeki (rect ve circle) şekil yapısını aynı fonksiyonda işlemek mümkün olmazdı.

func Calculator(s Shaper) float64 {
return s.Area()
}

Sizinde farkettiğiniz gibi, fonksiyon parametre olarak Shaper tipinde bir değişken kabul ediyor. Böylece Shaper arayüzünü uygulayan her değişken Calculator fonksiyonuna parametre olarak gönderilebilir.

package mainimport "fmt"type Shaper interface {
Area() float64
}
func Calculator(s Shaper) float64 {
return s.Area()
}
type Rect struct {
width float64
height float64
}
type Circle struct {
radius float64
}
func (r Rect) Area() float64 {
return r.width * r.height
}
func (c Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
func main() {
r := Rect{width: 5.0, height: 4.0}
c := Circle{radius: 3.0}
fmt.Printf("rect area is %.2f\n\n", Calculator(r))
fmt.Printf("circle area is %.2f\n\n", Calculator(c))
}

Go çekirdeğinde, çok sayıda ön tanımlı arayüz vardır, hatta arayüzler çok sık kullanılır. Zaman zaman geliştirdiğimiz programlarda çekirdek arayüzleri uygularız.

Daha fazla esneklik kazandırmak için fonksiyonlara ya da işlevlere parametre olarak yapı türü tanımlamak yerine, arayüz türü tanımlarız. Böylece yapı her ne olursa olsun, fonksiyon tanıdığı türde bir arayüz kabul eder.

Özetle:

  • Arayüz isimlendirirken er son ekiyle bitmesi önerilir.
  • Arayüzler sıfır, bir ya da çok sayıda işlev içerebilir. İşlevler, yapılar için tanımlanan fonksiyonlardır.
  • Arayüz işlevleri parametreli ya da parametresiz olabilir.
  • Arayüz işlevleri geriye sıfır, bir ya da çok sayıda değer döndürebilir.
  • Yapılar bir ya da çok sayıda arayüzü uygulayabilir. Büyük bir arayüz tanımlamak yerine, arayüzleri ortak amaca uygun olacak şekilde parçalamak daha doğrudur. (ReadWriter yerine iki ayrı Reader ve Writer arayüzleri)

Büyük uygulamalar sürekli gelişir ve değişime uğrar. Arayüzler doğru kullanıldığında, gelecekteki ihtiyaçlar daha az eforla kolayca karşılanabilir. Her zaman esnekliğe özen gösterin.

Dependency Injection konusunu araştırmanızı öneririm.

Packages

Yazının henüz başında, Go uygulamaları paketler aracılığıyla modüler olarak geliştirildiğinden bahsetmiştik.

Lego’yu hatırlayın. Paketler Lego’nun farklı birer parçasıdır. Parçaları bir araya getirerek hayalinizdeki harika evi hatta çok büyük kasabayı kolayca inşa edebilirsiniz. Parçalar birbirinden bağımsızdır ve yeniden kullanılabilirdir.

Go’da paketlerde öyle. Her bir birinden bağımsız yetenekler içerir ve bir kere yazdığınızda tekrar tekrar kullanabilirsiniz. Hatta açık kaynak olarak servis edebilirsiniz. Paketler farklı kaynaklardan (yerel veya uzak sunucudan) içe aktarılabilir:

  • “fmt”
  • “math/rand”
  • “github.com/guptarohit/asciigraph”

Kural olarak, paket adı içe aktarma yolunun son öğesiyle aynıdır. Ancak bu bazen farklılık gösterebilir. Özel durumlar paket kullanım kılavuzunda belirtilir. Yukarıdaki paketlerin içe aktarıldığında isim uzayları:

  • fmt
  • rand
  • asciigraph

olacaktır.

İhtiyacınız olan paketleri Github, Gitlab, Bitbucket gibi kod servis sitelerinden araştırabilirsiniz.

Paket İndirme

Go, paket yöneticisiyle birlikte gelir. İhtiyaç duyduğunuz paketleri go get komutu ile kolayca indirebilirsiniz. İndirdiğiniz paketi import deyimi ile programınızda içe aktarıp kullanabilirsiniz.

Terminali açın, çalışma dizinine girdikten sonra ve aşağıdaki komutu yazın:

go get github.com/guptarohit/asciigraph

İndirme işlemi tamamlandıktan sonra paket kullanıma hazır hale gelecektir. Paket açıklamalarında bulunan bir örneği kullarak paket kullanımını görelim:

package mainimport (
"fmt"
"github.com/guptarohit/asciigraph"
)
func main() {
data := []float64{3, 4, 9, 6, 2, 4, 5, 8, 5, 10, 2, 7, 2, 5, 6}
graph := asciigraph.Plot(data)
fmt.Println(graph)
}

Gayet basit. İhtiyaca uygun paketi go get ile geliştirme ortamına indir ve import ile projeye dahil ederek kullanmaya başla.

Bazen paket isimleri birbiriyle çakışabilir veya paket ismini takma bir ad ile kullanmak isteyebilirsiniz. Bu durumda paketi import ederken isminin başına takma ad eklemek yeterli olacaktır. Aşağıdaki örneği inceleyelim:

package mainimport (
"fmt"
rnd "math/rand"
)
func main() {
n := rnd.Int()
fmt.Println(n)
}

Paket adı rand olmasına rağmen rnd olarak kullanacağımızı bildirdik. Artık paket içindeki işlevleri çağırırken rand.Int() yerine rnd.Int() kullanmamız gerekiyor.

Son olarak paketleri içe aktarırken . (nokta) operatörünü kullanalım. Paket adının hemen önüne nokta operatörü eklendiğinde, paketin dışa açık olan tanımlayıcıları sanki geçerli pakete aitmiş gibi kullanılabilir.

package mainimport (
"fmt"
. "math/rand"
)
func main() {
n := Int()
fmt.Println(n)
}

Artık rand paketi içindeki tanımlayıcılar herhangi bir paket adı olmadan çağırılabilir. Sadece Int() olarak kullanılabilir.

Nokta operatörü, tanımlayıcının hangi pakette olduğunu gizlediği için çok tercih edilmez.

Paket Oluşturma

Go’da paket oluşturmak çok kolay. Hemen basit bir örnek yapalım. Çalışma alanında funding isminde bir klasör oluşturun ve içine fund.go dosyası oluşturun. Aşağıdaki örnek kodları yazarak kaydedin.

package fundingtype Fund struct {
balance int
}
func NewFund(initial int) *Fund {
return &Fund{
balance: initial,
}
}
func (f *Fund) Balance() int {
return f.balance
}
func (f *Fund) Withdraw(amount int) {
f.balance -= amount
}

Hepsi bu kadar. Go, main paketinin dışında olduğu için derlenme zamanında bu kodları yerel paket olarak anlayacak. Kullanmak istediğiniz yerde içe aktarıp, dışa aktarılan tüm tanımlayıcılara erişebilirsiniz. İçe aktarmak için:

import "PROJE-ADI/funding"

Yukarıdaki örnekte PROJE-ADI yazan yere, proje oluştururken isimlendirdiğiniz kendi projenizin adını yazmalısınız.

Dilerseniz bu basit paketi kod servislerine (Github) yükleyerek sürümleyebilir (git, svn) ve ihtiyacınız olduğunda go get ile indirebilirsiniz.

Daha fazlası için Go Modules ve How to Write Go Code dokümanlarını mutlaka inceleyin.

Testing

Geliştirmesi tamamlanan uygulama sıkı testlerden geçti ve her şey yolunda olduğu için dağıtıma hazır hale geldi. Dağıtımdan sonra yeni bir özellik eklemek istedik. Geliştirmeyi tamamladık ve yeniden test ettik. Bu döngü her geliştirme sonrasında tekrarlanmalı.

Sürekli her şeyi insan gücüyle kontrol etmek çok zor olmaz mı? Her geliştirme sonrasında geliştirmenin bir yeri etkileyip etkilemediğini kontrol etmek için uygulama en baştan (bütünüyle) test edilmeli. Gelişen uygulamada test edilecek özelliklerin sayısı sürekli artacak.

Yazılım madem sorun çözmek için var, neden bunu otomatik hale getirmiyoruz? Go zaten gömülü bir test paketi sunuyor.

Bir önceki bölümde oluşturduğumuz paket için test senaryolarını yazarak hızlıca test paketine göz atalım.

Go’da test edilecek dosya funding.go ise test dosyasının funding_test.go olarak isimlendirilmesi önerilir. Test işlemlerini yürüten araç *_test.go dosyalarını keşfeder ve bu dosya içindeki test metodlarını çalıştırır.

package fundingimport "testing"func TestNewFund(t *testing.T) {
f := NewFund(5)
if f.Balance() != 5 {
t.Error("Balance değeri hatalı")
}
}

Yukarıdaki örnekte NewFund fonksiyonu test edildi. Test edilecek öğenin isminin başına Test öneki eklemek yeterli. Bu yüzden, NewFund fonksiyonu için TestNewFund isminde yeni bir test fonksiyonu tanımladık. Başlangıçta yapıya atanan değerin doğruluğu test ediyor.

Testi çalıştırmak için go test aracı kullanılır. Bir dosyayı ya da uygulama/paket içindeki tüm testleri çalıştırabilirsiniz. Çalışma dizininde aşağıdaki komutu çalıştırdığınızda tüm testler çalıştırılır.

go test ./...

Eğer bir tek dosya için testi çalıştırmak isterseniz ./... yerine çalıştırmak istediğiniz Go doyasını yazmanız yeterli.

go test ./funding

Testleri bir kaç senaryoda test etmek isteyebilirsiniz:

package fundingimport "testing"func TestNewFund(t *testing.T) {
testPairs := []struct {
set int
get int
}{
{0, 0},
{5, 5},
}
for _, p := range testPairs {
f := NewFund(p.set)
if f.Balance() != p.get {
t.Errorf("Sonuç: %d, Beklenen: %d", f.Balance(), p.get)
}
}
}

Yukarıdaki örnekte testPairs adında test senaryolarını tutan yeni bir yapı oluşturduk. Başlangıçta yüklediğimiz değeri set ve geri almak istediğimiz değeri get adıyla sakladık. Döngü ile tümünü uygulayıp sonucun aynı olup olmadığını test ettik.

Paket içindeki tüm fonksiyonlar için test senaryoları hazırlayarak her birimi test edebiliriz.

package fundingimport "testing"testPairs := []struct {
i int // ilk değeri
w int // çekim miktarı
b int // kalan
}{
{0, 0, 0},
{0, 5, -5},
{10, 10, 0},
{10, 0, 10},
{10, 5, 5},
}
func TestFund(b *testing.B) {
for _, p := range testPairs {
f := NewFund(p.i)
f.Withdraw(p.w) if fund.Balance() != 0 {
t.Errorf("Sonuç: %d, Beklenen: %d", f.Balance(), p.b)
}
}
}

Yukarıda senaryoyu tek seferde test eden genel bir test yazdık. Böylece tüm akışı tek bir test fonksiyonunda test etmiş olduk. Genelde testler birime özel yazılsa da bazen senaryo bütünüyle test edilebilir.

Daha fazlası için paket paketi belgelerini ziyaret edebilirsiniz.

Kapanış

Gelişmeleri yakından takip etmek ve Go hakkında daha fazlasını öğrenmek için aşağıdaki bağlantıları takip edebilirsiniz:

Sevgiyle kalın.

Yorum bölümünden yazıyla ilgili eleştirilerinizi paylaşabilirsiniz. Eksik veya hatalı olduğunu düşündüğünüz yerleri belirtmekten çekinmeyin.

--

--