Go ile WebAssembly Derleme ve Tarayıcıda Çalıştırma

Go ile WebAssembly’e geçiş yapmak isteyip de nereden başlayacağını bilmeyenler için bu yazı iyi bir başlangıç noktası olacak. Rust ile WebAssembly konuşulduğunda herkes hemen wasm-pack ve wasm-bindgen dünyasına dalıyor, ama Go tarafında da güçlü bir ekosistem var ve özellikle Go zaten bilen bir ekip için öğrenme eğrisi çok daha yumuşak. Geçen ay bir müşterimizin mevcut Go mikroservislerinden bir kısmını tarayıcı tarafında da çalıştırma ihtiyacı doğduğunda bu yolu denemek zorunda kaldık ve öğrendiklerimizi paylaşmak istedim.

Go ve WebAssembly: Genel Resim

Go, 1.11 sürümünden itibaren resmi olarak GOOS=js GOARCH=wasm hedefini destekliyor. Bu, Go kodunu doğrudan .wasm dosyasına derleyebileceğiniz anlamına geliyor. Rust’ın WebAssembly ekosistemiyle karşılaştırıldığında Go’nun ürettiği dosyalar genellikle daha büyük oluyor, ama bu durum her zaman kritik bir sorun değil. Özellikle dahili araçlarda, yönetim panellerinde veya zaten Go bilen bir ekibin geliştireceği projelerde hız ve tanıdıklık büyük avantaj sağlıyor.

Bir şeyin altını çizmek gerekiyor: Go’nun WebAssembly desteği iki farklı şekilde geliyor.

  • Standart Go derleyicisi (GOOS=js GOARCH=wasm): Tam Go runtime’ı içeriyor, dosya boyutu büyük oluyor ama tüm Go özellikleri çalışıyor.
  • TinyGo: Gömülü sistemler ve WebAssembly için optimize edilmiş alternatif derleyici, çok daha küçük dosyalar üretiyor ama bazı standart kütüphane özellikleri eksik.

Bu yazıda her ikisini de ele alacağız.

Geliştirme Ortamını Hazırlamak

Önce temiz bir ortam kuralım. Go’nun güncel bir sürümünün kurulu olduğunu varsayıyorum:

# Go sürümünü kontrol et
go version

# Proje dizini oluştur
mkdir go-wasm-demo && cd go-wasm-demo
go mod init go-wasm-demo

# WebAssembly için gerekli JavaScript runtime dosyasını kopyala
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

Bu wasm_exec.js dosyası kritik. Go’nun WebAssembly runtime’ı bu dosya üzerinden tarayıcıyla iletişim kuruyor. Her Go sürümünde değişebiliyor, bu yüzden kendi Go sürümünüzden kopyalamak önemli. CDN’den veya başka bir kaynaktan almayın.

İlk WebAssembly Modülünü Yazmak

Basit bir örnekle başlayalım. Tarayıcıda çalışacak ve JavaScript’ten çağrılabilecek bir fonksiyon yazacağız:

// main.go
//go:build js && wasm

package main

import (
    "fmt"
    "syscall/js"
)

func hesapla(this js.Value, args []js.Value) interface{} {
    if len(args) < 2 {
        return "Hata: En az 2 parametre gerekli"
    }
    
    a := args[0].Float()
    b := args[1].Float()
    sonuc := a * b
    
    return fmt.Sprintf("%.2f x %.2f = %.2f", a, b, sonuc)
}

func topla(this js.Value, args []js.Value) interface{} {
    toplam := 0.0
    for _, arg := range args {
        toplam += arg.Float()
    }
    return toplam
}

func main() {
    // JavaScript global nesnesine fonksiyonları kaydet
    js.Global().Set("goHesapla", js.FuncOf(hesapla))
    js.Global().Set("goTopla", js.FuncOf(topla))
    
    fmt.Println("Go WebAssembly modülü yüklendi!")
    
    // Programın sonlanmaması için kanal beklet
    select {}
}

Şimdi bunu derleyelim:

GOOS=js GOARCH=wasm go build -o main.wasm main.go

# Dosya boyutunu kontrol edelim
ls -lh main.wasm
# Muhtemelen 2-5 MB arasında bir şey göreceksiniz

Tarayıcı Tarafını Hazırlamak

Go WebAssembly modülünü yükleyecek HTML ve JavaScript dosyalarını oluşturalım:

