DEV Community

Kittipat.po
Kittipat.po

Posted on

In-Memory Caching in Go: Boosting Performance and Efficiency

Image description

In-memory caching is one of the most effective techniques to enhance performance and reduce latency in your Go applications. By storing frequently accessed data in memory, you can minimize expensive operations like database queries or API calls. In this blog post, we'll explore how you can use localcache, an in-memory cache implementation, in your Go projects and look at some common use cases.

Why In-Memory Caching?

In-memory caching stores data in the RAM, which means it's incredibly fast. Some of the key advantages are:

  • Faster Data Access: Memory is much faster than disk or network access.
  • Reduced Database/Backend Load: Cache frequently accessed data, so you don’t have to query the database or external APIs repeatedly.
  • Low Latency: It improves the responsiveness of your application by serving cached data instantly. Now, let’s dive into practical use cases using localcache and see how it can benefit your Go applications.

Common Use Cases of In-Memory Caching

1. Caching Expensive Database Queries

Let’s say your application retrieves data from a database. This process can be time-consuming and resource-heavy, especially for queries that don’t change often. Using localcache, you can cache the query results to serve subsequent requests from memory.

package main

import (
    "context"
    "fmt"
    "time"
    "github.com/kittipat1413/go-common/framework/cache/localcache"
)

func main() {
    ctx := context.Background()

    // Create a new cache instance with a default expiration of 5 minutes
    c := localcache.New[string](
        localcache.WithDefaultExpiration(5 * time.Minute),
    )

    // Define a key for the cached data
    key := "user:123"

    // Fetch the user data from the database (simulate)
    initializer := func() (string, *time.Duration, error) {
        // Simulate a database query by returning user data
        fmt.Println("Fetching data from database...")
        duration := 5 * time.Minute
        return "User 123 Data", &duration, nil
    }

    // Get data from cache or initialize if not present
    data, err := c.Get(ctx, key, initializer)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    // Use the cached or fetched data
    fmt.Println("User data:", data)
}
Enter fullscreen mode Exit fullscreen mode

Output:

Fetching data from database...
User data: User 123 Data
Enter fullscreen mode Exit fullscreen mode

If you run the c.Get method again within the expiration time, you will get the cached data without querying the database again, significantly speeding up your application.

2. Caching API Responses

Fetching data from external APIs can also be time-consuming, especially when dealing with slow or rate-limited APIs. By caching API responses, you can avoid repeated network calls and serve users faster.

package main

import (
    "context"
    "fmt"
    "time"
    "github.com/kittipat1413/go-common/framework/cache/localcache"
)


// WeatherData struct to store detailed weather information
type WeatherData struct {
    Condition  string
    Temperature string
    UpdatedAt  time.Time
}

// Fetch weather data from an API (simulated) and return as WeatherData struct
func fetchWeatherAPI() (WeatherData, *time.Duration, error) {
    // Simulate an API call to fetch weather data
    fmt.Println("Fetching weather data from API...")
    data := WeatherData{
        Condition:   "Sunny",
        Temperature: "25°C",
        UpdatedAt:   time.Now(),
    }
    expiration := 10 * time.Minute // Cache for 10 minutes
    return data, &expiration, nil
}

func main() {
    ctx := context.Background()
    cache := localcache.New[WeatherData]() // Create cache for WeatherData type

    // Cache key for the weather API response
    cacheKey := "weather:city:newyork"

    // Get weather data from cache or API
    weather, err := cache.Get(ctx, cacheKey, fetchWeatherAPI)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    // Use the weather data
    fmt.Printf("Weather in New York: %s, Temperature: %s (Updated at: %s)\n",
        weather.Condition, weather.Temperature, weather.UpdatedAt.Format(time.RFC1123))
}
Enter fullscreen mode Exit fullscreen mode

Output:

Fetching weather data from API...
Weather in New York: Sunny, Temperature: 25°C (Updated at: Mon, 30 Sep 2024 12:34:56 UTC)
Enter fullscreen mode Exit fullscreen mode

3. Caching Configuration or Metadata

Applications often load configuration or metadata from files or external services. Instead of loading this data every time it's needed, you can cache it for quick access.

package main

import (
    "context"
    "fmt"
    "time"
    "github.com/kittipat1413/go-common/framework/cache/localcache"
)

// ConfigData struct to store configuration details
type ConfigData struct {
    FeatureEnabled bool
    UpdatedAt      time.Time
}

// Load the config data (simulate loading from a file)
func loadConfig() (ConfigData, *time.Duration, error) {
    fmt.Println("Loading config from file...")
    config := ConfigData{
        FeatureEnabled: true,
        UpdatedAt:      time.Now(),
    }
    expiration := 1 * time.Hour // Cache the config for an hour
    return config, &expiration, nil
}

func main() {
    ctx := context.Background()
    configCache := localcache.New[ConfigData]() // Create cache for ConfigData type

    // Cache key for configuration
    configKey := "app:config"

    // Get config data from cache or file
    config, err := configCache.Get(ctx, configKey, loadConfig)
    if err != nil {
        fmt.Println("Error loading config:", err)
        return
    }

    // Use the config data
    fmt.Printf("Config data: FeatureEnabled=%t, UpdatedAt=%s\n", config.FeatureEnabled, config.UpdatedAt.Format(time.RFC1123))
}
Enter fullscreen mode Exit fullscreen mode

Output:

Loading config from file...
Config data: FeatureEnabled=true, UpdatedAt=Mon, 30 Sep 2024 12:34:56 UTC
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Cache Expiration: Always set appropriate expiration times to prevent stale data from being served. Your cache should periodically clean up expired items to free memory.
2. Use Cache Wisely: Only cache data that is read often and changes infrequently. Avoid caching data that is constantly changing, as it may lead to serving outdated or incorrect information.
3. Handle Cache Misses: Gracefully handle cache misses by providing fallback mechanisms, such as re-fetching the data from the database or API.

Conclusion

In-memory caching can significantly boost the performance and scalability of your Go applications by reducing the load on databases, APIs, and other backend services. Whether you’re caching expensive database queries, API responses, or session tokens, localcache provides a flexible, easy-to-use solution.

If you're interested in the full implementation of the in-memory cache or want to explore advanced features like cache eviction policies or concurrency handling, check out the full code in this repository.

Top comments (0)