Go ile Basit HTTP Sunucusu Yazma ve Çalıştırma

Bir sysadmin olarak her gün onlarca servisi izliyor, log dosyalarını analiz ediyor ve çeşitli araçlarla uğraşıyorsunuz. Bu araçların büyük çoğunluğu ya kurulumu zahmetli bağımlılıklar gerektiriyor ya da production ortamında beklenmedik davranışlar sergiliyor. Go ile küçük bir HTTP sunucusu yazmak ise bambaşka bir deneyim: tek bir binary, sıfır bağımlılık, saniyeler içinde deploy. Bu yazıda Go ile HTTP sunucusu yazmayı ve çalıştırmayı gerçek dünya senaryoları eşliğinde ele alacağız.

Neden Go ile HTTP Sunucusu?

Python veya Ruby ile hızlıca bir HTTP sunucusu açmak mümkün ama bu yaklaşımların ciddi sınırları var. Önce runtime kurmanız gerekiyor, versiyon çakışmaları baş ağrısı yaratıyor, bağımlılık yönetimi ayrı bir dert oluyor.

Go’nun avantajları sysadmin perspektifinden şöyle sıralanabilir:

  • Statik binary: Derlenen uygulama tek bir dosya, hedef sistemde Go kurulu olmasına gerek yok
  • Düşük kaynak tüketimi: Node.js veya Ruby uygulamalarına kıyasla bellek ayak izi minimal
  • Hızlı başlangıç süresi: Servis restart senaryolarında milisaniye cinsinden ayağa kalkıyor
  • Standart kütüphane zenginliği: net/http paketi production-ready, harici framework gerekmeden işe yarıyor
  • Cross-compilation: Linux’ta yazıp Windows binary üretebiliyorsunuz

Go Kurulumu

Önce ortamı hazırlayalım. Ubuntu/Debian üzerinde:

# Resmi Go binary'sini indir
wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz

# Mevcut kurulumu temizle ve yenisini kur
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz

# PATH ayarını .bashrc veya .profile'a ekle
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc

# Kurulumu doğrula
go version

RHEL/CentOS tabanlı sistemlerde:

# DNF ile kurulum (repo'da eski versiyon olabilir)
sudo dnf install golang

# Ya da yukarıdaki wget yöntemiyle manuel kurulum yapabilirsiniz
# GOPATH ayarını da ekleyelim
echo 'export GOPATH=$HOME/go' >> ~/.bashrc
echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.bashrc
source ~/.bashrc

İlk HTTP Sunucusu: Basit Ama İşlevsel

Hemen işe girişelim. Proje dizinini oluşturup modülü başlatalım:

mkdir ~/go-http-server
cd ~/go-http-server
go mod init go-http-server

main.go dosyasını oluşturun:

cat > main.go << 'EOF'
package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func anaSayfa(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Merhaba! Sunucu %s adresinden çalışıyorn", r.Host)
    fmt.Fprintf(w, "İstek zamanı: %sn", time.Now().Format("2006-01-02 15:04:05"))
    fmt.Fprintf(w, "Client IP: %sn", r.RemoteAddr)
    fmt.Fprintf(w, "Method: %sn", r.Method)
}

func saglikKontrol(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, `{"status": "ok", "timestamp": "%s"}`, time.Now().Format(time.RFC3339))
}

func main() {
    http.HandleFunc("/", anaSayfa)
    http.HandleFunc("/health", saglikKontrol)

    port := ":8080"
    log.Printf("Sunucu %s portunda başlatılıyor...", port)

    if err := http.ListenAndServe(port, nil); err != nil {
        log.Fatalf("Sunucu başlatılamadı: %v", err)
    }
}
EOF

Derleyip çalıştıralım:

# Derle
go build -o http-server main.go

# Çalıştır
./http-server

# Başka bir terminalde test et
curl http://localhost:8080/
curl http://localhost:8080/health

Çıktı şuna benzer bir şey olmalı:

curl http://localhost:8080/health
# {"status": "ok", "timestamp": "2024-01-15T14:23:45+03:00"}

Gerçek Dünya Senaryosu 1: Log Toplama Endpoint’i

Ekibinizde çeşitli servisler var ve bunların loglarını merkezi bir yere göndermek istiyorsunuz. Karmaşık bir ELK stack kurmaya vaktiniz yok, geçici bir çözüme ihtiyacınız var. İşte tam burada Go devreye giriyor:

cat > log-collector.go << 'EOF'
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "time"
)