<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go WebAssembly Demo</title>
</head>
<body>
    <h1>Go WebAssembly Demo</h1>
    
    <div>
        <input type="number" id="sayi1" placeholder="Birinci sayı" value="12">
        <input type="number" id="sayi2" placeholder="İkinci sayı" value="7">
        <button onclick="hesaplaClick()">Çarp</button>
        <p id="sonuc">Sonuç burada görünecek</p>
    </div>

    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        
        WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
            .then((result) => {
                go.run(result.instance);
                console.log("WASM modülü hazır");
            })
            .catch(err => {
                console.error("WASM yükleme hatası:", err);
            });
        
        function hesaplaClick() {
            const s1 = parseFloat(document.getElementById("sayi1").value);
            const s2 = parseFloat(document.getElementById("sayi2").value);
            
            // Go'dan gelen global fonksiyonu çağır
            const sonuc = goHesapla(s1, s2);
            document.getElementById("sonuc").textContent = sonuc;
        }
    </script>
</body>
</html>

Lokal geliştirme için küçük bir HTTP sunucusu gerekiyor çünkü fetch API’si file:// protokolüyle çalışmıyor:

# Python ile hızlı HTTP sunucusu
python3 -m http.server 8080

# Ya da Go ile daha düzgün bir şey yazabilirsiniz
# go run server.go

Gerçek Dünya Senaryosu: Veri Doğrulama Kütüphanesi

Teorik örneklerden çıkıp gerçek bir kullanım senaryosuna geçelim. Diyelim ki backend’de Go ile yazdığınız veri doğrulama mantığını frontend’de de kullanmak istiyorsunuz. TC kimlik no doğrulama gibi bir şey mükemmel örnek:

// validator.go
//go:build js && wasm

package main

import (
    "strconv"
    "syscall/js"
)

func tcKimlikDogrula(this js.Value, args []js.Value) interface{} {
    if len(args) == 0 {
        return map[string]interface{}{
            "gecerli": false,
            "hata":    "TC kimlik numarası boş",
        }
    }
    
    tc := args[0].String()
    
    if len(tc) != 11 {
        return map[string]interface{}{
            "gecerli": false,
            "hata":    "TC kimlik numarası 11 haneli olmalı",
        }
    }
    
    if tc[0] == '0' {
        return map[string]interface{}{
            "gecerli": false,
            "hata":    "TC kimlik numarası 0 ile başlayamaz",
        }
    }
    
    rakamlar := make([]int, 11)
    for i, c := range tc {
        sayi, err := strconv.Atoi(string(c))
        if err != nil {
            return map[string]interface{}{
                "gecerli": false,
                "hata":    "Geçersiz karakter bulundu",
            }
        }
        rakamlar[i] = sayi
    }
    
    // Algoritma kontrolü
    tekCift1 := (rakamlar[0] + rakamlar[2] + rakamlar[4] + rakamlar[6] + rakamlar[8]) * 7
    tekCift2 := rakamlar[1] + rakamlar[3] + rakamlar[5] + rakamlar[7]
    
    d10 := (tekCift1 - tekCift2) % 10
    if d10 != rakamlar[9] {
        return map[string]interface{}{
            "gecerli": false,
            "hata":    "Kontrol hanesi geçersiz",
        }
    }
    
    toplam := 0
    for i := 0; i < 10; i++ {
        toplam += rakamlar[i]
    }
    
    if toplam%10 != rakamlar[10] {
        return map[string]interface{}{
            "gecerli": false,
            "hata":    "Son kontrol hanesi geçersiz",
        }
    }
    
    return map[string]interface{}{
        "gecerli": true,
        "hata":    "",
    }
}

func main() {
    js.Global().Set("tcKimlikDogrula", js.FuncOf(tcKimlikDogrula))
    select {}
}

Bu senaryoda hem backend hem frontend aynı doğrulama mantığını kullanıyor. Sunucu tarafında test geçen kod, tarayıcıda da aynı şekilde çalışıyor. API’ye gereksiz istek atmadan anlık doğrulama yapılabiliyor.

TinyGo ile Dosya Boyutunu Küçültmek

Standart Go derleyicisinin ürettiği 2-5 MB’lık WASM dosyaları bazı senaryolarda kabul edilemez. TinyGo burada devreye giriyor:

# TinyGo kurulumu (Ubuntu/Debian)
wget https://github.com/tinygo-org/tinygo/releases/download/v0.31.2/tinygo_0.31.2_amd64.deb
sudo dpkg -i tinygo_0.31.2_amd64.deb

