Go Nedir, Neden Var? C# Geliştiricisi için Go'nun Felsefesi, Toolchain'i ve Ne Zaman Seçilir
Go, Google'da büyük codebase'lerin yavaş build'ine ve karmaşıklığına cevap olarak doğmuş, az özellikli, native koda derlenen ve runtime'ı binary'nin içinde taşıyan bir dil. İki endpoint'li Bookstore servisinin Windows için 9,9 MB'lık tek .exe olması, 10 ms'de açılıp 12 MB kullanması, ASP.NET Core ile karşılaştırma tablosu, go build/vet/fmt/fix/doc, cross-compile, altı aylık release politikası ve Go'yu ne zaman seçmen gerektiği.
Sürüm notu: Bu yazıdaki kodlar Go 1.27.4 ile derlenip çalıştırılmıştır; aynı kod Go 1.26'da da derlenir. Karşılaştırma için kullanılan ASP.NET Core uygulaması .NET 10 SDK 10.0.404 ile derlenmiştir.
Önce Tahmin Et
Bu seride sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'sinin bir kısmını Go ile yazacağız. İlk adım küçük: iki endpoint'i olan tek dosyalık bir servis. Biri düz metin döndürüyor, diğeri üç kitabı JSON olarak listeliyor. Hiçbir paket indirmiyoruz, sadece Go'nun standart kütüphanesi var.
// Bookstore API'nin ilk hâli: tek dosya, sadece standart kütüphane
package main
import (
"encoding/json"
"log"
"net/http"
)
type book struct {
ISBN string `json:"isbn"`
Title string `json:"title"`
}
var books = []book{
{ISBN: "9780441172719", Title: "Dune"},
{ISBN: "9780547928227", Title: "The Hobbit"},
{ISBN: "9780141439549", Title: "Middlemarch"},
}
func main() {
http.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from Bookstore API\n"))
})
http.HandleFunc("GET /api/books", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(books)
})
addr := "127.0.0.1:52710"
log.Printf("bookstore-api listening on http://%s", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
Aynı iki endpoint'i ASP.NET Core 10'da minimal API olarak da yazdım (CreateSlimBuilder, MapGet ve bir record Book). Şimdi iki soru. Bu servisleri Windows x64 için tek dosya hâlinde build edersen dosyalar kaç MB olur ve çalıştırmak için makinede ne kurulu olmalı? Servis ayağa kalkıp ilk cevabı kaç milisaniyede verir ve 200 istekten sonra kaç MB bellek kullanır?
go mod init bookstore
go run .
go: creating new go.mod: module bookstore
2026/11/15 03:48:48 bookstore-api listening on http://127.0.0.1:52710
curl -i http://127.0.0.1:52710/api/books
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 15 Nov 2026 00:48:51 GMT
Content-Length: 135
[{"isbn":"9780441172719","title":"Dune"},{"isbn":"9780547928227","title":"The Hobbit"},{"isbn":"9780141439549","title":"Middlemarch"}]
Windows için build ettiğimde sonuçlar şöyle çıktı:
| Build | Dosya boyutu | Hedef makinede gereken |
|---|---|---|
Go, go build | 9,9 MB tek .exe | Hiçbir şey |
Go, -trimpath -ldflags="-s -w" | 6,8 MB tek .exe | Hiçbir şey |
| .NET, framework-dependent | 162 KB .exe + 7 KB .dll + config dosyaları | ASP.NET Core Runtime 10 |
| .NET, self-contained single-file | 103,4 MB | Hiçbir şey |
| .NET, self-contained single-file + trimmed | 17,4 MB | Hiçbir şey |
Başlangıç ve bellek ölçümünü yazıyı hazırladığım makinede, iki uygulamanın da o makine için derlenmiş hâliyle yaptım: process'i başlatıp /hello ilk cevabı verene kadar geçen süreyi ve 200 /api/books isteğinden sonraki RSS (Resident Set Size; process'in o an fiziksel bellekte tuttuğu alan) değerini küçük bir script'le, yedi koşunun medyanı olarak aldım. Go servisi ilk cevabı 10 ms'de verdi ve 12,2 MB kullandı. ASP.NET Core servisi ilk cevabı 203 ms'de verdi ve 75,5 MB kullandı. Windows'ta mutlak sayılar farklı çıkar, ama aradaki farkın büyüklüğü bu yazının konusunu iyi özetliyor: Go, runtime'ı binary'nin içine koyup native kod üreten, küçük ve hızlı açılan servisler için tasarlanmış bir dil. Yazının geri kalanında bunun nedenlerini, C#'tan farklarını ve Go'yu ne zaman seçmen gerektiğini konuşacağız.
Go Neden Var?
Go'nun FAQ (sık sorulan sorular) sayfasına göre Robert Griesemer, Rob Pike ve Ken Thompson dilin hedeflerini 21 Eylül 2007'de bir beyaz tahtada çizmeye başladı. Dil 10 Kasım 2009'da açık kaynak oldu, Go 1 ise 28 Mart 2012'de çıktı. Çıkış noktası Google'daki günlük sorunlardı: C++ ile yazılmış dev bir codebase'in derlenmesi uzun sürüyordu, bağımlılıklar karmaşıklaşmıştı, çok çekirdekli makineler yaygınlaşmıştı ama dillerin çoğu bunları güvenli programlamaya pek yardım etmiyordu. Ekip, dinamik bir dilin yazma kolaylığını derlenen ve statik tipli bir dilin hızı ve güvenliğiyle birleştirmek istedi. Hedeflerden biri de büyük bir programı birkaç saniyede build etmekti.
Bu hedefin dile yansıması "az özellik" oldu. Go'da class yok, inheritance yok, exception yok, operator overloading yok, method overloading yok, default parametre yok. Döngü olarak sadece for var. Generics 2022'de, Go 1.18 ile geldi; C#'ta generics 2005'teki C# 2.0'dan beri var. Bu eksikler tasarım kararı: Go ekibi bir özelliği eklemeden önce o özelliğin kodu okuyan kişiye getireceği yükü tartıyor. Sonuç, bir Go dosyasını açtığında kimin yazdığını pek anlayamaman. Bir projede bir hafta geçiren biri, başka bir Go projesinin kodunu da rahat okur.
İkinci önemli karar Go 1 uyumluluk sözü. Go 1 için yazılmış kod, güvenlik düzeltmesi gibi birkaç istisna dışında sonraki Go 1.x sürümlerinde derlenip çalışmaya devam eder. Davranış değiştiren düzeltmeler ise GODEBUG ayarlarıyla ve go.mod'daki go satırıyla kontrol edilir; bunu 02'de göreceğiz. .NET'te major sürüm geçişlerinde breaking change listesi okumaya alışkınsan, Go'daki yükseltmelerin çoğunun sadece toolchain sürümünü değiştirmekten ibaret olduğunu göreceksin.
C# Geliştiricisi için Go: Yan Yana
C# Temelleri serisinin "C# ve .NET Nedir?" yazısında dil, runtime ve SDK ayrımını anlatmıştık. Go'da bu üçü tek bir indirme dosyasında gelir: derleyici, standart kütüphane ve go komutu. Tabloda geçen kısaltmalar: IL (Intermediate Language; C# derleyicisinin ürettiği ara kod), CLR (Common Language Runtime; .NET'te IL'i çalıştıran runtime), JIT (Just-In-Time; kodu çalışırken makine koduna çeviren derleme) ve AOT (Ahead-Of-Time; kodu çalıştırmadan önce native koda derleme). Farkların özeti şöyle:
| Konu | C# / .NET | Go |
|---|---|---|
| Çalışma modeli | IL'e derlenir, CLR'de JIT ile çalışır; Native AOT seçenek | Doğrudan native koda derlenir, VM yok |
| Bellek | Garbage collector | Garbage collector, runtime binary'nin içinde |
| Deploy | Runtime kurulu makine ya da self-contained publish | Tek static binary |
| Tipler | class, struct, record, inheritance | struct ve method'lar; inheritance yok, embedding var |
| Interface | Açıkça implement edilir (: IRepository) | Implicit: method'lar uyuyorsa tip interface'i sağlar |
| Hata yönetimi | Exception, try/catch | error dönüş değeri, if err != nil |
| Concurrency | Task, async/await | Goroutine ve channel, renkli fonksiyon yok |
| Null | Nullable reference types | Zero value; pointer, map, slice, interface için nil |
| Paketler | NuGet, .csproj | Module'ler, go.mod ve go.sum |
| Kod stili | .editorconfig, analyzer'lar | gofmt, tek resmî biçim |
"Renkli fonksiyon yok" ifadesini biraz açalım. C#'ta bir method async olunca onu çağıran method da genellikle async olur; async kod çağrı zincirinde yukarı doğru yayılır. Go'da böyle bir ayrım yok. Her fonksiyon normal bir fonksiyon gibi yazılır, I/O beklerken bloklanır gibi görünür, ama runtime o sırada thread'i başka bir goroutine'e verir. C# Asenkron Programlama serisinin Task yazısında anlattığımız state machine'in işini Go'da runtime'ın scheduler'ı yapıyor. Bunu 07'de ayrıntılı göreceğiz.
İlk Program Satır Satır
package main ve func main(), bu dosyanın bir kütüphane değil çalıştırılabilir bir program olduğunu söylüyor. C#'taki top-level statement'ların karşılığı. import bloğundaki üç package standart kütüphaneden geliyor; NuGet'teki gibi ayrıca bir şey eklemedik. go mod init bookstore komutu bir go.mod dosyası oluşturdu. Go'daki bookstore module'ü, C# tarafındaki Bookstore.Api projesinin karşılığı; Bookstore.Catalog gibi namespace'ler 02'den itibaren internal/catalog gibi package'lar olacak.
book struct'ı küçük harfle başlıyor. Go'da bir ismin package dışından görünüp görünmediğini ilk harf belirler: büyük harf export edilir, küçük harf package içinde kalır. public ya da private anahtar kelimesi yok. Field'lar ISBN ve Title büyük harfli, çünkü encoding/json package'ı sadece export edilmiş field'ları görebilir. Field'ın yanındaki `json:"isbn"` bir struct tag'i; C#'taki [JsonPropertyName("isbn")] attribute'unun karşılığı.
http.HandleFunc("GET /api/books", ...) satırındaki pattern'de HTTP method'u da var. Bu, Go 1.22'den beri standart kütüphanenin router'ında olan bir özellik; minimal API'deki app.MapGet("/api/books", ...) ile aynı iş. Bu yüzden POST /api/books isteği 405, tanımlı olmayan / isteği 404 döndü. Handler'ın imzası func(w http.ResponseWriter, r *http.Request): cevabı w'ya yazarsın, isteği r'den okursun. ASP.NET Core'daki HttpContext'in iki parçaya ayrılmış hâli gibi düşünebilirsin.
Adres olarak 127.0.0.1:52710 kullandım. Sadece :8080 yazsaydın servis bütün network arayüzlerini dinlerdi ve Windows'ta ilk çalıştırmada Defender Firewall izin penceresi açılırdı. Geliştirme sırasında loopback adresine bağlanmak hem bunu önler hem de servisi yerel ağa açmaz. 52710 ise başka bir uygulamayla çakışma ihtimali düşük, seri boyunca kullanacağımız port.
Bu kodda iki şeyi bilerek bıraktım. w.Write ve Encode birer error döndürüyor ve ben ikisine de bakmadım; Go'da hata dönüş değeridir ve görmezden gelmek bir tercihtir. Bunu 05'te düzelteceğiz. http.ListenAndServe ise timeout'suz bir server açıyor; deneme için yeterli, production için değil. 09'da http.Server'ı timeout'larıyla kuracağız.
Tek Araç: go Komutu
.NET'te dotnet CLI'ı, analyzer'lar, formatter ve test runner farklı parçalardan gelir. Go'da hepsi tek bir go komutunun alt komutları: go build derler, go run derleyip çalıştırır, go test testleri koşar, go vet şüpheli kodu bulur, go fmt biçimlendirir, go doc dokümantasyonu gösterir, go fix kodu yeni sürümün deyimlerine çevirir, go mod bağımlılıkları yönetir. Bilerek kötü yazılmış küçük bir dosyayla bunları deneyelim:
// Bilerek kötü biçimlendirilmiş ve hatalı bir dosya
package main
import "fmt"
func describe(v interface{}) string {
return fmt.Sprintf("%v", v)
}
func main() {
stock := map[string]int{"Dune": 3, "Emma": 0}
for i := 0; i < 3; i++ {
fmt.Printf("round %d: Dune stock is %s\n", i, stock["Dune"])
}
fmt.Println(describe(stock))
}
gofmt -l .
go vet ./...
main.go
main.go:12:41: fmt.Printf format %s has arg stock["Dune"] of wrong type int
gofmt -l biçimi bozuk dosyaları listeledi. go vet ise derleyicinin yakalamadığı bir hatayı buldu: %s bir string bekliyor, biz int verdik. Bu kod derlenir ve çalışır, ama çıktıda round 0: Dune stock is %!s(int=3) satırını görürsün. go test çalışırken vet'in bir alt kümesini kendiliğinden koşar, yani bu tür hataları çoğu zaman test sırasında fark edersin.
go fix Go 1.26'da baştan yazıldı ve artık modernizer'ların (kodu dilin yeni deyimlerine çeviren düzeltmelerin) yeri. -diff ile neyi değiştireceğini görebilirsin:
go fix -diff ./...
--- /home/user/tools01/main.go (old)
+++ /home/user/tools01/main.go (new)
@@ -2,14 +2,14 @@
import "fmt"
-func describe(v interface{}) string {
- return fmt.Sprintf("%v", v)
+func describe(v any) string {
+ return fmt.Sprintf("%v", v)
}
func main() {
- stock := map[string]int{"Dune": 3, "Emma": 0}
- for i := 0; i < 3; i++ {
- fmt.Printf("round %d: Dune stock is %s\n", i, stock["Dune"])
- }
- fmt.Println(describe(stock))
+ stock := map[string]int{"Dune": 3, "Emma": 0}
+ for i := range 3 {
+ fmt.Printf("round %d: Dune stock is %s\n", i, stock["Dune"])
+ }
+ fmt.Println(describe(stock))
}
interface{} yerine any, sayaçlı döngü yerine for i := range 3 yazdı ve dosyayı da biçimlendirdi. Girintiler tab oldu: gofmt'un tek kuralı bu ve tartışmaya açık değil. Go dünyasında "tab mı space mi", "süslü parantez nereye" tartışması yapılmaz, çünkü herkes aynı aracı kullanır. Bu yazılarda kodu okunaklı olsun diye 4 boşlukla gösteriyorum, ama dosyalarda tab var. vet'in bulduğu %s hatasına ise dokunmadı; go fix sadece davranışı değiştirmeyen düzeltmeleri uygular.
Dokümantasyon da aynı araçta. go doc bir package'ın ya da fonksiyonun açıklamasını terminalde gösterir:
go doc net/http.HandleFunc
package http // import "net/http"
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
HandleFunc registers the handler function for the given pattern in
DefaultServeMux. The documentation for ServeMux explains how patterns are
matched.
Tek Binary ve Cross-Compile
Önce Tahmin Et'teki .exe'yi Windows'ta değil, başka bir işletim sisteminde build ettim. Go'da hedef platform iki environment variable ile seçilir: GOOS ve GOARCH. Ek bir SDK ya da runtime pack indirmek gerekmez:
GOOS=windows GOARCH=amd64 go build -o bin/bookstore-api.exe .
go version -m bin/bookstore-api.exe
bin/bookstore-api.exe: go1.27.4
path bookstore
mod bookstore (devel)
build -buildmode=exe
build -compiler=gc
build CGO_ENABLED=0
build GOARCH=amd64
build GOOS=windows
build GOAMD64=v1
build vcs=git
build vcs.modified=true
go version -m binary'nin içine gömülü build bilgisini okur: hangi Go sürümüyle, hangi platform için, hangi ayarlarla derlendiğini. CGO_ENABLED=0 satırı önemli. Cross-compile yaparken cgo (Go'dan C kodu çağırma mekanizması) varsayılan olarak kapanır, binary hiçbir C library'sine bağlı olmaz. Bu yüzden dosyayı Windows'a kopyalayıp doğrudan çalıştırabilirsin. Windows'ta çalışıyorsan ve bir Linux sunucusu için build alacaksan PowerShell'de $env:GOOS="linux"; $env:GOARCH="amd64"; go build -o bin/bookstore-api . yazarsın (bu satırı Windows'ta denemedim). Bu yazıdaki .exe'yi de Windows'ta çalıştırmadım; sadece build ettim ve içindeki bilgiyi okudum.
.NET tarafında aynı sonuca self-contained publish ile ulaşırsın, ama o zaman runtime'ın kendisi de pakete girer ve dosya 100 MB'ı geçer. Trimming bunu 17,4 MB'a indiriyor. Native AOT ise daha küçük bir dosya üretir, ama Windows için Windows'ta build etmeyi gerektirir; bu yüzden burada ölçmedim. Go'da küçük ve bağımsız binary varsayılan davranış, ayrıca bir ayar gerekmiyor. -ldflags="-s -w" debug sembollerini çıkarıp dosyayı 6,8 MB'a indirdi; -trimpath ise build makinesindeki dosya yollarını binary'den siler. İkisini 13'te production build'iyle birlikte konuşacağız.
Release Politikası: Altı Ayda Bir Sürüm, İki Sürüm Destek
Go her yıl iki major sürüm çıkarıyor: Şubat'ta ve Ağustos'ta. Go 1.26 10 Şubat 2026'da, Go 1.27 19 Ağustos 2026'da yayınlandı. Release History sayfasındaki kural kısa: her major sürüm, kendisinden yeni iki major sürüm çıkana kadar desteklenir. Yani bugün 1.27 ve 1.26 destekleniyor; Şubat 2027'de 1.28 çıkınca 1.26'nın desteği bitecek. Desteklenen sürümlere güvenlik ve kritik hata düzeltmeleri minor sürüm olarak gelir. Bu yazıyı denediğim 1.27.4 ve onunla aynı gün çıkan 1.26.11, 10 Kasım 2026'da yayınlanan güvenlik sürümleri.
.NET'teki LTS (Long Term Support; uzun süreli destek) ve STS (Standard Term Support; standart süreli destek) ayrımı Go'da yok. Her sürümün desteği yaklaşık bir yıl sürer, bu yüzden Go projelerinde toolchain'i yılda en az bir kez yükseltmek normal bir iştir. Uyumluluk sözü sayesinde bu yükseltmeler genellikle sorunsuzdur. Bu seri Go 1.27 üzerine kurulu, ama örnekleri 1.26'da da derleyecek şekilde yazıyorum; 1.27'ye özgü bir şey kullandığımda ayrıca belirteceğim.
Go Ne Zaman Seçilir, Ne Zaman Seçilmez?
Go'nun en güçlü olduğu alan network servisleri ve altyapı araçları. Docker, Kubernetes, Terraform ve Prometheus Go ile yazılmış; bulut altyapısı ekosisteminin büyük kısmı Go konuşuyor. Bunun teknik nedenleri yukarıda gördüklerimiz: tek binary deploy, düşük bellek, hızlı açılış, kolay cross-compile ve standart kütüphanede production'a yakın bir HTTP server. CLI araçları da aynı nedenle Go'da yazılmaya uygun: kullanıcı tek bir dosya indirir ve çalıştırır.
Go'nun zayıf kaldığı yerler de belli. Zengin bir domain modeli, yani çok sayıda kuralı, durum geçişini ve ilişkiyi olan bir iş mantığı, C#'ın tip sistemiyle ve Entity Framework Core ile daha rahat yazılır. Go'da yaygın bir ORM (Object-Relational Mapping; veritabanı tablolarını object'lere map eden library) kültürü yok; topluluk elle yazılmış SQL'i ya da SQL'den kod üreten araçları tercih ediyor (11'de bunu Dapper ile kıyaslayacağız). Ekibin tamamı .NET'te derinse ve servis zaten iyi çalışıyorsa, sadece daha az bellek için dil değiştirmek çoğu zaman değmez.
Bookstore özelinde düşünürsek, sipariş, ödeme ve kurumsal onay akışı gibi kuralı bol çekirdek .NET'te kalır. Stok bilgisini depodan çekip dağıtan küçük bir servis, kargo firmasından gelen webhook'ları alan bir uç, ya da back office ekibinin kullandığı bir komut satırı aracı Go için iyi adaylardır. Bu seride öğrenmek için Bookstore API'sinin bir kısmını Go'da yazıyoruz, çünkü bir dili öğrenmenin en iyi yolu tanıdık bir problemi onunla çözmek.
Üretimde Dikkat
Toolchain sürümünü sabitle ve takip et. Her major sürüm yaklaşık bir yıl desteklendiği için, CI'da kullandığın Go sürümünü bir yerde açıkça yazmalısın. Bunun Go'daki yeri go.mod'daki go ve toolchain satırları; 02'de ikisini ayrıntılı göreceğiz. Güvenlik sürümlerini de .NET'in Patch Tuesday güncellemeleri gibi düzenli uygula; 1.27.4 de standart kütüphane ve toolchain için güvenlik düzeltmeleri getiren bir sürüm.
Binary'ye neyin girdiğini bil. go version -m çıktısı, production'da çalışan bir binary'nin hangi Go sürümüyle ve hangi bağımlılıklarla derlendiğini sonradan öğrenmeni sağlar. Güvenlik taraması yapan araçlar da bu bilgiyi okur.
Ölçümlere temkinli bak. 10 ms ve 12 MB etkileyici sayılar, ama bunlar iki endpoint'li bir servisin sayıları. Gerçek bir servis veritabanı bağlantıları, log'lar ve cache ile büyür. Go'yu seçmenin gerekçesi bu sayılar olacaksa, ölçümü kendi servisinin gerçekçi bir versiyonuyla yap.
Sık Düşülen Tuzaklar
Go'yu C# gibi yazmaya çalışmak. İlk haftalarda her şey için interface açmak, derin package hiyerarşileri kurmak, exception yerine her yerde panic kullanmak sık görülür. Go'da sade kod tercih edilir; seri boyunca C# alışkanlığı ile Go deyimini yan yana koyacağım.
Hata dönüşlerini görmezden gelmek. Yukarıdaki kodda Encode'un döndürdüğü hataya bakmadık. Derleyici buna izin verir, çünkü dönüş değerini hiç kullanmıyoruz. Ama değeri bir değişkene atayıp kullanmazsan derleyici hata verir: Go'da kullanılmayan değişken ve kullanılmayan import derleme hatasıdır.
Servisi :8080 gibi bir adresle açıp bütün arayüzleri dinlemek. Geliştirme makinesinde servis farkında olmadan yerel ağa açılır. Production'da da dinlenecek adres configuration'dan gelmeli; 13'te bunu kuracağız.
go run'ı production'da kullanmak. go run her seferinde derleyip geçici bir klasörde çalıştırır ve makinede Go toolchain'i olmasını ister. Sunucuya her zaman go build çıktısını koy.
Küçük Bir Deneme
★☆☆ main.go'ya GET /api/books/count diye bir endpoint ekle ve kitap sayısını düz metin olarak döndür. len(books) bir int döndürüyor; w.Write ise []byte istiyor. Derleyici ne diyor? fmt.Fprintf(w, "%d\n", len(books)) ile çöz.
★★☆ Servisi kendi işletim sistemin için ve bir de Linux için (GOOS=linux GOARCH=amd64) build et. İki dosyanın boyutunu karşılaştır ve ikisine de go version -m uygula. Hangi satırlar farklı?
★★★ Önce Tahmin Et'teki ASP.NET Core minimal API'sini kendin yaz ve iki servisin başlangıç süresini ve belleğini kendi makinende ölç. Sonra .NET tarafını Native AOT ile publish et (-p:PublishAot=true, Windows'ta Visual Studio'nun C++ build araçları gerekir) ve ölçümü tekrarla. Fark ne kadar kapanıyor?
Go'nun ne olduğunu ve neden var olduğunu gördük. Bir sonraki yazıda go.mod'u açıp içindeki her satırın anlamına bakacağız: module path, go ve toolchain satırları, go.sum ve module proxy. Sonra tek dosyalık servisi cmd/bookstore-api ve internal/catalog ile gerçek bir proje yapısına taşıyacağız.