Bir program, yukarıdan aşağıya akan bir talimat listesi olsaydı yapabildiği şeyler çok sınırlı olurdu. Programları yararlı kılan şey karar verebilmeleri ve tekrar edebilmeleridir: Bakiye yeterliyse ödemeyi yap, listedeki her sipariş için kargo etiketi üret, kullanıcı çıkış diyene kadar bekle. Bu derste Go'nun bu kararları ve tekrarları nasıl ifade ettiğini göreceksin.
Go bu konuda yine sadeliği seçmiştir: while, do-while, foreach gibi ayrı anahtar kelimeler yoktur. Tek bir döngü anahtar kelimesi vardır: for. Diğer dillerdeki tüm döngü biçimleri for'un farklı yazımlarıyla elde edilir. switch ise Go'da C ailesinden çok daha güçlü ve çok daha az tehlikelidir: Otomatik geçiş (fallthrough) yoktur, koşulsuz kullanılabilir ve bir if/else if zincirinin yerini şık biçimde alır.
Dersin sonunda operatörlerin önceliğini bilecek, if ve switch'in kısa ifade biçimlerini kullanabilecek, for'un beş farklı yazımını tanıyacak, range ile dilim, harita, string ve tamsayı üzerinde gezebilecek ve iç içe döngülerden etiketlerle (label) çıkabileceksin.
Operatörler
Go'nun operatörleri tanıdıktır; sürprizler ayrıntılarda saklıdır.
Dört önemli ayrıntı:
Tamsayı bölmesi kalanı atar. 7 / 2 sonucu 3'tür, 3.5 değil. Negatif sayılarda Go sıfıra doğru keser: -7 / 2 sonucu -3, -7 % 2 sonucu -1'dir.
&& ve || kısa devre yapar. Soldaki işlenen sonucu belirliyorsa sağdaki hiç değerlendirilmez. Bu, if m != nil && m["a"] > 0 gibi güvenli kontroller yazmanı sağlar.
++ ve -- birer deyimdir. Kendi başlarına satır oluştururlar; başka bir ifadenin içine gömülemezler. Ayrıca ön ek biçimleri (++i) yoktur.
&^ (AND NOT) bit temizler. p &^ q ifadesi, q'da 1 olan bitleri p'den siler. İzin ve bayrak kümelerinde çok kullanışlıdır.
package main
import "fmt"
func main() {
a, b := 17, 5
fmt.Println(a+b, a-b, a*b, a/b, a%b)
fmt.Println(-7/2, -7%2)
x, y := 0b1100, 0b1010 // ikilik sabitler
fmt.Printf("&=%04b |=%04b ^=%04b &^=%04b\n", x&y, x|y, x^y, x&^y)
fmt.Printf("sola kaydır: %d, sağa kaydır: %d\n", 3<<4, 256>>3)
// Kısa devre: bölen sıfırsa sağdaki ifade hiç çalışmaz
divisor := 0
if divisor != 0 && 100/divisor > 10 {
fmt.Println("büyük")
} else {
fmt.Println("bölme hiç yapılmadı")
}
}22 12 85 3 2 -3 -1 &=1000 |=1110 ^=0110 &^=0100 sola kaydır: 48, sağa kaydır: 32 bölme hiç yapılmadı
Bit operatörlerini ve kullanım alanlarını daha derinlemesine Bit Manipülasyonu dersinde göreceksin. Şimdilik bilmen gereken, bu operatörlerin sayıların ikilik gösterimi üzerinde bit bit çalıştığıdır: & iki bitin de 1 olduğu yerde 1 üretir, | en az birinin 1 olduğu yerde, ^ ise tam olarak birinin 1 olduğu yerde. Sola kaydırma ikiyle çarpmaya, sağa kaydırma ikiye bölmeye karşılık gelir ve her ikisi de çarpma/bölme işleminden çok daha ucuzdur.
Operatör önceliği
Go'nun operatör önceliği C'dekinden daha sadedir; yalnızca beş seviye vardır (en yüksekten en düşüğe): tekli operatörler; * / % << >> & &^; + - | ^; karşılaştırmalar; &&; ||. Dikkat çekici olan, bit operatörlerinin çarpma ve toplama seviyelerine yerleştirilmiş olmasıdır. Bu, a & b == c gibi bir ifadenin a & (b == c) değil (a & b) == c diye okunmasına yol açar — ki bu da C'deki en meşhur tuzaklardan birini ortadan kaldırır.
Yine de kural basittir: Karışabilecek her yerde parantez kullan. gofmt gereksiz parantezleri silmez, okunabilirliği artıranı korur. Kodun bir kez yazılıp yüzlerce kez okunacağını unutma.
if ve kısa ifade
if koşulu parantez içine alınmaz, ama gövde her zaman süslü parantezle yazılır. Tek satırlık gövdelerde bile parantezi atlayamazsın:
if x > 0 { // doğru
fmt.Println("pozitif")
}
if x > 0 fmt.Println("pozitif") // HATA
if (x > 0) { ... } // derlenir ama gofmt parantezleri kaldırırGo'nun en sevilen özelliklerinden biri if'in kısa ifade (short statement) biçimidir: Koşuldan önce noktalı virgülle ayrılmış bir deyim çalıştırabilirsin. Burada tanımlanan değişkenler yalnızca if/else if/else bloklarında görünür:
package main
import (
"fmt"
"strconv"
)
func classify(input string) string {
if n, err := strconv.Atoi(input); err != nil {
return input + ": sayı değil"
} else if n < 0 {
return input + ": negatif"
} else if n == 0 {
return input + ": sıfır"
}
return input + ": pozitif"
}
func main() {
for _, s := range []string{"42", "-3", "0", "elma"} {
fmt.Println(classify(s))
}
}42: pozitif -3: negatif 0: sıfır elma: sayı değil
Bu desen, değişkenlerin kapsamını olabildiğince dar tutar: n ve err yalnızca ihtiyaç duyuldukları yerde vardır, fonksiyonun geri kalanını kirletmezler.
switch: Go'nun güçlü kalıp eşleyicisi
Çok dallı kararlarda uzun bir if/else if zinciri yazmak yerine switch kullanmak hem daha okunaklıdır hem de derleyicinin daha iyi kod üretmesine olanak tanır. Go'da switch, C'dekinden üç önemli noktada ayrılır:
- Otomatik geçiş yoktur. Eşleşen
caseçalıştıktan sonraswitchbiter;breakyazmana gerek yoktur. caseifadeleri sabit olmak zorunda değildir. Herhangi bir ifade, hatta karşılaştırma yazabilirsin.- Koşulsuz
switchyazılabilir.switchsonrası hiçbir şey yazmazsanswitch truevarsayılır veif/else ifzincirinin yerini alır.
package main
import "fmt"
func grade(score int) string {
switch { // koşulsuz switch
case score >= 90:
return "AA"
case score >= 80:
return "BA"
case score >= 70:
return "BB"
case score >= 60:
return "CC"
default:
return "FF"
}
}
func dayType(day string) string {
switch day {
case "cumartesi", "pazar": // tek case'te birden çok değer
return "hafta sonu"
case "cuma":
return "hafta sonuna bir gün"
default:
return "hafta içi"
}
}
func main() {
for _, s := range []int{95, 83, 71, 64, 30} {
fmt.Print(grade(s), " ")
}
fmt.Println()
for _, d := range []string{"pazartesi", "cuma", "pazar"} {
fmt.Printf("%s → %s\n", d, dayType(d))
}
// Kısa ifadeli switch
switch n := len("merhaba"); {
case n > 10:
fmt.Println("uzun")
case n > 5:
fmt.Println("orta:", n)
default:
fmt.Println("kısa")
}
}AA BA BB CC FF pazartesi → hafta içi cuma → hafta sonuna bir gün pazar → hafta sonu orta: 7
fallthrough
Bir sonraki case'in gövdesini de çalıştırmak istediğin nadir durumlarda fallthrough kullanılır. Koşulu kontrol etmeden doğrudan bir sonraki gövdeye geçer, bu yüzden dikkatli kullanılmalıdır:
package main
import "fmt"
func main() {
level := 2
switch level {
case 3:
fmt.Println("yönetici yetkileri")
fallthrough
case 2:
fmt.Println("düzenleme yetkileri")
fallthrough
case 1:
fmt.Println("okuma yetkileri")
default:
fmt.Println("yetki yok")
}
}düzenleme yetkileri okuma yetkileri
fallthrough yalnızca case gövdesinin son deyimi olabilir ve default'tan sonra kullanılamaz. Pratikte yukarıdaki gibi "kümülatif yetki" senaryoları dışında nadiren gerekir.
Bir de switch'in tip üzerinde çalışan biçimi vardır (switch v := x.(type)); bunu arayüzleri öğrendikten sonra Arayüzler dersinde göreceksin.
for: tek döngü, beş yazım
Go'da yalnızca for vardır. Diğer dillerdeki tüm döngü biçimleri onun yazımlarıdır:
package main
import "fmt"
func main() {
// 1) Klasik: başlangıç; koşul; adım
for i := 0; i < 3; i++ {
fmt.Print(i, " ")
}
fmt.Println()
// 2) while benzeri: yalnızca koşul
n := 1
for n < 20 {
n *= 3
}
fmt.Println("n =", n)
// 3) Sonsuz döngü + break
count := 0
for {
count++
if count == 4 {
break
}
}
fmt.Println("count =", count)
// 4) range ile tamsayı (Go 1.22+): 0'dan n-1'e
total := 0
for i := range 5 {
total += i
}
fmt.Println("0..4 toplamı:", total)
// 5) range ile koleksiyon
for i, ch := range "ağ" {
fmt.Printf("[%d]=%c ", i, ch)
}
fmt.Println()
}0 1 2 n = 27 count = 4 0..4 toplamı: 10 [0]=a [1]=ğ
range'in dört yüzü
range'in döndürdüğü değerler, gezdiğin şeye göre değişir:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
prices := map[string]int{"elma": 30, "armut": 45, "kiraz": 120}
// Harita gezinme sırası KASITLI olarak rastgeledir.
// Deterministik çıktı için anahtarları sırala:
for _, key := range slices.Sorted(maps.Keys(prices)) {
fmt.Printf("%s=%d ", key, prices[key])
}
fmt.Println()
nums := []int{10, 20, 30}
// İkinci değer bir KOPYADIR; onu değiştirmek diziyi değiştirmez
for _, v := range nums {
v *= 2
}
fmt.Println("kopya değiştirildi:", nums)
// Gerçekten değiştirmek için indeksi kullan
for i := range nums {
nums[i] *= 2
}
fmt.Println("indeksle değiştirildi:", nums)
}armut=45 elma=30 kiraz=120 kopya değiştirildi: [10 20 30] indeksle değiştirildi: [20 40 60]
Haritaların gezinme sırasının rastgele olması bir kusur değil, bilinçli bir tasarımdır: Programcıların sıraya bağımlı kod yazmasını baştan engeller. Sıralı çıktı gerekiyorsa anahtarları toplayıp sıralaman gerekir.
Bir dilimi range ile gezerken ikinci değerin kopya olması, özellikle büyük struct'lardan oluşan dilimlerde performans açısından da önemlidir: Her yinelemede tüm struct kopyalanır. Elemanları yalnızca okuyacaksan ve struct büyükse, indeksle erişmek (items[i].Name) gereksiz kopyalamayı önler. Küçük tiplerde ise bu farkın pratikte bir önemi yoktur; önce okunabilirliği gözet.
range ifadesinin sağındaki değer döngü başlamadan bir kez değerlendirilir. Yani döngü içinde dilime eleman eklemek, o döngünün gezeceği eleman sayısını değiştirmez. Bu davranış, listeyi gezerken ona ekleme yapan kodların sonsuz döngüye girmesini engeller; ama unutulduğunda "neden yeni elemanlar işlenmedi?" sorusuna yol açar.
break, continue ve etiketler
break en içteki for veya switch'i sonlandırır; continue bir sonraki yinelemeye geçer. İç içe döngülerde dıştaki döngüden çıkmak istediğinde etiket (label) kullanılır:
package main
import "fmt"
func main() {
grid := [][]int{
{3, 7, 1},
{9, 4, 8},
{2, 5, 6},
}
target := 8
outer:
for i, row := range grid {
for j, v := range row {
if v == target {
fmt.Printf("%d bulundu: satır %d, sütun %d\n", target, i, j)
break outer // iki döngüyü birden kırar
}
}
}
// continue + etiket: dıştaki döngünün bir sonraki adımına atlar
next:
for _, row := range grid {
for _, v := range row {
if v%2 == 0 {
continue next // bu satırda çift sayı varsa satırı atla
}
}
fmt.Println("tamamı tek olan satır:", row)
}
// switch içindeki break yalnızca switch'i kırar
for i := range 5 {
switch {
case i == 3:
break // döngüyü DEĞİL, switch'i kırar
default:
fmt.Print(i, " ")
}
}
fmt.Println()
}8 bulundu: satır 1, sütun 2 tamamı tek olan satır: [3 7 1] 0 1 2 4
Etiketler, Go'da gerçekten yararlı olduğu için korunmuş nadir "eski" özelliklerden biridir. Bir matris içinde arama yapan, iki listeyi karşılaştıran veya bir koşul sağlandığında tüm iç içe yapıdan çıkması gereken algoritmalarda kodu hem kısaltır hem de bayrak (flag) değişkenleriyle uğraşmaktan kurtarır. Alternatifi, döngüyü ayrı bir fonksiyona taşıyıp return kullanmaktır; çoğu zaman bu daha da temiz bir çözümdür.
Son örnek önemli bir tuzağı gösterir: switch içindeki break döngüyü değil switch'i sonlandırır. Döngüyü kırmak istiyorsan etiket kullanmalısın.
Go'da goto da vardır ama yalnızca aynı fonksiyon içinde ve bir değişken tanımının üzerinden atlamayacak şekilde çalışır. Gerçek kodda neredeyse hiç kullanılmaz; hata yolunda ortak temizlik yapan düşük seviye kütüphaneler dışında görürsen şüphelen.
Döngülerde performans ve okunabilirlik
Kontrol akışı yazarken sık sorulan bir soru şudur: Döngüyü "elle" mi yazmalı, yoksa standart kütüphanenin hazır fonksiyonlarını mı kullanmalı? Go'nun cevabı genelde açıktır: Basit ve okunur olanı seç. slices paketindeki slices.Contains, slices.Index, slices.Max gibi fonksiyonlar, elle yazılmış bir döngüyle aynı işi yapar ama niyeti çok daha net anlatır. Buna karşılık birden çok işi tek geçişte yapman gerekiyorsa (aynı anda hem toplam hem en büyük değeri hesaplamak gibi) elle yazılmış tek bir döngü hem daha hızlıdır hem de daha anlaşılırdır.
İkinci bir ipucu: Döngü gövdesi büyüdükçe okunabilirlik hızla düşer. Gövde ekrana sığmıyorsa, içindeki mantığı ayrı bir fonksiyona taşımayı düşün. Böylece continue ve break yerine return kullanabilir, iç içe girintileri azaltabilirsin. Go'da fonksiyon çağrısı ucuzdur; derleyici küçük fonksiyonları çoğu zaman satır içine alır (inline).
Son olarak, sonsuz döngülerden korkma. for { ... } yazımı, çıkış koşulu gövdenin ortasında olan durumlarda (bir kanaldan veri beklemek, kullanıcı girdisi okumak, yeniden deneme yapmak) en dürüst ifadedir. Koşulu zorla döngü başlığına sıkıştırmaya çalışmak genelde daha karmaşık kod üretir.
Sık yapılan hatalar
- Süslü parantezi unutmak. Go'da tek satırlık gövdelerde bile
{ }zorunludur. case'lerin arasınabreakyazmak. Gereksizdir; Go otomatik geçiş yapmaz.switchiçindekibreakile döngüden çıkmaya çalışmak. Etiket kullan.range'in ikinci değerini değiştirerek diziyi güncellemeye çalışmak. O bir kopyadır;nums[i]ile yaz.- Harita gezinme sırasına güvenmek. Sıra her çalıştırmada değişir; test bugünü geçse yarın kalabilir.
x := i++yazmak.++bir deyimdir, değer üretmez.- Tamsayı bölmesinden ondalık sonuç beklemek.
1/2sıfırdır;1.0/2yarımdır. - Döngü içinde
len()sonucunu her seferinde yeniden hesaplamaktan korkmak. Go derleyicisi bunu genelde iyileştirir; okunabilirlikten ödün verme.
Alıştırmalar
1'den 20'ye kadar olan sayıları yazdır; ancak 3'e bölünenler yerine Fizz, 5'e bölünenler yerine Buzz, her ikisine de bölünenler yerine FizzBuzz yaz. Çıktı tek satırda, aralarında boşlukla olsun.
İpucu
Önce 15'e bölünme durumunu kontrol et; yoksa 3 ve 5 koşulları onu gölgeler. Alternatif olarak bir switch bloğu kullanabilirsin.
Çözümü göster
package main
import "fmt"
func main() {
for i := 1; i <= 20; i++ {
switch {
case i%15 == 0:
fmt.Print("FizzBuzz ")
case i%3 == 0:
fmt.Print("Fizz ")
case i%5 == 0:
fmt.Print("Buzz ")
default:
fmt.Print(i, " ")
}
}
fmt.Println()
}1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz
Koşulsuz switch, if/else if zincirinden daha okunaklıdır ve case'ler yukarıdan aşağıya sırayla denenir — bu yüzden en özel koşul (%15) en üste yazılır.
Bir tamsayı dilimindeki en büyük değeri, en küçük değeri, toplamı ve ortalamayı tek bir geçişte hesaplayan bir program yaz. Ortalamayı iki ondalık basamakla göster. Boş dilim durumunu da ele al.
İpucu
En büyük ve en küçüğü ilk elemanla başlat; min ve max yerleşik fonksiyonları Go 1.21'den beri mevcuttur.
Çözümü göster
package main
import "fmt"
func stats(nums []int) (lo, hi, sum int, avg float64, ok bool) {
if len(nums) == 0 {
return 0, 0, 0, 0, false
}
lo, hi = nums[0], nums[0]
for _, n := range nums {
lo = min(lo, n)
hi = max(hi, n)
sum += n
}
return lo, hi, sum, float64(sum) / float64(len(nums)), true
}
func main() {
data := []int{14, -3, 27, 8, 0, 27, 5}
lo, hi, sum, avg, ok := stats(data)
if !ok {
fmt.Println("veri yok")
return
}
fmt.Printf("en küçük: %d\nen büyük: %d\ntoplam: %d\nortalama: %.2f\n", lo, hi, sum, avg)
if _, _, _, _, ok := stats(nil); !ok {
fmt.Println("boş dilim: hesaplanamadı")
}
}en küçük: -3 en büyük: 27 toplam: 78 ortalama: 11.14 boş dilim: hesaplanamadı
min ve max yerleşikleri (Go 1.21) her sıralanabilir tiple çalışır ve ayrı bir if bloğuna gerek bırakmaz. Tek geçiş olduğu için karmaşıklık O(n)'dir.
Bir sayıyı asal çarpanlarına ayıran ve 360 = 2^3 × 3^2 × 5 biçiminde yazdıran bir program yaz. İç içe döngü, continue ve etiket kullanımını gerektiren bir problemdir. 360, 97 ve 1024 için çalıştır.
İpucu
2'den başlayarak d*d <= n olduğu sürece böl; her bölen için üssü say. Döngü bittiğinde n > 1 kaldıysa o da bir asal çarpandır.
Çözümü göster
package main
import (
"fmt"
"strings"
)
func factorize(n int) string {
if n < 2 {
return fmt.Sprint(n)
}
var parts []string
original := n
for d := 2; d*d <= n; d++ {
power := 0
for n%d == 0 {
n /= d
power++
}
if power == 0 {
continue
}
if power == 1 {
parts = append(parts, fmt.Sprint(d))
} else {
parts = append(parts, fmt.Sprintf("%d^%d", d, power))
}
}
if n > 1 { // kalan sayının kendisi asaldır
parts = append(parts, fmt.Sprint(n))
}
return fmt.Sprintf("%d = %s", original, strings.Join(parts, " × "))
}
func main() {
for _, n := range []int{360, 97, 1024, 2310} {
fmt.Println(factorize(n))
}
}360 = 2^3 × 3^2 × 5 97 = 97 1024 = 2^10 2310 = 2 × 3 × 5 × 7 × 11
Dış döngü d*d <= n koşuluyla döner: Bir sayının √n'den büyük en fazla bir asal çarpanı olabilir, o da döngüden sonra n > 1 kontrolüyle yakalanır. Bu sayede karmaşıklık O(√n) olur. continue, bölen bulunamadığında gereksiz if girintisinden kurtarır.
Kısa sınav
Go'da bir case gövdesi çalıştıktan sonra ne olur?
for _, v := range nums { v *= 2 } döngüsü nums dilimini değiştirir mi?
Bir haritayı range ile gezerken elemanların sırası nasıldır?
İç içe iki for döngüsünde dıştaki döngüden çıkmak için ne kullanılır?
fmt.Println(-7/2, -7%2) ne yazdırır?
Aşağıdaki if biçiminde tanımlanan n nerede görünür: if n := f(); n > 0 { } else { }
Özet
- Tamsayı bölmesi kalanı atar ve sıfıra doğru keser;
%yalnızca tamsayılarda çalışır. &&ve||kısa devre yapar;++ve--birer deyimdir, ifade değildir.ifkoşulu parantezsiz, gövdesi her zaman süslü parantezlidir; kısa ifade biçimi değişken kapsamını daraltır.switchotomatik geçiş yapmaz,case'lerde ifade kullanılabilir ve koşulsuz biçimiif/else ifzincirinin yerini alır.- Go'da tek döngü anahtar kelimesi
for'dur: klasik, while benzeri, sonsuz,range nverange koleksiyonbiçimleriyle. rangedilimde indeks+kopya, string'te bayt indeksi+rune, haritada anahtar+değer üretir; harita sırası rastgeledir.breakvecontinueen içteki döngüye etki eder; dıştakine ulaşmak için etiket kullanılır.switchiçindekibreakdöngüyü değilswitch'i sonlandırır.