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:
reflectpaketi kısmen destekleniyor- Bazı
encoding/jsonözellikleri çalışmıyor - Goroutine desteği kısıtlı
fmtpaketi 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.Funckarışı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, immutablebaş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.Funcnesneleri içinRelease()ç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.
