0

My Go packages sometimes require setup - it may be a connection pool for a DB, the creation of a *Regexp struct that I use throughout the package, or any number of other things.

Given that these functions/setup routines have to be called in order for my packages to function correctly, that the setup may be computationally expensive to run, and that the setups only have to be run once, I usually put them an init() function within my package - e.g.:

//Taken from example code here: https://stackoverflow.com/a/36358573/13296826
var myDb *sql.DB

func init() {
    var err error
    myDb, err = sql.Open("driver-name", "database=test1")

    if err != nil {
        panic(err)
    }
}

To set up a (hypothetical) connection pool to a DB where the params are hardcoded. Or:

var myRegexp *regexp.Regexp

func init() {
     //Will panic if MustCompile() fails.
     myRegexp = regexp.MustCompile("[a-zA-Z0-9]{3}")
}

To compile a regexp struct for use throughout the package.

The two problems that I can see with this approach are:

  1. Package-level variables (even unexported ones) are seen by many as bad practice (e.g. https://stackoverflow.com/a/50844500/13296826 & https://www.reddit.com/r/golang/comments/8d8qes/when_is_it_okay_to_use_package_level_variables/) - this viewpoint is often down to the fact that a package-level variable can lead to race conditions (although both sql.DB and regexp.Regexp are safe for concurrent use).
  2. The init() functions in both of the above code examples can result in an error or panic. Since the init() is called when the package is initialised (and not directly invoked by calling package.init()), handling an error or recovering from a panic isn't possible outside of the init() (according to this: https://stackoverflow.com/a/30659042/13296826).

So, is there a better way to handle initialising a package-level variable that accounts for any errors/panics that may occur?

ChardeeMacdennis
  • 175
  • 1
  • 10
  • 3
    3. Create a function that returns a new `*sql.DB` for you. Then invoke it once in your `main` and pass elsewhere explicitly. – zerkms Apr 30 '21 at 22:45
  • 2
    Fix the panic problem in the first example by replacing the `init()` function with a function that opens the database and returns an error. Call that function from main and check the returned error. The second example is considered good practice: it's OK to panic on programmer errors, particularly programmer errors that are executed on every code path. – Charlie Tumahai Apr 30 '21 at 22:46
  • So, in the first example I'd essentially just replace `init()` with something like `func NewDB() (*sql.DB, error)` which would handle any setup logic for the DB, and then in any function calling `NewDB()`, I'd add in handling for any returned error? – ChardeeMacdennis Apr 30 '21 at 22:57
  • @ChardeeMacdennis Yup, that fixes the panic problem. It also gives you an opportunity to pass the connection string as an argument to the function. – Charlie Tumahai Apr 30 '21 at 23:31
  • Package-level variables are acceptable practice when the variable is not modified after package initialization. –  Apr 30 '21 at 23:36
  • @CeriseLimón So, in the parent package where I call `NewDB()` and check for errors, am I okay to store the returned `*sql.DB` struct in an (unexported) package-level variable? – ChardeeMacdennis May 01 '21 at 00:08

0 Answers0