İçeriğe atla

Node.js'ten Go'ya AWS Lambda Geçişi: Pratik Rehber

AWS Lambda'da Node.js'den Go'ya geçiş ne zaman kendini amorti eder, ne zaman etmez: karar çerçevesi, serverless Go pattern'ları ve maliyet matematiği.

Ayhan Sipahi Ayhan Sipahi

Serverless faturaları beklenenden hızlı büyüme eğilimindedir ve takımların ilk uzandığı kollardan biri dil seçimidir. Node.js’den Go’ya geçiş bu kolun alışılmış adayı: aynı Lambda platformunda, aynı event payload’larıyla çalışan, bellekte az yer kaplayan derlenmiş bir binary.

İşe yarayan varsayılan dar kapsamlı. Tek bir serviste ölçülebilir bir hacim, gecikme veya bellek maliyeti problemi yoksa Node.js’de kal; varsa yalnızca o servisi taşı. Go, mantığı basit ve trafiği yüksek handler’larda kendini amorti eder. İki yılda oturmuş karmaşık business logic servisinde ise nadiren amorti eder; kararın zor yarısı da zaten ne zaman taşımamak gerektiğini görmektir.

Go Lambda’da Ne Zaman Mantıklı#

Burada dil tercihinden çok bir karar ağacı işe yarar. Soru hiçbir zaman Go’nun Node.js’den daha iyi bir dil olup olmadığı değildir; Go’nun hâlihazırda ölçebildiğin bir problemi çözüp çözmediğidir.

Yüksek Volume, Basit Mantık#

Go’nun serverless’ta parladığı yerler:

Go tutarlı bir şekilde şu servislerde değer katıyor:

  • Dakikada binlerce isteği öngörülebilir paternlerle işleyen
  • CPU-yoğun operasyonlar yapan (data transformation, validasyon, encoding)
  • Yük altında tutarlı sub-100ms response time’a ihtiyaç duyan
  • Lambda maliyet optimizasyonu nedeniyle memory kısıtları olan

En dramatik iyileştirmeler bu spesifik paternlerde ortaya çıkar:

  • API Gateway handler’ları JSON validasyon ve transformation yapan
  • Event processing fonksiyonları SQS/SNS mesajlarını scale’de işleyen
  • Data pipeline componentları streaming data işleyen
  • Authentication servisleri JWT validasyon ve kullanıcı lookup yapan

Node.js’in Kalması Gereken Yerler#

İşte Go migration isteğine direnmenin doğru olduğu yerler:

Karmaşık business logic servisleri: 2000 satırlık karmaşık e-ticaret workflow’larını handle eden Node.js servisin mi var? Migration çabası takımının velocity’sini aylarca öldürür ve performans kazanımı karmaşıklığı haklı çıkarmaz.

Hızlı prototipleme ortamları: Eğer takımın haftalık yeni özellikler çıkarıyor ve kullanıcı feedback’ine göre iterate ediyorsa, JavaScript’in esnekliği ve ekosistemi Go’nun compile-time güvenliğinden daha iyi hizmet edecektir.

Küçük takım, çok junior geliştirici: Go’nun learning curve’ü gerçek. Takımlar interface’ler, error handling patternları ve type system konularında aylarca zorlanabilir.

Runtime Farkı Nereden Geliyor#

“Go daha hızlı” cümlesi, invocation’ın hangi parçasının hızlandığını söylemeden hiçbir şey ifade etmez. Aynı handler yeniden yazıldığında üç şey değişir: runtime’ın senin kodun çalışmadan önce yüklediği şeyler, execution boyunca bellekte kalan alan ve dilin JSON parse ile object üretimine harcadığı iş.

Üçünün de altında tek bir platform farkı yatıyor. AWS’in Go belgeleri, Go doğrudan çalıştırılabilir bir binary’ye derlendiği için ayrı bir dil runtime’ına ihtiyaç duymadığını söylüyor ve Go fonksiyonlarını provided ailesinden bir OS-only runtime’a yönlendiriyor; go1.x kullanımdan kaldırıldı, provided.al2023 için destek 30 Haziran 2029’a kadar sürüyor. Node.js ise AWS’in derlediği, yamaladığı ve senin adına başlattığı managed bir runtime üzerinde çalışıyor. Aşağıdakilerin çoğu bu tek ayrımın sonucu.

Yeniden Yazılan Bir Ödeme Handler’ı#

Ödeme endpoint’i karşılaştırma için elverişli bir şekil, çünkü yapılan iş büyük ölçüde parse et, doğrula, dış sağlayıcıyı çağır, yanıt dön.

Öncesi (Node.js):

// Geçiş öncesi tipik bir Node.js Lambda handler'ı
exports.handler = async (event) => {
    try {
        const request = JSON.parse(event.body);
        
        // Payment data validasyonu (karmaşık business rules)
        const validation = await validatePaymentRequest(request);
        if (!validation.isValid) {
            return errorResponse(400, validation.errors);
        }
        
        // External service üzerinden ödeme işleme
        const result = await paymentProvider.processPayment(request);
        
        // Audit log ve metrics
        await Promise.all([
            auditLogger.log('payment_processed', result),
            metrics.increment('payments.success')
        ]);
        
        return successResponse(result);
    } catch (error) {
        logger.error('Payment processing failed', error);
        return errorResponse(500, 'Payment processing unavailable');
    }
};

Bu sürümün runtime’da ödediği bedel: handler çalışmadan önce her require’ı çözüp değerlendiren bir init fazı, V8 heap’ini ve yüklenen bütün modülleri bellekte tutan bir resident set ve JSON işleme ile garbage collection’ın ödeme çağrısıyla süre için yarıştığı bir execution.

Sonrası (Go):

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "sync"
    
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

type PaymentRequest struct {
    Amount  int64  `json:"amount" validate:"required,min=1"`
    Currency string `json:"currency" validate:"required,len=3"`
    CardToken string `json:"card_token" validate:"required"`
}