type LogEntry struct {
    Servis  string `json:"servis"`
    Seviye  string `json:"seviye"`
    Mesaj   string `json:"mesaj"`
    Zaman   string `json:"zaman"`
}

var logDosyasi *os.File

func logKaydet(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Sadece POST destekleniyor", http.StatusMethodNotAllowed)
        return
    }

    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Body okunamadı", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()

    var entry LogEntry
    if err := json.Unmarshal(body, &entry); err != nil {
        http.Error(w, "JSON parse hatası", http.StatusBadRequest)
        return
    }

    entry.Zaman = time.Now().Format(time.RFC3339)

    // Dosyaya yaz
    satirr := fmt.Sprintf("[%s] [%s] [%s] %sn",
        entry.Zaman, entry.Servis, entry.Seviye, entry.Mesaj)
    logDosyasi.WriteString(satirr)

    // Konsola da yaz
    log.Print(satirr)

    w.WriteHeader(http.StatusCreated)
    fmt.Fprintf(w, `{"durum": "kaydedildi"}`)
}

func logOku(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    logDosyasi.Seek(0, 0)
    io.Copy(w, logDosyasi)
}

func main() {
    var err error
    logDosyasi, err = os.OpenFile("uygulama.log",
        os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
    if err != nil {
        log.Fatalf("Log dosyası açılamadı: %v", err)
    }
    defer logDosyasi.Close()

    http.HandleFunc("/log", logKaydet)
    http.HandleFunc("/logs", logOku)
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, `{"status": "ok"}`)
    })

    log.Println("Log collector :9090 portunda başladı")
    log.Fatal(http.ListenAndServe(":9090", nil))
}
EOF

go run log-collector.go &

# Test edelim
curl -X POST http://localhost:9090/log 
  -H "Content-Type: application/json" 
  -d '{"servis": "nginx", "seviye": "ERROR", "mesaj": "Connection refused"}'

curl -X POST http://localhost:9090/log 
  -H "Content-Type: application/json" 
  -d '{"servis": "mysql", "seviye": "WARN", "mesaj": "Slow query detected"}'

# Logları oku
curl http://localhost:9090/logs

Middleware: İstek Loglama ve Basit Auth

Production ortamında her HTTP isteğini loglamak ve bazı endpoint’leri korumak şart. Middleware pattern’i bu iş için biçilmiş kaftan:

cat > middleware.go << 'EOF'
package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

// İstek loglama middleware'i
func istekLogla(sonraki http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        baslangic := time.Now()

        // Wrapped ResponseWriter ile status code'u yakala
        sw := &statusWriter{ResponseWriter: w, statusKod: 200}

        sonraki(sw, r)

        sure := time.Since(baslangic)
        log.Printf("%s %s %s %d %v %s",
            r.RemoteAddr,
            r.Method,
            r.URL.Path,
            sw.statusKod,
            sure,
            r.UserAgent(),
        )
    }
}

// Basit token auth middleware'i
func tokenKontrol(sonraki http.HandlerFunc) http.HandlerFunc {
    gizliToken := "super-gizli-token-123"

    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("X-Auth-Token")
        if token != gizliToken {
            http.Error(w, "Yetkisiz erişim", http.StatusUnauthorized)
            return
        }
        sonraki(w, r)
    }
}

// Status code'u yakalamak için wrapper
type statusWriter struct {
    http.ResponseWriter
    statusKod int
}

func (sw *statusWriter) WriteHeader(kod int) {
    sw.statusKod = kod
    sw.ResponseWriter.WriteHeader(kod)
}

func gizliEndpoint(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Bu korumalı bir endpoint! Hoş geldiniz.n")
}

func acikEndpoint(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Bu herkese açık.n")
}

func main() {
    // Middleware zinciri oluştur
    http.HandleFunc("/acik", istekLogla(acikEndpoint))
    http.HandleFunc("/gizli", istekLogla(tokenKontrol(gizliEndpoint)))

    log.Println("Middleware örneği :8081 portunda başladı")
    log.Fatal(http.ListenAndServe(":8081", nil))
}
EOF

go run middleware.go &

# Açık endpoint
curl http://localhost:8081/acik

# Token olmadan gizli endpoint
curl http://localhost:8081/gizli

# Doğru token ile
curl -H "X-Auth-Token: super-gizli-token-123" http://localhost:8081/gizli

Gerçek Dünya Senaryosu 2: Sunucu Metrik Endpoint’i

Monitoring sistemlerinize sunucu metriklerini HTTP üzerinden servis etmek istiyorsunuz. Prometheus exporter yazmak yerine hızlı bir çözüm:

cat > metrik-server.go << 'EOF'
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "runtime"
    "time"
)

type SunucuMetrik struct {
    Zaman       string  `json:"zaman"`
    Goroutine   int     `json:"goroutine_sayisi"`
    HeapMB      float64 `json:"heap_mb"`
    GoroutineMs int64   `json:"gc_sure_us"`
    Hostname    string  `json:"hostname"`
    GOOS        string  `json:"os"`
    GoVersiyon  string  `json:"go_versiyon"`
    CPUSayisi   int     `json:"cpu_sayisi"`
}

func metrikler(w http.ResponseWriter, r *http.Request) {
    var memStats runtime.MemStats
    runtime.ReadMemStats(&memStats)

    hostname, _ := os.Hostname()

    metrik := SunucuMetrik{
        Zaman:       time.Now().Format(time.RFC3339),
        Goroutine:   runtime.NumGoroutine(),
        HeapMB:      float64(memStats.HeapAlloc) / 1024 / 1024,
        GoroutineMs: int64(memStats.PauseTotalNs / 1000),
        Hostname:    hostname,
        GOOS:        runtime.GOOS,
        GoVersiyon:  runtime.Version(),
        CPUSayisi:   runtime.NumCPU(),
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(metrik)
}

func ozet(w http.ResponseWriter, r *http.Request) {
    hostname, _ := os.Hostname()
    fmt.Fprintf(w, "Hostname: %sn", hostname)
    fmt.Fprintf(w, "OS: %s/%sn", runtime.GOOS, runtime.GOARCH)
    fmt.Fprintf(w, "Go: %sn", runtime.Version())
    fmt.Fprintf(w, "CPU: %d çekirdekn", runtime.NumCPU())
    fmt.Fprintf(w, "Goroutine: %dn", runtime.NumGoroutine())
}

func main() {
    http.HandleFunc("/metrics", metrikler)
    http.HandleFunc("/ozet", ozet)
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprintf(w, "OK")
    })

    log.Println("Metrik sunucu :9091 portunda başladı")
    log.Fatal(http.ListenAndServe(":9091", nil))
}
EOF

go run metrik-server.go &
curl http://localhost:9091/metrics | python3 -m json.tool
curl http://localhost:9091/ozet

Binary Derleme ve Deployment

Go’nun en güçlü yanı cross-compilation. Bir kere derleyip farklı platformlara deploy edebiliyorsunuz:

# Mevcut platform için derle
go build -o http-server main.go

# Binary boyutunu küçült (strip symbols)
go build -ldflags="-s -w" -o http-server-kucuk main.go

# Boyut karşılaştır
ls -lh http-server http-server-kucuk

# Linux amd64 için (macOS'tan)
GOOS=linux GOARCH=amd64 go build -o http-server-linux main.go

# Windows için
GOOS=windows GOARCH=amd64 go build -o http-server.exe main.go

# ARM için (Raspberry Pi, ARM sunucular)
GOOS=linux GOARCH=arm64 go build -o http-server-arm64 main.go

# Derlenmiş binary'yi uzak sunucuya kopyala ve çalıştır
scp http-server-linux kullanici@sunucu:/opt/http-server/
ssh kullanici@sunucu "chmod +x /opt/http-server/http-server-linux && /opt/http-server/http-server-linux &"

Systemd Service Olarak Çalıştırma

Sunucunuzun sistem başlangıcında otomatik başlamasını ve crash durumunda restart etmesini istiyorsunuz:

# Binary'yi sistem dizinine kopyala
sudo cp http-server /usr/local/bin/
sudo chmod +x /usr/local/bin/http-server

# Systemd servis dosyası oluştur
sudo tee /etc/systemd/system/http-server.service << 'EOF'
[Unit]
Description=Go HTTP Sunucusu
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/local/bin/http-server
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=http-server

# Güvenlik ayarları
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target
EOF

# Servisi etkinleştir ve başlat
sudo systemctl daemon-reload
sudo systemctl enable http-server
sudo systemctl start http-server
sudo systemctl status http-server

# Logları izle
sudo journalctl -u http-server -f

Graceful Shutdown

Production’da çalışan bir servisi aniden kesmek veri kaybına yol açabilir. Graceful shutdown ile devam eden isteklerin tamamlanmasını bekleyebilirsiniz:

cat > graceful.go << 'EOF'
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func uzunIslem(w http.ResponseWriter, r *http.Request) {
    log.Println("Uzun işlem başladı...")
    time.Sleep(5 * time.Second)
    fmt.Fprintf(w, "İşlem tamamlandı!n")
    log.Println("Uzun işlem bitti.")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/islem", uzunIslem)
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "OK")
    })

    server := &http.Server{
        Addr:         ":8082",
        Handler:      mux,
        ReadTimeout:  30 * time.Second,
        WriteTimeout: 60 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    // Sinyal kanalı oluştur
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

    // Sunucuyu goroutine'de başlat
    go func() {
        log.Println("Sunucu :8082 portunda başladı")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Sunucu hatası: %v", err)
        }
    }()

    // Sinyal bekle
    sig := <-quit
    log.Printf("Kapatma sinyali alındı: %v", sig)

    // 30 saniye içinde graceful shutdown
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Graceful shutdown başarısız: %v", err)
    }

    log.Println("Sunucu düzgün kapatıldı.")
}
EOF

go run graceful.go &
# Test: uzun işlemi başlat, hemen sonra Ctrl+C'ye bas
curl http://localhost:8082/islem &
sleep 1
kill -TERM $(pgrep -f graceful)

Statik Dosya Servisi

Bazen sadece bazı dosyaları HTTP üzerinden servis etmek gerekiyor. Go’nun built-in file server’ı bu iş için fazlasıyla yeterli:

cat > dosya-server.go << 'EOF'
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "time"
)

func main() {
    // Servis edilecek dizin
    dizin := "./statik"
    if len(os.Args) > 1 {
        dizin = os.Args[1]
    }

    // Dizin yoksa oluştur
    os.MkdirAll(dizin, 0755)

    // Test dosyası oluştur
    os.WriteFile(dizin+"/test.txt",
        []byte(fmt.Sprintf("Test dosyası - %sn", time.Now().Format(time.RFC3339))),
        0644)

    fs := http.FileServer(http.Dir(dizin))

    log.Printf("'%s' dizini :8083 portunda servis ediliyor", dizin)
    log.Printf("http://localhost:8083 adresinden erişebilirsiniz")

    log.Fatal(http.ListenAndServe(":8083", fs))
}
EOF

go run dosya-server.go
# Başka terminalde:
curl http://localhost:8083/test.txt
curl http://localhost:8083/  # Dizin listesi

Performans İpuçları ve Dikkat Edilmesi Gerekenler

Gerçek production kullanımında göz önünde bulundurmanız gereken konular:

  • Timeout ayarları: ReadTimeout, WriteTimeout ve IdleTimeout mutlaka set edilmeli, varsayılan değerler sonsuz bekleyebilir
  • Goroutine leak: Her HTTP handler ayrı bir goroutine’de çalışıyor, context’i düzgün yönetmezseniz goroutine sızıntısı olabilir
  • Rate limiting: Harici saldırılara karşı golang.org/x/time/rate paketi ile rate limiting eklenebilir
  • TLS/HTTPS: http.ListenAndServeTLS ile Let’s Encrypt sertifikası kullanabilirsiniz
  • Binary boyutu: UPX ile binary daha da küçültülebilir ama bazı antivirus yazılımları false positive verebilir
  • Port seçimi: 1024 altı portlar için root yetkisi gerekiyor, ya yüksek port kullanın ya da CAP_NET_BIND_SERVICE capability ekleyin
  • Log rotation: log paketi dosyaya yazarken rotation yapmıyor, logrotate veya lumberjack kütüphanesi kullanılabilir

Sonuç

Go ile HTTP sunucusu yazmak, sysadmin araç kutunuza güçlü bir ekleme yapıyor. Standart kütüphane ile production-ready bir sunucu yazabiliyorsunuz, harici framework veya karmaşık bağımlılıklar olmadan. Derlenen binary tek dosya halinde her yere taşınabiliyor, systemd ile entegrasyonu kolay, kaynak tüketimi minimal.

Bu yazıda ele aldığımız senaryolar, günlük sysadmin hayatında karşılaşılan gerçek ihtiyaçları yansıtıyor: log toplama, metrik servisi, statik dosya sunumu, basit auth. Bunların hepsini birkaç satır Go koduyla çözebiliyorsunuz.

Bir sonraki adım olarak net/http paketini daha derinlemesine incelemenizi, Prometheus metrik formatını eklemenizi ve HTTPS desteğini hayata geçirmenizi öneririm. Go standart kütüphanesi bu konularda da yeterince güçlü, çoğu zaman dışarıdan bir şey çekmenize gerek kalmıyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir