Hone logo
Hone
Problems

Go Configuration Manager

Developing robust applications requires a flexible and maintainable way to manage application settings. This challenge focuses on building a Go package that can load and provide access to application configuration from various sources, ensuring your Go applications can be easily configured and deployed in different environments.

Problem Description

Your task is to create a Go package, let's call it configmgr, that provides a robust mechanism for managing application configuration. The package should be able to load configuration values from multiple sources, with a defined precedence order. This allows developers to override default settings with values from environment variables or configuration files.

Key Requirements:

  1. Configuration Structure: Define a flexible way to represent configuration. This could be a Go struct or a map. The solution should ideally support mapping configuration values to struct fields.
  2. Loading Sources:
    • Defaults: The configuration manager should accept a default configuration.
    • Environment Variables: Support loading configuration values from environment variables.
    • Configuration Files: Support loading configuration from files (e.g., JSON, YAML).
  3. Precedence Order: Define a clear precedence for loading configurations. For example: Environment Variables > Configuration File > Defaults.
  4. Accessing Values: Provide methods to retrieve configuration values, ideally with type safety (e.g., GetInt(key), GetString(key)).
  5. Error Handling: Gracefully handle errors during loading (e.g., file not found, invalid format, missing required values).

Expected Behavior:

  • When configmgr is initialized, it should load defaults.
  • If a configuration file is provided, its values should be merged, overriding defaults where present.
  • Environment variables should override both default and file-based configurations.
  • Accessing a configuration key should return the value from the highest precedence source that contains it.
  • Attempting to access a non-existent key should result in an error or a zero-value with an error, depending on the accessor method.

Edge Cases to Consider:

  • Configuration files with nested structures.
  • Environment variables that don't match configuration keys.
  • Required configuration values that are missing from all sources.
  • Different data types for configuration values (strings, integers, booleans, slices).

Examples

Example 1: Basic Loading and Access

Assume the following defaults and a configuration file:

  • Defaults:
    type AppConfig struct {
        DatabaseURL string `default:"postgres://user:pass@localhost:5432/mydb"`
        Port        int    `default:"8080"`
    }
    
  • config.json:
    {
      "port": 9090,
      "database_url": "postgres://override:secret@db.example.com:5432/appdb"
    }
    
  • Environment Variables:
    • APP_PORT=3000

Input:

An instance of configmgr initialized with the AppConfig struct as defaults, pointing to config.json, and with APP_PORT set in the environment.

Output:

The configmgr should resolve to the following effective configuration:

{
  "DatabaseURL": "postgres://override:secret@db.example.com:5432/appdb",
  "Port": 3000
}

Explanation:

  • Port is set to 3000 because the environment variable APP_PORT has the highest precedence.
  • DatabaseURL is set to the value from config.json because it overrides the default and there's no environment variable for it.

Example 2: Nested Configuration and Missing Values

Assume the following defaults and a configuration file:

  • Defaults:
    type AppConfig struct {
        Server struct {
            Host string `default:"localhost"`
            Port int    `default:"8000"`
        }
        APIKey string `default:""`
    }
    
  • config.yaml:
    server:
      host: "127.0.0.1"
    
  • Environment Variables:
    • API_KEY=supersecretkey123

Input:

An instance of configmgr initialized with the AppConfig struct as defaults, pointing to config.yaml, and with API_KEY set in the environment.

Output:

  • configmgr.GetString("server.host") should return "127.0.0.1".
  • configmgr.GetInt("server.port") should return 8000.
  • configmgr.GetString("api_key") should return "supersecretkey123".
  • configmgr.GetString("nonexistent_key") should return an error or an empty string with an error.
  • configmgr.GetString("api_key") when API_KEY is not set and defaults.APIKey is empty should return an error indicating a missing required configuration.

Explanation:

  • server.host is from config.yaml, overriding the default.
  • server.port is from the default because it's not in config.yaml or environment variables.
  • api_key is from the environment variable, overriding the default empty string.
  • Accessing a non-existent key should signal an error.
  • If APIKey were truly required and not set in any source, the manager should indicate this.

Constraints

  • The configuration manager must be thread-safe for reading configurations.
  • Supported configuration file formats should include at least JSON and YAML.
  • Environment variable names for configuration keys should follow a consistent naming convention (e.g., APP_SECTION_KEY for nested keys like section.key).
  • The solution should aim for reasonable performance, avoiding excessive parsing or reflection overhead during configuration access after initial loading.
  • The package should provide clear API documentation.

Notes

  • Consider using libraries like viper or koanf as inspiration for features and API design, but aim to build a core functionality from scratch to understand the underlying principles.
  • Think about how you will map environment variable names to your configuration struct keys, especially for nested structures. A common pattern is to use UPPERCASE_UNDERSCORE for environment variables corresponding to CamelCase or snake_case struct fields.
  • The definition of "missing required values" might need careful consideration. You could, for example, use pointers in your struct to distinguish between a zero-value and an unset value, or rely on tags.
Loading editor...
go