type PaymentResponse struct {
    TransactionID string `json:"transaction_id"`
    Status  string `json:"status"`
    ProcessedAt  int64  `json:"processed_at"`
}

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    var paymentReq PaymentRequest
    
    if err := json.Unmarshal([]byte(request.Body), &paymentReq); err != nil {
        return errorResponse(400, "Invalid JSON"), nil
    }
    
    // Payment data validasyonu (aynı business rules, farklı implementation)
    if err := validatePaymentRequest(&paymentReq); err != nil {
        return errorResponse(400, err.Error()), nil
    }
    
    // External service üzerinden ödeme işleme
    result, err := processPayment(ctx, &paymentReq)
    if err != nil {
        log.Printf("Payment processing failed: %v", err)
        return errorResponse(500, "Payment processing unavailable"), nil
    }
    
    // Audit ve metrics concurrent çalışıyor ama handler onları bekliyor.
    // Lambda, response döndükten sonra execution environment'ı donduruyor;
    // başıboş bırakılan bir goroutine hiç bitmeyebilir.
    var wg sync.WaitGroup
    wg.Add(2)
    
    go func() {
        defer wg.Done()
        if err := auditLogger.Log("payment_processed", result); err != nil {
            log.Printf("Audit logging failed: %v", err)
        }
    }()
    
    go func() {
        defer wg.Done()
        metrics.Increment("payments.success")
    }()
    
    wg.Wait()
    
    responseBody, _ := json.Marshal(PaymentResponse{
        TransactionID: result.ID,
        Status:  result.Status,
        ProcessedAt:  result.Timestamp,
    })
    
    return events.APIGatewayProxyResponse{
        StatusCode: 200,
        Headers: map[string]string{
            "Content-Type": "application/json",
        },
        Body: string(responseBody),
    }, nil
}

func main() {
    lambda.Start(Handler)
}

Go sürümünde değişenler: binary zaten linklenmiş durumda, dolayısıyla init aşamasında modül çözümlemesi yok; bellekte kalan alan interpreter heap’i değil binary ile onun allocation’ları; süre de meşgul bir heap üzerindeki GC duraklamaları yerine encoding/json ile sağlayıcı çağrısına gidiyor.

Bu kazancın büyüklüğü, business işi ile runtime overhead arasındaki kendi dağılımına bağlı. Süresinin çoğunu ödeme sağlayıcısını beklemekle geçiren bir handler hiçbir dilde kayda değer şekilde hızlanmaz. İki faktörün de hareket edeceğini varsaymadan önce mevcut fonksiyonun billed duration ve max memory used değerlerini oku.

Açık Bir Benchmark Neyi Ölçüyor#

Bu üç değişiklikten ilk ikisine sayı koyan bir kaynak var: maxday/lambda-perf projesi. Proje her Lambda runtime’ı için bir hello-world fonksiyonunu her gün sıfırdan deploy ediyor, on kez cold start olarak çağırıyor ve CloudWatch REPORT satırındaki Init Duration, Duration ile Max Memory Used değerlerini günlük JSON dosyaları hâlinde yayımlıyor. Dolayısıyla yayımladığı her rakam bir cold çağrıyı anlatıyor; proje warm yolu hiç ölçmüyor. 15 Temmuz - 13 Ağustos 2026 arasında yayımlanan 29 günlük dosyanın medyanları, konfigürasyon başına yaklaşık 280 cold start, us-east-1’de x86_64 ve zip paketleme:

RuntimeMedyan init süresiMedyan max memory used
provided.al2023 üzerinde Go49,7 ms16,0 - 17,9 MB
nodejs20.x139,7 - 142,0 ms66 - 68 MB
nodejs22.x138,9 - 140,1 ms73 - 75 MB
nodejs24.x131,3 - 134,2 ms75 - 79,2 MB

Bu benchmark’ta Go’nun init’i kabaca 2,7 ila 2,9 kat daha hızlı; mutlak fark yaklaşık 90 ms. Aynı proje arm64’te Go’yu 42,2 - 43,1 ms, Node.js 24’ü 120,6 - 124,7 ms ölçüyor; orada fark yaklaşık 80 ms’ye iniyor.

Bu fark bir taban değil, bağımlılıksız bir başlangıç ölçümü; aradaki ayrım önemli. Ölçülen fonksiyonun hiç bağımlılığı yok; runtime bootstrap’ini modül çözümlemesinden ayıran şey de tam olarak bu. Yani elimizdeki sayı, iki handler da tek bir SDK client’ı yüklemeden önceki fark. Bağımlılıklar girdiğinde iki taraf da hareket eder. @aws-sdk import eden bir Node.js handler’ı o modülleri çözmenin ve değerlendirmenin bedelini öder; bundle etmek, tree-shaking ve tembel yükleme bu bedelin bir kısmını geri alır. Aynı SDK’yı linkleyen bir Go binary’si ise daha büyük bir çalıştırılabilir dosya taşır ve handler’dan önce paket init’lerini çalıştırır. Hangi tarafın daha çok hareket ettiği, kendi gerçek bağımlılık setini iki runtime’da da ölçen bir benchmark’ın sorusu.

Asıl ilginç sütun bellek sütunu ve orada bir kayma var. Bu veride Node’un bellek tabanı her sürümle büyüyor: Node.js 20’de 66 - 68 MB, Node.js 24’te 75 - 79,2 MB. Go 18 MB’ın altında kalıyor.

Init rakamını benchmark’a özgü kabul et. K-I-Soft’un eu-central-2’de arm64 üzerinde, konfigürasyon başına 50 cold start ile yaptığı bağımsız ölçüm, Node.js 24 init’ini 1024 MB’de yaklaşık 316 ms buluyor; lambda-perf’in arm64 medyanının kabaca 2,6 katı. O ölçüm Go’yu kapsamadığı için karşılaştırmayı tek başına taşıyamaz ama init süresinin bölgeye, payload’a ve ölçüm düzeneğine göre oynadığını gösteriyor. Sıralama sağlam, milisaniye değeri değil.

Bellek Ayarı Aynı Zamanda CPU Ayarıdır#

O bellek sütunundan çıkan ilk sonuç, Go ile fonksiyonu daha küçük konfigüre edebileceğin olur. Öyle değil. AWS’in bellek belgeleri aralığı 128 MB ile 10.240 MB arasında, 1 MB’lık adımlarla tanımlıyor; Lambda da kullandığın belleği değil ayarladığın belleği faturalandırıyor. Dolayısıyla 17 MB’lık bir Go fonksiyonu da 75 MB’lık bir Node.js fonksiyonu da aynı 128 MB tabanında oturuyor. 58 MB’lık fark ancak business logic allocation’ları Node.js fonksiyonunu, Go fonksiyonunun altında kaldığı bir ayarın üstüne ittiğinde faturaya yansıyor.

