Bir programın yaptığı her iş, sonuçta bellekteki değerleri okumak ve değiştirmektir. Değişkenler bu değerlere verdiğimiz isimlerdir. Go'da değişken tanımlamak basittir ama dilin iki kararlı tercihi baştan bilinmelidir: her değişkenin bir tipi vardır ve bu tip derleme sırasında kesinleşir; ayrıca hiçbir değişken tanımsız kalmaz — değer vermezsen tipine uygun bir sıfır değer alır.
Bu iki kural birlikte, başka dillerde başını çok ağrıtan bir hata sınıfını ortadan kaldırır. Go'da undefined, null pointer sürprizleri yerine belirli ve öngörülebilir başlangıç değerleri vardır: sayılar 0, metinler "", boolean'lar false olarak başlar. Buna karşılık Go, farklı tipler arasında örtük dönüşüm yapmaz: Bir int ile bir float64'ü doğrudan toplayamazsın. İlk başta katı görünen bu kural, kayan nokta hatalarının ve taşmaların sessizce oluşmasını engeller.
Bu derste var ve := ile tanımlamayı, sıfır değerleri, tamsayı ve ondalık tiplerin boyutlarını, byte ile rune arasındaki farkı, açık tip dönüşümlerini, const ve tipsiz sabitleri, iota ile numaralandırma yapmayı, kapsam (scope) kurallarını ve en sinsi hatalardan biri olan gölgelemeyi (shadowing) öğreneceksin.
Değişken tanımlamanın yolları
Go'da bir değişkeni tanımlamanın üç temel biçimi vardır:
package main
import "fmt"
func main() {
// 1) var + tip + değer
var city string = "Bursa"
// 2) var + tip (değer verilmez → sıfır değer)
var count int
// 3) var + değer (tipi derleyici çıkarır)
var ratio = 0.75
// 4) := kısa tanımlama (yalnızca fonksiyon içinde)
name := "Deniz"
count = 3
fmt.Println(city, count, ratio, name)
// Tek satırda birden çok değişken
var x, y int = 3, 4
a, b := "sol", "sağ"
fmt.Println(x, y, a, b)
// Gruplanmış tanım bloğu
var (
title = "Rapor"
pages = 12
printed bool
)
fmt.Println(title, pages, printed)
}Bursa 3 0.75 Deniz 3 4 sol sağ Rapor 12 false
:= yalnızca fonksiyonların içinde çalışır; paket düzeyinde (fonksiyon dışında) tanım yapıyorsan var kullanmak zorundasın. Pratikte Go kodu büyük ölçüde := ile yazılır. var şu durumlarda tercih edilir: sıfır değerle başlatmak istediğinde, tipi açıkça belirtmek gerektiğinde (var timeout float64 = 3), ve paket düzeyi tanımlarda.
Tip çıkarımı ve varsayılan tipler
:= veya değer verilen bir var kullandığında derleyici tipi sağdaki ifadeden çıkarır. Bu çıkarım sihirli değildir, belirli kurallara dayanır: Sağdaki ifade tipsiz bir sabitse, o sabitin varsayılan tipi kullanılır. Varsayılan tipler şunlardır: tamsayı sabitleri için int, ondalık sabitler için float64, rune sabitleri ('a') için rune, metinler için string, true/false için bool.
Bu yüzden x := 3 ifadesi int, y := 3.0 ifadesi float64 üretir; ikisi de üç sayısını temsil etse bile tipleri farklıdır ve doğrudan toplanamazlar. Aynı şekilde r := 'A' bir rune (yani int32) üretir, byte değil. Tipi kendin belirlemek istediğinde ya var ile açıkça yazarsın (var y float32 = 3) ya da dönüşümle zorlarsın (y := float32(3)).
Bir başka ince nokta: Fonksiyon çağrılarında tip çıkarımı fonksiyonun dönüş tipinden gelir. n := len(s) ifadesinde n bir int'tir, çünkü len int döndürür. Bu yüzden len sonucunu bir int64 alana atarken dönüşüm gerekir.
Sıfır değerler
Değer verilmeyen her değişken, tipine göre belirli bir başlangıç değeri alır:
package main
import "fmt"
func main() {
var (
n int
f float64
s string
ok bool
list []int
table map[string]int
)
fmt.Printf("%d %.1f %q %t\n", n, f, s, ok)
fmt.Println(list == nil, len(list))
fmt.Println(table == nil, len(table))
}0 0.0 "" false true 0 true 0
Bu, Go'da "sıfır değeri kullanışlı olan tipler tasarla" (make the zero value useful) fikrinin temelidir: var b strings.Builder veya var mu sync.Mutex yazdığında değişken doğrudan kullanıma hazırdır, ayrıca başlatman gerekmez.
Sayısal tipler ve boyutları
Go'da tamsayı tipleri işaretli ve işaretsiz olmak üzere ikiye ayrılır, boyutları isimlerinde yazar:
Pratik kural: Özel bir nedenin yoksa int kullan. Boyutu belirli tipler ikili dosya biçimleri, ağ protokolleri, bellek kısıtı olan büyük diziler ve bit işlemleri için vardır. int tipinin genişliği platforma bağlıdır: Bugün kullandığın hemen her makinede 64 bittir, ancak gömülü sistemlerde veya 32 bitlik hedeflerde 32 bit olabilir. Bu nedenle bir değerin gerçekten 64 bit olması gerekiyorsa (örneğin bir zaman damgasını nanosaniye cinsinden tutuyorsan) int64 yazmak doğru tercihtir.
İşaretsiz tipler (uint, uint8…) ilk bakışta "negatif olmayan sayılar" için doğal görünür, ama Go'da dikkatli kullanılmaları gerekir. Çünkü işaretsiz bir değişken sıfırın altına indiğinde negatife düşmez, en büyük değere sarmalanır. for i := len(s) - 1; i >= 0; i-- gibi geriye sayan bir döngüde i işaretsiz olsaydı koşul hiçbir zaman bozulmaz ve döngü sonsuza kadar dönerdi. Bu yüzden Go standart kütüphanesi uzunluk ve indeks için her yerde işaretli int kullanır.
Ondalık sayılarda iki tip vardır: float32 ve float64. Varsayılan olarak float64 kullanılır; float32 yalnızca bellek gerçekten önemliyse tercih edilir. Ayrıca complex64 ve complex128 karmaşık sayı tipleri de vardır, ancak günlük kodda nadiren görülürler.
package main
import (
"fmt"
"math"
)
func main() {
var small int8 = 127
small++ // taşma: sarmalanır, hata vermez
fmt.Println("int8 taşması:", small)
var u uint8 = 0
u-- // işaretsiz tipte 0'ın altına inmek
fmt.Println("uint8 alt taşması:", u)
fmt.Println("int64 en büyük:", math.MaxInt64)
// Değişkenlerle yapılan ondalık aritmetik derleme sırasında değil,
// çalışma zamanında IEEE-754 kurallarıyla hesaplanır:
a, b, c := 0.1, 0.2, 0.3
fmt.Println("toplam:", a+b)
fmt.Println("eşit mi:", a+b == c)
fmt.Printf("fark: %.20f\n", (a+b)-c)
}int8 taşması: -128 uint8 alt taşması: 255 int64 en büyük: 9223372036854775807 toplam: 0.30000000000000004 eşit mi: false fark: 0.00000000000000005551
bool, string, byte ve rune
bool yalnızca true veya false değerini alır. Go'da 0 ve 1 boolean yerine geçmez; if count yazamazsın, if count > 0 yazman gerekir. Bu katılık, C ve JavaScript gibi dillerde sık görülen "doğruluk değeri" (truthiness) karışıklığını tamamen ortadan kaldırır: Bir koşulun ne zaman doğru olduğunu okurken tahmin yürütmen gerekmez.
string değiştirilemez (immutable) bir bayt dizisidir ve içeriği UTF-8 kodludur. Bir string'i indekslediğinde bayt alırsın, karakter değil:
package main
import "fmt"
func main() {
s := "Göl"
fmt.Println("bayt uzunluğu:", len(s))
fmt.Println("ilk bayt:", s[0], "tipi byte")
var b byte = s[0] // byte = uint8 takma adı
var r rune = 'ö' // rune = int32 takma adı, Unicode kod noktası
fmt.Printf("%c (%d) | %c (%d)\n", b, b, r, r)
fmt.Printf("%T %T\n", b, r)
for i, ch := range s { // range bir string'i rune rune gezer
fmt.Printf("%d:%c ", i, ch)
}
fmt.Println()
}bayt uzunluğu: 4 ilk bayt: 71 tipi byte G (71) | ö (246) uint8 int32 0:G 1:ö 3:l
byte, uint8 için; rune ise int32 için bir takma addır (alias). İkisi de gerçek birer tip değil, okunabilirliği artıran isimlerdir: byte "ham veri", rune "bir Unicode karakteri" demektir. Bu ayrım Türkçe metinlerde kritik önemdedir ve Stringler ve Rune'lar dersinin ana konusudur.
Açık tip dönüşümleri
Go farklı tipler arasında hiçbir zaman örtük dönüşüm yapmaz. Aşağıdaki kod derlenmez:
var i int = 3
var f float64 = 1.5
fmt.Println(i * f) // HATA: invalid operation: mismatched types int and float64Dönüşümü T(deger) biçiminde açıkça yazmalısın:
package main
import "fmt"
func main() {
i := 7
f := 2.0
fmt.Println(float64(i) / f) // 3.5
fmt.Println(i / int(f)) // 3 — tamsayı bölmesi, kalan atılır
// Daraltan dönüşümler sessizce veri kaybeder
big := 300 // değişken olduğu için derleyici taşmayı yakalayamaz
fmt.Println("int8'e:", int8(big)) // 300 int8'e sığmaz
fmt.Println("uint8'e:", uint8(big)) // 300 - 256 = 44
// Ondalıktan tamsayıya: kesme (yuvarlama değil!)
price, debt := 3.99, -3.99
fmt.Println(int(price), int(debt))
// Sayıdan metne: strconv gerekir, string(int) rune'a çevirir
fmt.Println(string(rune(65)), "←", 65)
}3.5 3 int8'e: 44 uint8'e: 44 3 -3 A ← 65
Daraltan dönüşümlerde kaybolan bilgi sessizce gider: int8(300) ifadesi 300 sayısının yalnızca alt 8 bitini alır ve 44 verir. Derleyici bunu yalnızca sabit ifadelerde yakalar; değişkenle yaptığında uyarı almazsın. Bir dönüşümün güvenli olup olmadığını kontrol etmen gerekiyorsa math.MaxInt8 gibi sınırlarla karşılaştırma yapman ya da dönüşümden sonra değeri geri çevirip eşitliği sınaman gerekir.
Son satır klasik bir tuzaktır: string(65) ifadesi "65" değil, 65 numaralı Unicode karakteri olan "A"'yı verir. Bu yüzden Go derleyicisi string(int) yazımına uyarı verir. Sayıyı metne çevirmek için strconv.Itoa(65) veya fmt.Sprintf("%d", 65) kullanılır.
Sabitler: const ve tipsiz sabitler
const ile tanımlanan değerler derleme sırasında hesaplanır ve çalışma zamanında değiştirilemez. Bir sabit yalnızca temel tiplerden (sayı, string, bool, rune) olabilir; dilim veya map sabit olamaz.
Go'nun sabitlerdeki en güçlü özelliği tipsiz sabitlerdir (untyped constants). Bir sabite tip vermezsen, kullanıldığı yere göre uygun tipe bürünür ve rastgele hassasiyetle saklanır:
package main
import "fmt"
const (
Pi = 3.14159265358979323846 // tipsiz ondalık
MaxUsers = 1000 // tipsiz tamsayı
AppName = "go.me" // tipsiz metin
)
const TypedRate float64 = 0.18 // tipli sabit
func main() {
var radius float32 = 2
// Pi tipsiz olduğu için float32 ile çarpılabiliyor:
fmt.Println("alan:", Pi*radius*radius)
var limit int32 = MaxUsers // tipsiz sabit int32'ye uyum sağlar
fmt.Println("limit:", limit)
// Tipli sabit uyum sağlamaz; dönüşüm gerekir:
var amount float32 = float32(TypedRate) * 100
fmt.Printf("%.2f\n", amount)
// Sabit ifadeleri derleme sırasında hesaplanır
const KB = 1 << 10
const MB = KB * 1024
fmt.Println(KB, MB)
}alan: 12.566371 limit: 1000 18.00 1024 1048576
Tipsiz bir sabit taşacağı bir tipe atanırsa hata derleme sırasında verilir: const big = 1 << 40 ifadesini var x int8 = big şeklinde atamaya çalışmak constant overflows int8 hatası üretir. Çalışma zamanı taşmasının aksine bu hata derhal görülür.
iota ile numaralandırma
Go'da enum anahtar kelimesi yoktur. Yerine const bloğu ve iota sayacı kullanılır. iota, her const bloğunda 0'dan başlar ve her satırda bir artar:
package main
import "fmt"
type Weekday int
const (
Monday Weekday = iota // 0
Tuesday // 1
Wednesday // 2
Thursday // 3
Friday // 4
Saturday // 5
Sunday // 6
)
// String metodu, fmt paketinin bu tipi nasıl yazdıracağını belirler
func (d Weekday) String() string {
names := []string{"Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi", "Pazar"}
if d < Monday || d > Sunday {
return "bilinmeyen"
}
return names[d]
}
// Bit bayrakları için kaydırmalı iota
type Permission uint8
const (
Read Permission = 1 << iota // 1 (0b001)
Write // 2 (0b010)
Exec // 4 (0b100)
)
func main() {
fmt.Println(Wednesday, int(Wednesday))
fmt.Println(Saturday, Sunday)
p := Read | Write
fmt.Printf("%03b okuma:%t yazma:%t çalıştırma:%t\n",
p, p&Read != 0, p&Write != 0, p&Exec != 0)
// Atlanan değerler için _ kullanılır
const (
_ = iota // 0 atlanır
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
)
fmt.Println(KB, MB, GB)
}Çarşamba 2 Cumartesi Pazar 011 okuma:true yazma:true çalıştırma:false 1024 1048576 1073741824
Weekday gibi kendi tipini tanımlamak, sayıyı anlamlandırır: Fonksiyon imzasında int yerine Weekday görürsen ne beklendiğini anlarsın ve yanlışlıkla rastgele bir sayı geçirmen zorlaşır. String() metodunun nasıl çalıştığını Arayüzler dersinde göreceksin.
Kapsam ve gölgeleme
Bir değişkenin görünür olduğu bölgeye kapsam (scope) denir. Go'da kapsam süslü parantezlerle belirlenir: Bir blok içinde tanımlanan değişken yalnızca o blokta (ve iç bloklarda) görünür.
paket kapsamı
└── fonksiyon kapsamı
└── if / for / switch bloğu kapsamı
└── iç blok kapsamıİç bloktaki bir tanım, dıştaki aynı isimli değişkeni gölgeler (shadowing). Dıştaki değişken silinmez; yalnızca iç blok boyunca erişilemez olur:
package main
import "fmt"
func main() {
x := "dış"
{
x := "iç" // yeni bir değişken; dıştakini gölgeler
fmt.Println("blok içinde:", x)
}
fmt.Println("blok sonrası:", x)
count := 0
for i := range 3 { // i yalnızca döngü içinde görünür
count += i
}
fmt.Println("toplam:", count)
if n := len("merhaba"); n > 5 { // n yalnızca if/else içinde görünür
fmt.Println("uzun:", n)
} else {
fmt.Println("kısa:", n)
}
}blok içinde: iç blok sonrası: dış toplam: 3 uzun: 7
Gölgeleme en çok hata yönetiminde sorun çıkarır. Aşağıdaki desende dıştaki err hiçbir zaman güncellenmez, çünkü := iç blokta yeni bir err yaratır:
var err error
if f, err := os.Open("veri.txt"); err == nil { // buradaki err YENİ bir değişken
defer f.Close()
}
// dıştaki err hâlâ nil — hata sessizce kayboldugo vet bu tür gölgelemeleri varsayılan olarak yakalamaz; golangci-lint gibi araçların shadow denetimi yakalar. Kural olarak: := kullandığın her yerde solundaki isimlerden en az birinin yeni olduğunu unutma. a, err := f() satırında a yeniyse err yeniden tanımlanmaz, atanır — ama bu yalnızca aynı kapsamdaysalar geçerlidir.
Sık yapılan hatalar
:=ile paket düzeyinde tanım yapmaya çalışmak. Fonksiyon dışında yalnızcavar,const,typevefuncbildirimleri bulunabilir.- Örtük dönüşüm beklemek.
intvefloat64doğrudan toplanmaz;float64(i) + fyazmalısın. string(65)ile sayıyı metne çevirmeye çalışmak. Sonuç"A"olur.strconv.Itoakullan.- Ondalık sayıları
==ile karşılaştırmak. Tolerans kullan veya tamsayı cinsinden sakla. - Gölgelemeyi fark etmemek. İç blokta
:=ile aynı ismi yeniden tanımlamak dıştakini güncellemez. - Taşmayı fark etmemek.
int8,uint8gibi dar tiplerde toplama ve çıkarma sessizce sarmalanır (127 + 1 = -128). Çalışma zamanı hatası oluşmaz. - Gereksiz yere
int32/int64kullanmak. Özel bir gerekçen yoksaintkullan; API'ler deintbekler.
Alıştırmalar
int, float64, string, bool ve []string tiplerinden birer değişkeni var ile, değer vermeden tanımla. Her biri için tipini ve sıfır değerini %T ve %q/%v fiilleriyle tek satırda yazdır. Dilimin nil olup olmadığını da göster.
İpucu
%q metin için tırnaklı gösterim verir; boş metnin "" olduğunu görmek için bu fiili kullan.
Çözümü göster
package main
import "fmt"
func main() {
var n int
var f float64
var s string
var b bool
var list []string
fmt.Printf("%-10T %v\n", n, n)
fmt.Printf("%-10T %v\n", f, f)
fmt.Printf("%-10T %q\n", s, s)
fmt.Printf("%-10T %v\n", b, b)
fmt.Printf("%-10T %v (nil mi: %t, uzunluk: %d)\n", list, list, list == nil, len(list))
}int 0 float64 0 string "" bool false []string [] (nil mi: true, uzunluk: 0)
nil bir dilimin uzunluğu 0'dır ve len, range, append ile güvenle kullanılabilir — bu, "sıfır değeri kullanışlı olsun" ilkesinin tipik bir örneğidir.
Celsius cinsinden bir tamsayı sıcaklığı Fahrenheit'a çeviren bir program yaz. Dönüşüm formülü F = C × 9/5 + 32. Dikkat: 9/5 tamsayı bölmesiyle 1 sonucunu verir, bu yüzden hesabı ondalık yapmalısın. −10, 0, 25 ve 100 dereceler için sonucu bir ondalık basamakla yazdır. Ayrıca tamsayı bölmesinin yanlış sonucunu da göstererek farkı ortaya koy.
İpucu
float64(c)*9/5 + 32 ifadesinde bölme ondalık yapılır. c*9/5 + 32 ise tamamen tamsayı aritmetiğiyle hesaplanır.
Çözümü göster
package main
import "fmt"
func toFahrenheit(c int) float64 {
return float64(c)*9/5 + 32
}
func wrongConversion(c int) int {
return c * (9 / 5) // 9/5 tamsayı bölmesi → 1
}
func main() {
temps := []int{-10, 0, 25, 100}
fmt.Printf("%6s %10s %10s\n", "C", "F (doğru)", "F (yanlış)")
for _, c := range temps {
fmt.Printf("%6d %10.1f %10d\n", c, toFahrenheit(c), wrongConversion(c)+32)
}
}C F (doğru) F (yanlış)
-10 14.0 22
0 32.0 32
25 77.0 57
100 212.0 1329/5 ifadesindeki her iki sayı da tipsiz tamsayı sabiti olduğu için sonuç 1 olur. float64(c)*9/5 ifadesinde ise ilk çarpma sonucu float64 olduğundan bölme de ondalık yapılır. İşlemlerin sırası kritiktir: float64(c*9/5) yazsaydın yine yanlış sonuç alırdın.
iota ve bit kaydırma kullanarak Read, Write, Exec izinlerini tanımla. Bir izin kümesini alıp rwx biçiminde metne çeviren String() metodunu yaz (izin yoksa ilgili yere - koy). Ardından birkaç izin birleşimi oluştur, bunları | ile birleştir, &^ (bit temizleme) ile bir izni kaldır ve sonuçları yazdır.
İpucu
p&Read != 0 ifadesi iznin verilip verilmediğini söyler. Bir izni kaldırmak için p &^ Write kullanılır.
Çözümü göster
package main
import (
"fmt"
"strings"
)
type Permission uint8
const (
Read Permission = 1 << iota
Write
Exec
)
func (p Permission) String() string {
var sb strings.Builder
for _, item := range []struct {
bit Permission
char string
}{{Read, "r"}, {Write, "w"}, {Exec, "x"}} {
if p&item.bit != 0 {
sb.WriteString(item.char)
} else {
sb.WriteString("-")
}
}
return sb.String()
}
func main() {
owner := Read | Write | Exec
group := Read | Exec
other := Read
fmt.Printf("%v %v %v\n", owner, group, other)
fmt.Printf("ham değerler: %d %d %d\n", owner, group, other)
readOnly := owner &^ (Write | Exec) // yazma ve çalıştırmayı kaldır
fmt.Println("salt okunur:", readOnly)
fmt.Println("group yazabilir mi:", group&Write != 0)
}rwx r-x r-- ham değerler: 7 5 1 salt okunur: r-- group yazabilir mi: false
1 << iota deseni her sabite ayrı bir bit verir: 001, 010, 100. Böylece birden çok izin tek bir sayıda saklanabilir; | ekler, & sorgular, &^ kaldırır. String() metodu tanımlandığı için %v ve Println bu tipi otomatik olarak okunur biçimde yazar.
Kısa sınav
var s string ile tanımlanan bir değişkenin değeri nedir?
fmt.Println(7 / 2, 7.0 / 2) ne yazdırır?
byte ve rune tipleri hangi tiplerin takma adıdır?
Tipsiz bir sabitin (const Pi = 3.14) tipli bir sabitten (const Pi float64 = 3.14) farkı nedir?
Aşağıdaki kodda son satır ne yazdırır?\nx := 1; if true \{ x := 2; _ = x \}; fmt.Println(x)
var x int8 = 127 iken x++ çalıştırılırsa ne olur?
Özet
varher yerde,:=yalnızca fonksiyon içinde kullanılır; kullanılmayan yerel değişkenler derleme hatasıdır.- Değer verilmeyen her değişken sıfır değer alır: sayılar
0,string"",boolfalse, referans tiplerinil. - Özel bir neden yoksa
intvefloat64kullan; boyutu belirli tipler protokol, dosya biçimi ve bit işlemleri içindir. byte=uint8(ham bayt),rune=int32(Unicode kod noktası).- Go örtük tip dönüşümü yapmaz;
T(deger)ile açıkça dönüştürürsün. Daraltan dönüşümler sessizce veri kaybeder. - Tipsiz sabitler kullanıldıkları bağlama uyum sağlar; sabit taşmaları derleme sırasında yakalanır.
iota,constbloğunda 0'dan başlayan bir sayaçtır; numaralandırma ve bit bayrakları için kullanılır.- Kapsam süslü parantezlerle belirlenir; iç bloktaki
:=dıştaki aynı isimli değişkeni gölgeler ve bu, hata yönetiminde sessiz hatalara yol açar.