Optimizing Golang API Performance: Lessons from Production

2 min read
#golang #performance #api #optimization

Setelah beberapa tahun bekerja dengan Golang di production environment, saya ingin berbagi beberapa teknik optimasi API yang terbukti efektif meningkatkan performa aplikasi.

1. Database Query Optimization

Salah satu bottleneck terbesar biasanya ada di database queries. Beberapa hal yang saya lakukan:

N+1 Query Problem

// ❌ Bad: N+1 queries
func GetUsersWithOrders(db *sql.DB) ([]*User, error) {
    users, _ := db.Query("SELECT * FROM users")
    for _, user := range users {
        orders, _ := db.Query("SELECT * FROM orders WHERE user_id = ?", user.ID)
        user.Orders = orders
    }
    return users, nil
}

// ✅ Good: Single query with JOIN
func GetUsersWithOrders(db *sql.DB) ([]*User, error) {
    query := `
        SELECT u.*, o.id, o.amount
        FROM users u
        LEFT JOIN orders o ON u.id = o.user_id
    `
    rows, _ := db.Query(query)
    // Map results efficiently
    return users, nil
}

Use Prepared Statements

Prepared statements mengurangi parsing overhead dan meningkatkan security:

stmt, err := db.Prepare("SELECT * FROM users WHERE id = ?")
defer stmt.Close()

for _, id := range userIDs {
    row := stmt.QueryRow(id)
    // process row
}

2. Caching Strategy

Implementasi Redis caching untuk data yang sering diakses:

func GetUser(ctx context.Context, userID int) (*User, error) {
    // Try cache first
    cacheKey := fmt.Sprintf("user:%d", userID)

    if cached, err := redis.Get(ctx, cacheKey).Result(); err == nil {
        var user User
        json.Unmarshal([]byte(cached), &user)
        return &user, nil
    }

    // Cache miss, get from DB
    user, err := db.GetUserByID(ctx, userID)
    if err != nil {
        return nil, err
    }

    // Set cache with TTL
    data, _ := json.Marshal(user)
    redis.Set(ctx, cacheKey, data, 5*time.Minute)

    return user, nil
}

3. Connection Pooling

Konfigurasi connection pool yang tepat sangat penting:

db.SetMaxOpenConns(25)           // Maximum connections
db.SetMaxIdleConns(5)            // Idle connections
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)

4. Profiling dengan pprof

Selalu gunakan pprof untuk identify bottlenecks:

import _ "net/http/pprof"

go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

Akses via: go tool pprof http://localhost:6060/debug/pprof/profile

5. Use Context for Timeout

Implementasi timeout untuk prevent hanging requests:

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()

    result, err := fetchDataWithContext(ctx)
    if err == context.DeadlineExceeded {
        http.Error(w, "Request timeout", http.StatusGatewayTimeout)
        return
    }

    json.NewEncoder(w).Encode(result)
}

Hasil

Dengan implementasi teknik-teknik di atas, saya berhasil:

  • Mengurangi response time dari ~800ms menjadi ~120ms
  • Meningkatkan throughput dari 500 req/s menjadi 2000 req/s
  • Mengurangi database load hingga 60%

Kesimpulan

Optimasi API bukan hanya tentang code yang cepat, tapi juga tentang memahami bottleneck di system Anda. Selalu measure, identify, dan optimize berdasarkan data real.

Happy coding! 🚀

Share this article

AS

Arian Saputra

Backend Engineer with 6+ years across BPM & distributed systems. Go, microservices, CI/CD, performance tuning.