Karşılığını veren mekanizma aynı belgede. AWS, CPU gücünü ayarlanan bellekle orantılı dağıttığını ve 1.769 MB’de bir fonksiyonun bir vCPU’ya denk geldiğini yazıyor. Bellek, CPU kolunun kendisi; bu kolun her runtime’a maliyetini de lambda-perf’in aynı cold çağrılarda kaydettiği süreler gösteriyor. Go hello-world handler’ını 1,45 - 1,61 ms’de çalıştırıyor ve 128 MB’den 1024 MB’ye kadar düz gidiyor, çünkü CPU beklemiyor. Node.js düz gitmiyor: nodejs22.x 128 MB’de 12,10 ms, 256 MB’de 3,17 ms; nodejs24.x aynı adımda 10,54 ms ve 2,86 ms. Bellek ayarını ikiye katlamak Node’un süresini yaklaşık 3,7 - 3,8 kat kısaltıyor, Go tarafında ölçülebilir hiçbir şey yapmıyor.

Go’nun avantajı, rekabetçi bir süreye ulaşmak için bellek kolundan CPU satın almaya ihtiyaç duymaması; bir ayara bağlanmadan önce iki ucu da ölç.

Node.js belleğini handler içinden okumak:

// Memory kullanımını gerçekten monitor etmenin ortaya koyduğu
const memoryBefore = process.memoryUsage();
await processBusinessLogic();
const memoryAfter = process.memoryUsage();

console.log({
    heapUsed: (memoryAfter.heapUsed - memoryBefore.heapUsed) / 1024 / 1024,
    external: (memoryAfter.external - memoryBefore.external) / 1024 / 1024,
    // V8 ve yüklenen her modül, senin kodun ilk objesini
    // allocate etmeden önce bellekte hazır duruyor
    overhead: 'runtime + kütüphane baseline; her deployment için ölç'
});

Go belleğini handler içinden okumak:

// Go'nun memory hikayesi çok daha öngörülebilir
func trackMemoryUsage() {
    var m1, m2 runtime.MemStats
    
    runtime.ReadMemStats(&m1)
    processBusinessLogic()
    runtime.ReadMemStats(&m2)
    
    fmt.Printf("Operasyon için allocated memory: %d KB\n", 
        (m2.Alloc-m1.Alloc)/1024)
    fmt.Printf("Total system memory: %d KB\n", m2.Sys/1024)
    
    // m.Sys tüm process'in ayak izi; Lambda'nın CloudWatch'a yazdığı
    // Max Memory Used satırıyla karşılaştır
}

Yerel profil çıkarmak allocation’ların nereden geldiğini söyler. Lambda’nın CloudWatch’a yazdığı Max Memory Used satırı ise faturaya ulaşan tek şeyi söyler: ayarlanan değerin aşağı inip inemeyeceğini.

Cold Start Rakamlarının Anlatmadığı#

Cold startlar herkesin konuştuğu serverless performans konusu, ama gerçeklik “Go daha hızlı başlar”dan daha nuanslı.

Cold Start Süresi Nereye Gidiyor#

Cold start sırasında gerçekte ne oluyor:

  1. Lambda initialization: Container oluşturma ve runtime kurulum
  2. Application bootstrap: Kodunu ve dependency’leri yükleme
  3. First request handling: Gerçek business logic’in

Node.js Cold Start Anatomisi:

// Bu cold start sırasında oluyor, handler çalışmadan önce
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const mongoose = require('mongoose');
const customBusinessLogic = require('./src/business');

// Her require hem dosya sistemi I/O'su hem de evaluation demek ve
// hepsi Lambda'nın Init Duration olarak raporladığı init fazında geçer.
// Baseline'ın bir blog benchmark'ı değil, o log satırıdır.

Go Cold Start Gerçeği:

// Bağımlılıklar başlangıçta değil, build sırasında çözülüyor
import (
    "context"
    "database/sql"
    "github.com/aws/aws-lambda-go/lambda"
    // Bunların hepsi deploy edilen binary'ye linkleniyor
)

// Init aşaması container kurulumu artı process başlatmadan ibaret.
// Runtime'da çözülecek bağımlılık kalmıyor; Go'nun cold start
// avantajının tamamı bu yokluktan geliyor.

Cold Start Ne Zaman Gerçekten Önemli#

Cold start optimizasyonu yalnızca belirli use case’lerde kendini amorti eder:

Yüksek-impact senaryolar:

  • Kullanıcıya dönük API’ler strict SLA gereksinimleriyle (<100ms p95)
  • Event-driven mimariler bursty traffic paternleriyle
  • Maliyet-hassas workload’lar her milisaniyelik faturayı etkilediği yerler

Düşük-impact senaryolar:

  • Background processing 200ms vs 50ms’in kullanıcı deneyimini etkilemediği
  • Yüksek-frekans API’ler Lambda container’larının warm kaldığı
  • Internal API’ler gevşek performans gereksinimli

Go Geçişinin İnsan Tarafı#

Teknik geçiş çoğu zaman insan tarafındaki geçişten daha kolaydır. Takımları bu sürece sokarken işe yarayan yaklaşımlar şunlar:

Strangler Fig ile Geçiş#

Doğru ilk servisi seçmek

En kritik servisinle başlama. En basit servisinle de başlama. Şu özelliklere sahip bir şey seç:

  • Net, iyi tanımlanmış API boundary’leri
  • Orta düzey karmaşıklık (trivial değil, mission-critical değil)
  • Ölçebileceğin ve geliştirebileceğin performans darboğazı
  • Öğrenmeye istekli küçük, motive takım

İyi bir ilk migration hedefi: JWT validasyon ve kullanıcı lookup’ı yapan bir authentication servisi. Girdileri ve çıktıları net, performans etkisi tek bir dashboard’da görünüyor ve geçiş sürerken sistemin geri kalanında hiçbir şeyin değişmesi gerekmiyor.

// İlk migration hedefi: tek fonksiyon, tek imza, tek kütüphane
func ValidateJWT(ctx context.Context, tokenString string) (*UserClaims, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return jwtSecret, nil
    })
    
    if err != nil {
        return nil, fmt.Errorf("invalid token: %w", err)
    }
    
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        return mapClaimsToUser(claims), nil
    }
    
    return nil, fmt.Errorf("invalid token claims")
}