# TinyGo ile derleme
tinygo build -o main-tiny.wasm -target wasm ./main.go

# Boyut karşılaştırması
ls -lh main.wasm main-tiny.wasm
# main.wasm: ~3.2MB
# main-tiny.wasm: ~150KB

Ancak TinyGo’nun sınırlılıklarını bilmek gerekiyor:

  • reflect paketi kısmen destekleniyor
  • Bazı encoding/json özellikleri çalışmıyor
  • Goroutine desteği kısıtlı
  • fmt paketi bazı edge case’lerde farklı davranabiliyor

TinyGo için wasm_exec.js de farklı. TinyGo kendi runtime dosyasını içeriyor:

# TinyGo'nun wasm_exec.js dosyasını al
cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js ./wasm_exec_tiny.js

Performans Kritik İşlemler: Görüntü İşleme Örneği

Go WebAssembly’nin gerçekten parlayan noktası CPU-yoğun işlemler. Saf JavaScript’te yavaş olan şeyler Go’da çok daha hızlı çalışabiliyor. Basit bir görüntü filtresi örneği:

// imageprocess.go
//go:build js && wasm

package main

import (
    "syscall/js"
)

// Gri tonlama filtresi - JavaScript'e kıyasla belirgin hız farkı
func griTonla(this js.Value, args []js.Value) interface{} {
    if len(args) == 0 {
        return nil
    }
    
    // JavaScript'ten Uint8ClampedArray al (canvas ImageData)
    pikselVerisi := args[0]
    uzunluk := pikselVerisi.Get("length").Int()
    
    // Go slice'a kopyala
    veri := make([]byte, uzunluk)
    js.CopyBytesToGo(veri, pikselVerisi)
    
    // Her piksel için gri tonlama hesapla
    for i := 0; i < uzunluk; i += 4 {
        r := float64(veri[i])
        g := float64(veri[i+1])
        b := float64(veri[i+2])
        // Luminance formülü
        gri := byte(0.299*r + 0.587*g + 0.114*b)
        veri[i] = gri
        veri[i+1] = gri
        veri[i+2] = gri
        // Alpha kanalı değişmiyor: veri[i+3]
    }
    
    // Sonucu JavaScript'e geri kopyala
    js.CopyBytesToJS(pikselVerisi, veri)
    return nil
}

func main() {
    js.Global().Set("goGriTonla", js.FuncOf(griTonla))
    fmt.Println("Görüntü işleme modülü hazır")
    select {}
}

JavaScript tarafında canvas ile kullanımı:

// canvas-demo.js
async function gorselIsle() {
    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    
    // Go fonksiyonunu çağır
    const baslangic = performance.now();
    goGriTonla(imageData.data);
    const bitis = performance.now();
    
    ctx.putImageData(imageData, 0, 0);
    console.log(`İşlem süresi: ${bitis - baslangic}ms`);
}

Webpack ve Build Pipeline Entegrasyonu

Gerçek bir projede WASM dosyalarını build pipeline’a entegre etmek gerekiyor. Basit bir Makefile ile başlayalım:

# Makefile

GOOS=js
GOARCH=wasm
WASM_OUT=dist/main.wasm

.PHONY: build-wasm clean serve

build-wasm:
	GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="-s -w" -o $(WASM_OUT) ./wasm/
	cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" dist/
	@echo "WASM boyutu: $(shell ls -lh $(WASM_OUT) | awk '{print $$5}')"

# wasm-opt ile optimize et (binaryen gerekli)
optimize:
	wasm-opt -O3 -o $(WASM_OUT).opt $(WASM_OUT)
	mv $(WASM_OUT).opt $(WASM_OUT)
	@echo "Optimize sonrası boyut: $(shell ls -lh $(WASM_OUT) | awk '{print $$5}')"

serve:
	go run tools/server.go

clean:
	rm -f dist/*.wasm dist/wasm_exec.js

-ldflags="-s -w" bayrağı debug sembollerini ve DWARF bilgisini çıkarıyor, dosya boyutunu %20-30 küçültüyor.

Hata Ayıklama ve Yaygın Sorunlar

Sahada karşılaştığımız en sık sorunları paylaşayım:

MIME type hatası: Sunucu application/wasm MIME tipini dönmüyorsa tarayıcı WASM dosyasını reddediyor.

# Nginx için MIME type ekle
# /etc/nginx/mime.types dosyasına ekle:
# application/wasm wasm;

# Ya da Go HTTP sunucusunda:
// server.go - Geliştirme için basit sunucu
package main

import (
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    
    // WASM MIME type'ı için özel handler
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if len(r.URL.Path) > 5 && r.URL.Path[len(r.URL.Path)-5:] == ".wasm" {
            w.Header().Set("Content-Type", "application/wasm")
        }
        http.FileServer(http.Dir("./dist")).ServeHTTP(w, r)
    })
    
    http.ListenAndServe(":8080", mux)
}

Diğer yaygın sorunlar:

  • select {} unutmak: Main fonksiyonu dönerse WASM modülü kapanıyor, tüm kayıtlı fonksiyonlar kullanılamaz hale geliyor.
  • Büyük nesne transferi: JavaScript ile Go arasında büyük veri aktarımı pahalı. Mümkünse SharedArrayBuffer kullanın.
  • Goroutine ve js.Func karışımı: Goroutine içinden JavaScript DOM’una erişmeye çalışmak panic’e yol açabiliyor.
  • wasm_exec.js sürüm uyumsuzluğu: Go sürümü güncellediğinizde bu dosyayı da güncellemeyi unutmayın.

CI/CD Pipeline’a Entegrasyon

GitHub Actions ile otomatik WASM build örneği:

# .github/workflows/wasm-build.yml
name: WASM Build

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Go Setup
      uses: actions/setup-go@v5
      with:
        go-version: '1.22'
    
    - name: WASM Derle
      run: |
        mkdir -p dist
        GOOS=js GOARCH=wasm go build 
          -ldflags="-s -w" 
          -o dist/main.wasm 
          ./wasm/
        cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" dist/
    
    - name: WASM Testlerini Çalıştır
      run: |
        # Node.js ile WASM testleri
        node --experimental-wasm-modules tests/wasm_test.js
    
    - name: Artifact Yükle
      uses: actions/upload-artifact@v4
      with:
        name: wasm-dist
        path: dist/

Üretim Ortamında Dikkat Edilmesi Gerekenler

Deneyimlerimden derlediğim birkaç önemli nokta:

  • Caching stratejisi: WASM dosyaları büyük olduğundan Cache-Control: max-age=31536000, immutable başlığıyla ve içerik hash’i ile sunun. Yeni deploy’larda dosya adını değiştirin.
  • Lazy loading: Sayfa yüklenirken WASM’ı hemen yüklemeyin. Kullanıcı ilgili özelliği açtığında yükleyin.
  • Fallback mekanizması: WASM yüklenemezse saf JavaScript fallback’i her zaman bulundurun, özellikle eski tarayıcılar için.
  • Bellek yönetimi: Go garbage collector WASM içinde de çalışıyor ama js.Func nesneleri için Release() çağırmayı unutmayın, aksi takdirde bellek sızıntısı oluşuyor.
  • Boyut bütçesi: 2MB’ın üzerindeki WASM dosyaları mobil kullanıcılarda kötü deneyim yaratıyor. TinyGo veya kritik modülleri ayrı WASM dosyalarına bölmek iyi bir pratik.

Sonuç

Go ile WebAssembly, özellikle zaten Go bilen ve backend mantığını frontend’de yeniden kullanmak isteyen ekipler için güçlü bir seçenek. Rust’ın sunduğu ince taneli kontrol veya minimum dosya boyutu yoksa da Go’nun geliştirici deneyimi ve standart kütüphanesinin zenginliği ciddi avantaj sağlıyor.

Standart Go derleyicisi dosya boyutu konusunda Rust’a kıyasla dezavantajlı, ama TinyGo bu farkı büyük ölçüde kapatıyor. Veri doğrulama kütüphaneleri, şifreleme işlemleri, sıkıştırma algoritmaları veya domain-specific hesaplamalar için Go WebAssembly gerçekten işe yarıyor. Müşterimizin projesinde backend’deki 3000 satırlık doğrulama mantığını sıfır değişiklikle (tamam, build tag ekledik ve main fonksiyonunu değiştirdik) tarayıcıda çalıştırabildik. Bu tür kod paylaşımı senaryoları için Go WebAssembly’nin maliyeti kesinlikle değiyor.

Bir sonraki adım olarak WASM Component Model ve WASI standartlarını takip etmenizi öneririm. Go ekibi bu alanlara aktif olarak yatırım yapıyor ve önümüzdeki iki yıl içinde ekosistem önemli ölçüde olgunlaşacak.

Bir yanıt yazın

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