Configuring Database Connection and Environment Variables in Go: A Clean Architecture Approach
Use Viper to load YAML configuration and automatically overlay environment variables, then construct a GORM MySQL connection using a centralized config struct in pkg/config/config.go.
The manakuro/golang-clean-architecture repository demonstrates how to isolate infrastructure concerns from business logic when configuring database connection and environment variables in Go. By combining Viper for configuration management with a global config struct, the codebase ensures that database credentials and environment-specific settings remain secure, flexible, and accessible throughout the application lifecycle.
Centralizing Configuration with Viper
The repository places all configuration logic in pkg/config/config.go, which defines a struct that mirrors the YAML structure in pkg/config/config.yml. This approach creates a single source of truth for application settings.
The Config Struct and YAML Structure
The config struct in pkg/config/config.go organizes settings into logical groups such as database and server parameters. The ReadConfig function unmarshals the YAML file into this struct and assigns it to the global variable C, making configuration available across packages without manual dependency injection.
// pkg/config/config.go
type config struct {
Database struct {
User string
Password string
Net string
Addr string
DBName string
AllowNativePasswords bool
Params struct {
ParseTime string
}
}
Server struct {
Address string
}
}
var C config
func ReadConfig() {
viper.SetConfigName("config")
viper.SetConfigType("yml")
viper.AddConfigPath(filepath.Join(rootDir(), "config"))
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
log.Fatal(err)
}
if err := viper.Unmarshal(&C); err != nil {
log.Fatal(err)
}
spew.Dump(C) // Debug output; remove in production
}
Loading Environment Variables Automatically
Viper's AutomaticEnv method enables seamless environment variable integration. When viper.AutomaticEnv() is called, Viper checks for environment variables that match struct fields and overrides YAML values accordingly. For example, setting export DATABASE_PASSWORD=supersecret in your shell automatically replaces the password defined in config.yml before the database connection initializes.
This mechanism keeps sensitive credentials out of version control while allowing developers to override any configuration value at runtime using Docker, CI pipelines, or local shell environments.
Building the Database Connection
The database infrastructure layer in pkg/infrastructure/datastore/db.go consumes the global configuration to construct a GORM-compatible MySQL connection. This separation ensures that database driver details remain isolated from domain logic.
Constructing the MySQL DSN
The NewDB function builds a MySQL Data Source Name (DSN) using parameters from config.C.Database. It leverages the go-sql-driver/mysql package's Config struct to format connection parameters correctly, including network type, address, credentials, and connection options like parseTime=true.
// pkg/infrastructure/datastore/db.go
package datastore
import (
"github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"github.com/manakuro/golang-clean-architecture/pkg/config"
)
func NewDB() *gorm.DB {
mySqlConfig := &mysql.Config{
User: config.C.Database.User,
Passwd: config.C.Database.Password,
Net: config.C.Database.Net,
Addr: config.C.Database.Addr,
DBName: config.C.Database.DBName,
AllowNativePasswords: config.C.Database.AllowNativePasswords,
Params: map[string]string{
"parseTime": config.C.Database.Params.ParseTime,
},
}
db, err := gorm.Open("mysql", mySqlConfig.FormatDSN())
if err != nil {
panic(err)
}
return db
}
Initializing GORM
The gorm.Open call receives the formatted DSN string and establishes the connection pool. Because NewDB depends on config.C already being populated, it must be called after config.ReadConfig() executes in the application entry point. The resulting *gorm.DB instance is then passed to repositories or used directly in infrastructure layers.
Bootstrapping the Application
Both the HTTP server and database seeding commands follow identical initialization patterns, ensuring consistent configuration across all entry points.
Entry Point Implementation
The main functions in cmd/app/main.go and cmd/seed/main.go demonstrate the required bootstrap sequence. First, they invoke config.ReadConfig() to load YAML and environment variables. Then they initialize the database connection using datastore.NewDB().
// cmd/app/main.go
package main
import (
"log"
"github.com/manakuro/golang-clean-architecture/pkg/config"
"github.com/manakuro/golang-clean-architecture/pkg/infrastructure/datastore"
)
func main() {
// Load configuration from YAML and environment variables
config.ReadConfig()
// Initialize database connection using loaded config
db := datastore.NewDB()
defer db.Close()
// Continue with server initialization...
log.Println("Database connection established")
}
This pattern guarantees that config.C contains the final, environment-specific values before any infrastructure code attempts to use it.
Docker Compose Integration
The repository includes a docker/docker-compose.yml file that defines MySQL service credentials. These values should align with the defaults in pkg/config/config.yml for local development, while production deployments override them via environment variables.
# docker/docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0.23
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: golang_clean_architecture
ports:
- "3306:3306"
When running the Go application locally against this Docker container, the default YAML configuration connects successfully. For production, inject the actual database password via DATABASE_PASSWORD environment variable without modifying any source files.
Summary
- Centralized configuration lives in
pkg/config/config.go, where Viper loadspkg/config/config.ymland automatically overlays matching environment variables. - Environment variable precedence is handled by
viper.AutomaticEnv(), allowing runtime overrides of YAML defaults for secrets and deployment-specific settings. - Database connection construction in
pkg/infrastructure/datastore/db.goconsumes the globalconfig.Cstruct to build a MySQL DSN for GORM. - Consistent bootstrapping across
cmd/app/main.goandcmd/seed/main.goensures configuration loads before infrastructure initializes.
Frequently Asked Questions
How does Viper handle environment variable precedence in this architecture?
Viper's AutomaticEnv() method checks for environment variables after loading the YAML file but before unmarshaling into the struct. If an environment variable exists that matches a configuration key (e.g., DATABASE_PASSWORD maps to database.password), Viper uses the environment value, overriding the YAML default. This precedence ensures that sensitive values injected at runtime take priority over version-controlled defaults.
Where should database credentials be stored in production?
Database credentials should never be stored in pkg/config/config.yml or any version-controlled file in production. Instead, export them as environment variables on the host system or orchestration platform (Kubernetes secrets, AWS ECS task definitions, or Docker Compose secrets). The application reads these via Viper's environment variable integration, keeping credentials out of source code while maintaining configuration flexibility.
How do I add a new configuration field to the application?
First, add the field to the config struct in pkg/config/config.go within the appropriate nested struct (e.g., Database or Server). Then, add the corresponding default value to pkg/config/config.yml. If the field might need environment-specific overrides, ensure the field name follows the pattern that Viper's AutomaticEnv() can match (e.g., NEW_FIELD_NAME for a field NewFieldName or new.field_name depending on your struct tags and Viper settings).
Can I use a .env file instead of exporting environment variables directly?
Yes, though the repository does not include this by default. To add .env file support, modify pkg/config/config.go to include viper.SetConfigFile(".env") and viper.ReadInConfig() before calling viper.AutomaticEnv(). This allows Viper to load variables from a .env file in addition to system environment variables, which is useful for local development workflows where exporting variables in every shell session is inconvenient.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →