go
Go dersleri
Go/Temeller

Operatörler ve Kontrol Akışı

Operatörler, if, switch ve for döngüsünün tüm biçimleri.

Ders 3 / 2120 dkBaşlangıç
Bu derste öğreneceklerin
  • Aritmetik, karşılaştırma, mantıksal ve bit operatörleri
  • if ve kısa ifade (if x := ...; koşul)
  • switch, koşulsuz switch ve fallthrough
  • for: klasik, while benzeri ve sonsuz döngü
  • range ile tamsayı, dilim, harita ve string gezinme
  • break, continue ve etiketler (labels)

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.

GrupOperatörlerNot
Aritmetik+ - * / %% yalnızca tamsayılarda çalışır
Karşılaştırma== != < <= > >=Sonuç her zaman bool
Mantıksal&& || !Kısa devre yapar
Bit& | ^ &^ << >>^ tekli kullanımda tümleyen (NOT)
Atama= += -= *= /= %= &= |= ^= <<= >>=
Artırma/azaltma++ --Deyimdir, ifade değil: x := i++ yazılamaz

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.

main.go
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ı")
	}
}
Çıktı
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ır

Go'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:

main.go
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))
	}
}
Çıktı
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:

  1. Otomatik geçiş yoktur. Eşleşen case çalıştıktan sonra switch biter; break yazmana gerek yoktur.
  2. case ifadeleri sabit olmak zorunda değildir. Herhangi bir ifade, hatta karşılaştırma yazabilirsin.
  3. Koşulsuz switch yazılabilir. switch sonrası hiçbir şey yazmazsan switch true varsayılır ve if/else if zincirinin yerini alır.
main.go
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")
	}
}
Çıktı
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:

main.go
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")
	}
}
Çıktı
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:

main.go
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()
}
Çıktı
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:

GezilenBirinci değerİkinci değer
Dilim, diziİndeksElemanın kopyası
StringBayt indeksirune (Unicode kod noktası)
Harita (map)AnahtarDeğer
KanalKanaldan gelen değer
Tamsayı n0…n−1
Fonksiyon (iteratör)Iteratörün ürettikleri
main.go
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)
}
Çıktı
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:

main.go
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()
}
Çıktı
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ına break yazmak. Gereksizdir; Go otomatik geçiş yapmaz.
  • switch içindeki break ile 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/2 sıfırdır; 1.0/2 yarı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

Alıştırma·FizzBuzz
Kolay

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
main.go
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()
}
Çıktı
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.

Alıştırma·Sayı istatistikleri
Orta

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
main.go
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ı")
	}
}
Çıktı
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.

Alıştırma·Asal çarpanlara ayırma
Zor

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
main.go
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))
	}
}
Çıktı
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

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.
  • if koşulu parantezsiz, gövdesi her zaman süslü parantezlidir; kısa ifade biçimi değişken kapsamını daraltır.
  • switch otomatik geçiş yapmaz, case'lerde ifade kullanılabilir ve koşulsuz biçimi if/else if zincirinin yerini alır.
  • Go'da tek döngü anahtar kelimesi for'dur: klasik, while benzeri, sonsuz, range n ve range koleksiyon biçimleriyle.
  • range dilimde indeks+kopya, string'te bayt indeksi+rune, haritada anahtar+değer üretir; harita sırası rastgeledir.
  • break ve continue en içteki döngüye etki eder; dıştakine ulaşmak için etiket kullanılır.
  • switch içindeki break döngüyü değil switch'i sonlandırır.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.