How to use dynamic location for godotenv.Load() .env file?
Asked Answered
U

1

10

Problem

I am building a REST API in Go. The godotenv package is used to load the environment variables. Running go run main.go, the project runs the API as expected, the environment variables are loaded.

However, when wanting to run the test using: go test ./... - which runs config/config_test.go among others - it throws the following error: Error loading .env file (as specified in function).

Given the following project structure:

> app
> auth
> config
  - config.go
  - config_test.go
> migrations
> static
> vendor
- .env
- .gitignore
- docker-compose.yml
- go.mod
- go.sum
- main.go
- README.md

In config.go, I use the following function to load the Database configuration.

func GetConfig() *Config {
    err := godotenv.Load(".env")

    if err != nil {
        log.Fatalf("Error loading .env file")
    }

    dbHost := os.Getenv("DB_HOST")
    dbPort := os.Getenv("DB_PORT")
    dbName := os.Getenv("DB_DATABASE")
    dbUsername := os.Getenv("DB_USERNAME")
    dbPassword := os.Getenv("DB_PASSWORD")

    return &Config{
        DB: &DBConfig{
            Connection: "mysql",
            Host:       dbHost,
            Port:       dbPort,
            Username:   dbUsername,
            Password:   dbPassword,
            Name:       dbName,
            Charset:    "utf8",
        },
    }
}

I understand that it works when running from root, because the .env resides in root. When running config/config_test.go, it tries to look for the .env file in the /config/.env. If I change the line: err := godotenv.Load(".env") to err := godotenv.Load("../.env"), the config_test.go runs successfully, but the go run main.go from root does not run successfully.

Question

How can I load the .env location dynamically from the GetConfig() function in config.go, so that both the go test ./... and go run main.go can load the .env?

Edit

I am aware that passing a path string parameter to the GetConfig() function would work in my application (I am initializing this config in the app package). However, I want to create multiple tests in different directories, and prefer not to pass a parameter. Is there another way to accomplish this?

Unimpeachable answered 12/7, 2021 at 11:15 Comment(6)
Pass the file location into GetConfig. Then in main.go pass .env, and in your test, pass ./testdata/.env or whatever.Apocopate
That is indeed a valid solution and I have considered it, likewise as a simple if statement in case loading fails. It will work in most cases since I have a App module that holds the config as well. However, I am planning on creating tests that will work individually of that initialization. I was hoping for a solution without having to pass parameters or creating if statements.Unimpeachable
Why do you not want to rely on passing a parameter? That's the correct solution. I'm also not sure what you mean by 'creating if statements"... what kinds of if statements would be required?Apocopate
There are multiple possible solutions discussed here - github.com/joho/godotenv/issues/43Phrixus
@Flimzy In case the first if statement fails, try another .env location ad infinitum. Not elegant and still the parameter solution is preferable. I rephrased not wanting to rely to prefer not to, because I believe there should be another solution and will try one of the suggested solutions provided by @Inian.Unimpeachable
@inian provided solution on Github did the trick. I have added it as the answer to my question.Unimpeachable
U
16

Following the suggestion of @Inian, I implemented the following solution, also listed on the Issues tab of the godotenv package.

In config.go I added a constant for the directory name (which is rest-api in my case). I added a loadEnv function that tries to get the root path of the project dynamically, based on the project name and the current working directory.

const projectDirName = "rest-api" // change to relevant project name

func loadEnv() {
    projectName := regexp.MustCompile(`^(.*` + projectDirName + `)`)
    currentWorkDirectory, _ := os.Getwd()
    rootPath := projectName.Find([]byte(currentWorkDirectory))

    err := godotenv.Load(string(rootPath) + `/.env`)

    if err != nil {
        log.Fatalf("Error loading .env file")
    }
}

func GetConfig() *Config {
    loadEnv()

    dbHost := os.Getenv("DB_HOST")
    dbPort := os.Getenv("DB_PORT")
    dbName := os.Getenv("DB_DATABASE")
    dbUsername := os.Getenv("DB_USERNAME")
    dbPassword := os.Getenv("DB_PASSWORD")

    return &Config{
        DB: &DBConfig{
            Connection: "mysql",
            Host:       dbHost,
            Port:       dbPort,
            Username:   dbUsername,
            Password:   dbPassword,
            Name:       dbName,
            Charset:    "utf8",
        },
    }
}
Unimpeachable answered 12/7, 2021 at 13:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.