// Migration yüzeyi bu kadar küçük: imza, token kütüphanesi ve claim
// eşlemesi. Yukarıdaki her şey aynı API'yi çağırmaya devam ediyor.

Takım güvenini inşa etmek

En başarılı migrationlar kasıtlı takım güven-inşa etme içerir:

  1. Pair programming session’ları Go deneyimli mühendislerle
  2. Code review kültürü öğrenme odaklı
  3. Internal dökümantasyon yaygın pattern’lar ve gotcha’lar için
  4. Lunch and learn session’ları migration kazanımları ve derslerini paylaşma

Pattern’i ölçeklendirmek

Takım rahat olduktan sonra, sonraki migration adaylarını belirle:

  • Başarılı ilk migrationa benzer servisler
  • İyileştirmenin görünür olacağı performans darboğazları
  • Zaten büyük değişikliklerin planlandığı servisler

Error Handling Kültür Değişimi#

En büyük takım zorluklarından biri Go’nun explicit error handling’i. Node.js try/catch pattern’larından gelince bu mindset değişimi gerektiriyor.

Node.js error handling paternleri:

// Takımın alışık olduğu
const processOrder = async (orderId) => {
  try {
    const order = await getOrder(orderId);
    const payment = await processPayment(order.paymentInfo);
    const fulfillment = await createFulfillment(order.items);
    
    return { success: true, orderId, fulfillmentId: fulfillment.id };
  } catch (error) {
    // Generic error handling
    logger.error('Order processing failed', error);
    throw new Error('Order processing unavailable');
  }
};

Go error handling adaptasyonu:

// Takımın öğrenmesi gereken
func ProcessOrder(orderID string) (*OrderResult, error) {
    order, err := getOrder(orderID)
    if err != nil {
        return nil, fmt.Errorf("failed to retrieve order %s: %w", orderID, err)
    }
    
    payment, err := processPayment(order.PaymentInfo)
    if err != nil {
        return nil, fmt.Errorf("payment processing failed for order %s: %w", orderID, err)
    }
    
    fulfillment, err := createFulfillment(order.Items)
    if err != nil {
        // Belki fulfillment hatası recover edilebilir?
        log.Printf("Fulfillment creation failed for order %s: %v", orderID, err)
        // Business karar: devam et mi yoksa fail mi?
        return nil, fmt.Errorf("fulfillment creation failed for order %s: %w", orderID, err)
    }
    
    return &OrderResult{
        Success:  true,
        OrderID:  orderID,
        FulfillmentID: fulfillment.ID,
    }, nil
}

Pratik etki: Go, en iyisini umup error’ları generic biçimde handle etmek yerine her adımda neyin yanlış gidebileceğini düşünmeye zorluyor.

Go geliştiricilerinin en çok bildirdiği sürtünme de burası. Go ekibinin 5.379 katılımcıyla yürüttüğü 2025 Go Developer Survey’de en sık dile getirilen zorluk, %33 ile Go idiom’larına ve iyi pratiklerine uygun kod yazmak. Katılımcıların %60’ı da diğer dillerden en çok exception’ları özlediğini söylüyor. Bu rakamlar Go’yu zaten kendisi seçmiş insanlardan geliyor; seçmemiş bir takım için taban kabul et.

Serverless-Specific Go Pattern’ları#

Birkaç Go pattern’ı neredeyse her Lambda kod tabanında hakkını veriyor ve ilk servis taşınmadan önce kurulmayı hak ediyor.

HTTP Handler Abstraction#

İşe yarayan pattern:

// Servisler arasında paylaşılan generic handler wrapper
type HandlerFunc func(ctx context.Context, request *APIRequest) (*APIResponse, error)

type APIRequest struct {
    Body  string
    Headers map[string]string
    Query  map[string]string
    Path  map[string]string
}

type APIResponse struct {
    StatusCode int
    Body  interface{}
    Headers  map[string]string
}

func MakeHandler(handler HandlerFunc) func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    return func(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
        request := &APIRequest{
            Body:  event.Body,
            Headers: event.Headers,
            Query:  event.QueryStringParameters,
            Path:  event.PathParameters,
        }
        
        response, err := handler(ctx, request)
        if err != nil {
            log.Printf("Handler error: %v", err)
            return events.APIGatewayProxyResponse{
                StatusCode: 500,
                Body:  `{"error": "Internal server error"}`,
            }, nil
        }
        
        bodyBytes, _ := json.Marshal(response.Body)
        
        return events.APIGatewayProxyResponse{
            StatusCode: response.StatusCode,
            Body:  string(bodyBytes),
            Headers:  response.Headers,
        }, nil
    }
}

// Kullanım temiz ve test edilebilir hale geliyor
func createUserHandler(ctx context.Context, req *APIRequest) (*APIResponse, error) {
    var user User
    if err := json.Unmarshal([]byte(req.Body), &user); err != nil {
        return &APIResponse{
            StatusCode: 400,
            Body:  map[string]string{"error": "Invalid JSON"},
        }, nil
    }
    
    // Business logic burada...
    
    return &APIResponse{
        StatusCode: 201,
        Body:  user,
    }, nil
}

// Main'de bağlama
func main() {
    lambda.Start(MakeHandler(createUserHandler))
}

Database Connection Pattern’ları#

Serverless Go’da en zor kısımlardan biri database connection management. İşte tutarlı şekilde işe yarayan pattern:

// Serverless için connection management
type DatabaseConnection struct {
    db  *sql.DB
    config DatabaseConfig
    mu  sync.Mutex
}

var dbConn *DatabaseConnection
var dbOnce sync.Once

func GetDB(ctx context.Context) (*sql.DB, error) {
    dbOnce.Do(func() {
        config := DatabaseConfig{
            Host:  os.Getenv("DB_HOST"),
            Username: os.Getenv("DB_USERNAME"),
            Password: os.Getenv("DB_PASSWORD"),
            Database: os.Getenv("DB_NAME"),
        }
        
        dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s", 
            config.Username, config.Password, config.Host, config.Database)
        
        db, err := sql.Open("mysql", dsn)
        if err != nil {
            log.Fatalf("Failed to connect to database: %v", err)
        }
        
        // Serverless-optimized connection pool settings
        db.SetMaxOpenConns(1)  // Lambda container başına tek connection
        db.SetMaxIdleConns(1)  // Invocation'lar arasında connection alive tut
        db.SetConnMaxLifetime(300 * time.Second)  // 5 dakika max connection yaşı
        
        dbConn = &DatabaseConnection{db: db, config: config}
    })
    
    // Her handler invocation'da connection test et
    if err := dbConn.db.PingContext(ctx); err != nil {
        return nil, fmt.Errorf("database connection failed: %w", err)
    }
    
    return dbConn.db, nil
}

Concurrent Processing Pattern’ları#

Go’nun goroutine’leri serverless ortamlarda, özellikle I/O-bound operasyonlar için mükemmel fırsatlar sunuyor:

// Pattern: Concurrent external API çağrıları
func enrichUserProfile(ctx context.Context, userID string) (*EnrichedProfile, error) {
    type result struct {
        data interface{}
        err  error
    }
    
    // Deadline goroutine'ler başlamadan önce var olmalı; aksi halde
    // hepsi orijinal context ile çalışır ve deadline'ı hiç görmez
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()
    
    // Sonuçları toplamak için channel'lar
    profileCh := make(chan result, 1)
    preferencesCh := make(chan result, 1)
    analyticsCh := make(chan result, 1)
    
    // Concurrent operasyonları başlat
    go func() {
        profile, err := fetchUserProfile(ctx, userID)
        profileCh <- result{profile, err}
    }()
    
    go func() {
        prefs, err := fetchUserPreferences(ctx, userID)
        preferencesCh <- result{prefs, err}
    }()
    
    go func() {
        analytics, err := fetchUserAnalytics(ctx, userID)
        analyticsCh <- result{analytics, err}
    }()
    
    // Sonuçları topla
    var profile *UserProfile
    var preferences *UserPreferences  
    var analytics *UserAnalytics
    
    for i := 0; i < 3; i++ {
        select {
        case res := <-profileCh:
            if res.err != nil {
                return nil, fmt.Errorf("profile fetch failed: %w", res.err)
            }
            profile = res.data.(*UserProfile)
            
        case res := <-preferencesCh:
            if res.err != nil {
                log.Printf("Preferences fetch failed: %v", res.err)
                preferences = &DefaultPreferences{} // Graceful degradation
            } else {
                preferences = res.data.(*UserPreferences)
            }
            
        case res := <-analyticsCh:
            if res.err != nil {
                log.Printf("Analytics fetch failed: %v", res.err)
                analytics = &EmptyAnalytics{} // Graceful degradation
            } else {
                analytics = res.data.(*UserAnalytics)
            }
            
        case <-ctx.Done():
            return nil, fmt.Errorf("user enrichment timed out: %w", ctx.Err())
        }
    }
    
    return &EnrichedProfile{
        Profile:  *profile,
        Preferences: *preferences,
        Analytics:  *analytics,
    }, nil
}

Zenginleştirmenin toplam süresi üç çağrının toplamı değil, en yavaşı kadar oluyor ve tek bir deadline hepsini birden kapsıyor. Kazancın tamamı bu. Bu üç çağrı aynı downstream servise gidiyorsa kuyruğu kaldırmış değil, yerini değiştirmiş olursun.

Lambda Gerçekte Neyi Faturalandırıyor#

Lambda iki şeyi faturalandırır: istek başına sabit bir ücret ve GB-saniye cinsinden ölçülen bir süre ücreti. AWS, us-east-1’de on-demand x86 fiyatını 1M istek başına $0.20 ve GB-saniye başına $0.0000166667 olarak listeliyor; süreyi de en yakın 1 ms’ye yukarı yuvarlıyor. Aynı sayfadaki iki ayrıntı çoğu geçiş gerekçesinden düşüyor. GB-saniye ücreti düz değil kademeli: x86’da aylık 6 milyar GB-saniyenin üstünde $0.0000150000’a, 15 milyarın üstünde $0.0000133334’e iniyor. Ve arm64 süre tarafında tam olarak %20 daha ucuz ($0.0000166667’ye karşı $0.0000133334), istek ücreti ise iki mimaride de 1M başına $0.20. Dil değiştirmek yalnızca süre terimini hareket ettirir.

İki ayrıntı da geçiş gerekçesinin aleyhine işliyor. Mevcut bir fonksiyonu arm64 için yeniden derlemek, bir yeniden yazmanın saldırdığı terimin beşte birini siliyor ve bu bir yeniden yazma değil, yeniden derleme. Kademeli fiyat da ölçekte gerekçeyi aşındırıyor: geçişin anlamlı hâle geldiği hacim, senin GB-saniye ücretinin zaten %10 ila %20 düştüğü hacim.

Süre Matematiğini Yapmak#

Invocation başına maliyet, GB cinsinden ayrılan memory çarpı saniye cinsinden faturalanan süre çarpı GB-saniye ücreti. 256 MB’de 120 ms çalışan bir fonksiyon 0.25 × 0.12 × 0.0000166667, yani yaklaşık $0.0000005 tutar. Bu invocation’lardan on milyon tanesi süre tarafında kabaca $5.00, istek ücreti tarafında $2.00 eder.

Go bu çarpımın bir çarpanını, bazen ikisini birden hareket ettirir. Sürenin daha azı parse ile allocation’a gider; yalnızca CPU almak için yüksek ayarlanmış bir fonksiyon da aşağı inebilir. Memory’yi yarıya, süreyi yarıya indirmek süre terimini dörde böler.

Bu aritmetikten iki sonuç çıkar. Birincisi, bu birim fiyatlarda düşük trafikli bir servisin maliyeti o kadar küçüktür ki hiçbir dil değişikliği kendini amorti edemez; aksini varsaymadan önce çarpımı kendi invocation sayınla yap. İkincisi, istek ücreti hiç değişmez; çok kısa sürelerde faturaya o hâkim olmaya başlar ve handler ne kadar hızlanırsa hızlansın tasarrufu sınırlar.

Dil Değişikliği Gerçekte Ne Kadar Kazandırır#

Bu tavanı sayıya dökmek gerekiyor; nerede bağladığı da çağrının cold olup olmamasına bağlı. lambda-perf’in medyanlarını, yukarıdaki liste fiyatlarını ve AWS’in 1 ms yuvarlamasını al; x86’da ilk kademe ücretiyle, önemsiz bir handler’ın bilerek yapılan bir milyon cold çağrısı için. Cold olması, init fazının iki runtime’da da faturalanması demek; dolayısıyla o da toplama giriyor:

  • 128 MB’de Go: 49,7 ms init artı 1,52 ms handler, 52 ms olarak faturalanır; yani 0.125 GB × 0.052 s × 1M = 6500 GB-saniye, süre tarafında $0.11.
  • 256 MB’de Node.js 24: 131,3 ms init artı 2,86 ms handler, 135 ms olarak faturalanır; yani 0.25 × 0.135 × 1M = 33750 GB-saniye, yani $0.56. Bu rakam tablodaki init aralığının alt ucunu alıyor; üst uç yaklaşık bir sent ekliyor. Aynı fonksiyon 128 MB’de handler’ı 2,86 ms yerine 10,54 ms’de çalıştırır; yani fonksiyon ısındıktan sonra ucuz olan ayar büyük olanı.

İki runtime’ın da dokunamadığı $0.20’lik istek ücretini ekle: cold yolun faturası $0.31’e karşı $0.76, yani Node kabaca Go’nun iki buçuk katı. Bu farkın neredeyse tamamı init. Init’i çıkarınca geriye kalan handler terimleri $0.0042’ye karşı $0.0125; tam olarak üçte biri ve bir milyon çağrıda aralarında bir sentten az fark var. Normal trafik çoğunlukla ikinci çifti öder. Bu benchmark’ın kaydettiği tek şey cold çağrılar; production’da geçen bir ayın geri kalanı warm çalışır. Node’un handler süresi de JIT devreye girdikten sonra düşer; gerçek bir ayı fiyatlamak, iki runtime’ı da yeniden kullanılan bir execution environment’a arka arkaya çağırarak ölçmek demek. Orada ayakta kalan kalem istek ücreti ve içinde hiç business logic olmayan bir handler için warm faturanın büyük kısmı zaten o.

Bu warm yolda yuvarlama ciddi iş görüyor. Go’nun 1,52 ms’si 2 ms olarak faturalanıyor, yani ölçülen sürenin üstüne %32 biniyor; süresi zaten 1 ms’ye yakın olan bir fonksiyonun kazanacak neredeyse hiçbir şeyi kalmamıştır.

Cold Start’ın Maliyeti#

Init farkı gecikme için optimize etmeye değer ama fatura onu neredeyse hiç fark etmiyor. AWS, cold start’ların tipik olarak çağrıların %1’inden azında görüldüğünü ve bir cold start’ın süresinin 100 ms’nin altından 1 saniyenin üstüne kadar değiştiğini yazıyor. Yukarıdaki tablodan gelen medyan x86 init farkını, yani 89,8 ms’yi, 256 MB’lik bir fonksiyona uygula: 0.25 × 0.0898 × 0.0000166667, cold start başına yaklaşık $0.00000037. Yukarıdaki örnekteki on milyon çağrının yüzde birinde bu 100.000 cold start ve kabaca $0.04 eder.

Bu tarafta bir şey değişti. AWS, 1 Ağustos 2025 itibarıyla INIT faturalandırmasını standartlaştırdı: INIT fazı artık bütün konfigürasyon tiplerinde faturalanıyor ve managed runtime kullanan ZIP fonksiyonların on-demand çağrılarında INIT süresi Billed Duration’a dahil ediliyor. Custom ve OS-only runtime’lar INIT’i zaten ödüyordu. Go provided ailesinde, Node.js ise managed bir runtime’da çalıştığı için bu değişiklik Node.js’in sahip olduğu, Go’nun hiç sahip olmadığı bir sübvansiyonu kaldırdı. Karşılaştırmada gerçek bir kayma ve yukarıdaki rakamlarla on milyon çağrı başına dört sent değerinde.

Migration’ın Gizli Maliyetleri#

Migration’ın bedeli mühendislik zamanıyla ödenir: mühendis başına öğrenme eğrisi, servis başına yeniden yazma ve test süresi, bir de kimsenin bütçelemediği dökümantasyon. Bu saatleri tahmin et, kendi saatlik maliyetinle çarp ve az önce hesapladığın aylık süre tasarrufuyla karşılaştır. Break-even birkaç çeyrekten uzağa düşüyorsa geçiş maliyetten başka bir gerekçeyle savunuluyor demektir; o gerekçeyi açıkça adlandırmakta fayda var.

Go Migrationları Nerede Başarısız Olur#

Her geçiş başarılı olmaz. Birkaç başarısızlık deseni, adlandırılmayı hak edecek kadar sık tekrarlanıyor.

Tek Seferde Yeniden Yazmalar#

Karmaşık business rule’ları ve bir düzine dış sistemle entegrasyonu olan olgun bir Node.js servisi, “performans kazanımı büyük olacak” gerekçesiyle tek sprint’lik bir yeniden yazmaya sokulur.

Takvimi kaydıran şey genelde Go değil, mevcut kodun içinde duran dökümante edilmemiş davranıştır: özel durumlar, toleranslı parse etme, her entegrasyonun ihtiyaç duyduğu retry alışkanlıkları. Bunların hiçbiri tip imzalarında görünmez; production’da teker teker bug olarak keşfedilir, üstelik takım aynı anda yeni bir error handling tarzı öğreniyorken. Kayan haftalar dile değil business rule’lara gider.

Yayımlanmış araştırma da aynı yönü gösteriyor ve buradaki zeminin ne kadar ince olduğunu bilmekte fayda var. Berger, Hollenbeck, Maj, Vitek ve Vitek, programlama dillerini hata oranlarına bağlayan en çok atıf almış çalışmayı yeniden üretti ve yalnızca dört dilin hatalarla istatistiksel olarak anlamlı bir ilişki gösterdiğini, o dillerde bile etki büyüklüğünün son derece küçük kaldığını raporladı. Orijinal yazarlar bir cevap yayımladı, onlara da cevap verildi; yani konu kapanmış değil, tartışmalı. Her iki okuma da aynı pratik sonuca çıkıyor: bir dil değişikliğinin hata oranını ne kadar oynattığına dair savunulabilir bir sayı kimsede yok, dolayısıyla bunu bütçesine koyan bir geçiş planı bir tahmini bütçeliyor demektir.

Sınır: oturmuş pattern’ları olan karmaşık business logic, ilk migration adayı olarak kötü bir seçimdir. Takım Go ile daha küçük bir şey teslim etmeden ve servisin riski haklı çıkaran bir maliyet ya da gecikme problemi olmadan taşınmamalı.

Zaten Maliyeti Olmayan Bir Servisi Optimize Etmek#

Günde belki 1000 istek gören, istek başına ortalama 200ms süren düşük trafikli bir admin API’si, genellikle “Go öğrenmek için yeterince basit” gerekçesiyle migration için seçilir.

Kabul etmeden önce hesabı yap. Ayda otuz bin çağrı, çağrı başına 200 ms, 256 MB’de istek ücreti dahil yaklaşık $0.03 tutar; süreyi %70 kısaltmak da haftalarca mühendislik zamanına karşılık ayda yaklaşık $0.02 tasarruf demektir. Bellek ayarını yükseltmek gerekçeyi kurtarmıyor: aynı trafik Lambda’nın üst sınırı olan 10.240 MB’de bile ayda yaklaşık $1.01 fatura eder. Migration kararlarını ölçülmüş problemler belirler; yani maliyet, gecikme veya güvenilirlik; Go öğrenmek kendi başına bütçelenmeye değer: bunu bir yan proje, production yeniden yazmasından çok daha ucuza satın alır.

Yukarıdan Dayatma#

Geçiş, takım ortada çözülecek bir problem olduğu konusunda hemfikir olmadan yukarıdan zorunlu kılınır.

Öngörülebilir sonuç şu: code review bir öğretim kuyruğuna dönüşür, Node.js servislerini yazan mühendisler dayatmayı kendi işleri hakkında verilmiş bir hüküm olarak okur ve Go’ya en uzak olanlar teslimat riskinin en büyük kısmını üstlenir. Bunların hiçbiri migration planında görünmez; hepsi takvimde görünür.

Bir dil geçişi, sonucun bakımını üstlenecek insanların onu istemesini gerektirir.

Takım Bulunabilirliği de Maliyetin Parçası#

Takım bulunabilirliği geçişin maliyetinin bir parçasıdır ve bu genelde tahmine hiç girmez. 2025 Stack Overflow Developer Survey’de katılımcıların %16,4’ü Go kullanıyor; Node.js için bu oran %48,7, JavaScript için %66. Yani Go ile yazılmış bir servis, yazarına ulaşılamadığında onu devralabilecek insan kümesini daraltıyor. Bu daralmayı, servisi Go’ya bağlamadan önce performans probleminin ağırlığıyla tart.

Pratik Migration Checklist#

Go migration’ı ile devam etmeye karar verdiysen, taktiksel checklist şöyle:

Başlamadan Önce#

Takım Hazırlığı:

  • Takımdaki Go championlarını belirle
  • Go tour ve temel Lambda tutorial’larını tamamla
  • Development ortamı ve tooling setup et
  • Internal dökümantasyon template’leri oluştur

Servis Analizi:

  • Mevcut servis performans baseline’ını dökümante et
  • Tüm external dependency’leri ve entegrasyonları belirle
  • Business logic karmaşıklığını haritalandır
  • Migration phase’lerini planla (hangi component’ler önce)

Infrastructure Hazırlığı:

  • Go servisleri için ayrı deployment pipeline setup et
  • Yeni servis için monitoring ve alerting konfigüre et
  • Rollback stratejileri ve feature flag’leri planla

Geçiş Sırasında#

Foundation:

  • Temel Go Lambda yapısını setup et
  • Core request/response handling’i implemente et
  • Temel error handling pattern’larını ekle
  • İlk unit test’leri yaz

Business Logic:

  • Business logic fonksiyonlarını port et
  • External servis entegrasyonlarını implemente et
  • Kapsamlı error handling ekle
  • Integration test’leri oluştur

Validasyon ve Deployment:

  • Performance testing ve karşılaştırma
  • Security review ve penetration testing
  • Dökümantasyon güncellemeleri
  • Gradual traffic shifting

Optimizasyon ve Monitoring:

  • Production verilerine göre performance tuning
  • Error handling iyileştirmeleri
  • Monitoring dashboard setup
  • Takım retrospektifi ve çıkarımlar

Geçişten Sonra#

Geçişin hemen ardından:

  • Performance metriklerinin günlük monitor edilmesi
  • Go deneyimi üzerine haftalık takım check-in’leri
  • Herhangi production sorununa hızlı response
  • Öğrenilenlere göre dökümantasyon güncellemeleri

Devam Eden:

  • Öğrenilenleri diğer takımlarla paylaş
  • Deneyime göre migration kılavuzlarını güncelle
  • Sonraki migration adaylarını planla
  • Maliyet/performans iyileştirmelerini ölç ve rapor et

Monitoring ve Observability Farkları#

Sık gözden kaçan bir konu var: serverless ortamlarda Node.js’den Go’ya geçerken monitoring’in kendisi de değişiyor.

Monitoring Pattern’ları#

Tipik bir Node.js Lambda’sının logladıkları:

// Lambda'da standart Node.js monitoring
const middy = require('@middy/core');
const httpEventNormalizer = require('@middy/http-event-normalizer');

const handler = middy(async (event) => {
    const start = Date.now();
    
    // Business logic burada
    const result = await processBusinessLogic(event);
    
    const duration = Date.now() - start;
    console.log(JSON.stringify({
        requestId: event.requestContext.requestId,
        duration,
        memoryUsed: process.memoryUsage().heapUsed,
        statusCode: result.statusCode
    }));
    
    return result;
});

// Middleware çoğu observability concern'ini handle ediyordu
handler.use(httpEventNormalizer());

Go handler’ı aynı alanları elle toplamak zorunda, çünkü bunu örtük şekilde yapan bir middleware katmanı yok:

Go monitoring nasıl görünüyor:

package main

import (
    "context"
    "encoding/json"
    "log"
    "runtime"
    "time"
    
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-lambda-go/lambdacontext"
)

type RequestMetrics struct {
    RequestID  string  `json:"request_id"`
    Duration  time.Duration `json:"duration_ms"`
    MemoryUsed  uint64  `json:"memory_used_kb"`
    StatusCode  int  `json:"status_code"`
    Goroutines  int  `json:"goroutines"`
}

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    start := time.Now()
    
    // Request ID için Lambda context al
    lc, _ := lambdacontext.FromContext(ctx)
    
    // Business logic burada
    result, err := processBusinessLogic(ctx, request)
    if err != nil {
        log.Printf("Business logic error: %v", err)
        result = events.APIGatewayProxyResponse{
            StatusCode: 500,
            Body:  `{"error": "Internal server error"}`,
        }
    }
    
    // Metrikleri topla
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    metrics := RequestMetrics{
        RequestID:  lc.AwsRequestID,
        Duration:  time.Since(start),
        MemoryUsed: m.Alloc / 1024,
        StatusCode: result.StatusCode,
        Goroutines: runtime.NumGoroutine(),
    }
    
    // CloudWatch parsing için structured metrikler log et
    metricsJSON, _ := json.Marshal(metrics)
    log.Printf("REQUEST_METRICS: %s", metricsJSON)
    
    return result, nil
}

func main() {
    lambda.Start(Handler)
}

Önemli Custom Metrikler#

İzlenmeye değer Go-spesifik metrikler:

// Memory usage pattern'ları Go'da farklı
func logMemoryMetrics() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    log.Printf("MEMORY_METRICS: %s", toJSON(map[string]interface{}{
        "allocated_kb":  m.Alloc / 1024,
        "total_alloc_kb":  m.TotalAlloc / 1024,
        "system_kb":  m.Sys / 1024,
        "gc_runs":  m.NumGC,
        "gc_pause_ns":  m.PauseNs[(m.NumGC+255)%256],
    }))
}

// Concurrent operasyonlar için goroutine tracking  
func logGoroutineMetrics() {
    log.Printf("GOROUTINE_METRICS: %s", toJSON(map[string]interface{}{
        "active_goroutines": runtime.NumGoroutine(),
        "max_procs":  runtime.GOMAXPROCS(0),
    }))
}

// Cold start detection
var startTime = time.Now()

func detectColdStart() bool {
    return time.Since(startTime) < 100*time.Millisecond
}

Alerting Farkları#

Farklı şekilde alert alınması gerekenler:

Node.js’te tipik alert’ler:

  • Ayrılan memory’nin >%80’i kullanılıyor
  • Response time p95 >200ms
  • Error rate >%1

Go’ya özgü alert’ler:

  • Ayrılan memory’nin >%60’ı kullanılıyor (Go belleği daha verimli kullanır)
  • GC pause time >10ms (memory pressure göstergesi)
  • Cold starts >%5 of requests (Go bunu çok daha düşük tutmalı)
  • Goroutine leaks (zaman içinde artan goroutine sayısı)

Sonraki Migration için Dersler#

Node.js’den Go geçişlerinde ortaya çıkan pattern’lar ve bir sonraki sefer farklı yapılacaklar.

Uzun Vadede Ayakta Kalanlar#

Hangi servisler taşınmış halde kalıyor:

  • Yüksek-volume, düşük-karmaşıklık API’ler (authentication, data validation)
  • CPU-yoğun processing fonksiyonları (image resizing, data transformation)
  • Maliyet-hassas background job’ları (batch processing, scheduled task’ler)
  • Net performans gereksinimleri ve SLA’ları olan servisler

Hangi takımlar iyi adapte oluyor:

  • Küçük, motive takımlar (3-8 mühendis)
  • Dedicated learning time ve management desteği olan takımlar
  • Basit migrationlarla başlayıp güven inşa eden takımlar
  • Net performans/maliyet baskılarının değişimi yönlendirdiği organizasyonlar

Bir Sonraki Sefer Farklı Yapılacaklar#

İlk denemeden daha küçük başla: tek fonksiyonluk bir Lambda servisi, multi-endpoint API’ler pattern’lar oturana kadar beklesin. Shared kütüphanelere, monitoring pattern’larına ve deployment pipeline’larına yatırımı bir production servisi taşınmadan önce yap.

Başlamadan önce baseline performansı, maliyetleri ve takım velocity’sini ölç; iyileştirmeleri izlenimle değil sayıyla takip et. Her migration’ın 24 saat içinde execute edilebilecek bir rollback planı olmalı, üstelik ilk servis taşınmadan önce yazılmış olmalı.

Stratejik Görüş#

Serverless’ta Go, platform standardı değil servis bazında verilen bir karardır. Sağlıklı organizasyonlar genellikle ikisini birden çalıştırır:

  • Go servisleri: Yüksek-performans, maliyet-hassas, stabil business logic
  • Node.js servisleri: Hızlı iteration, karmaşık entegrasyonlar, sık değişiklikler

Varsayılan ve Ne Zaman Esnetilir#

Serverless ortamlarda Node.js’den Go’ya geçmeyi düşünüyorsan, şu sorularla başla:

  1. Go’nun çözdüğü spesifik problemin var mı? (maliyet, performans, memory kullanımı)
  2. Takımın learning yatırımına hazır mı? (zaman, isteklilik, management desteği)
  3. Küçük başlayıp güven inşa edebilir misin? (basit servis, net başarı metrikleri)
  4. İşler yanlış giderse rollback planların var mı? (feature flag’ler, deployment stratejileri)

Bu soruların sırası önemli. Birincisine verilen “hayır”, diğer üçünden bağımsız olarak tartışmayı bitirir: ölçülmüş bir maliyet, gecikme veya güvenilirlik problemi yoksa geçiş, adı güzelleştirilmiş bir yeniden yazmadan ibarettir. Dördüne de “evet” diyorsan varsayılan şu: mantığı basit, trafiği yüksek tek bir servisi taşı, gerisini Node.js’de bırak ve o servisin arkasında bir haftalık production verisi birikince süre matematiğini yeniden çalıştır.

Kısıt aslında performans değilse varsayılanı es geç. Başka yerde zaten Go yazan bir takım çok daha küçük bir öğrenme bedeli öder; bu da ikinci ve üçüncü servisin eşiğini düşürür. Kadrosunda Go olmayan ve maliyet baskısı da bulunmayan bir takım aynı sorulara “henüz değil” cevabını vermeli.

Kaynaklar#

İlgili yazılar