Using Scoped Database Context in Go: Is It Possible or Good Practice?

In ASP.NET Core, there’s a convenient way to manage database contexts with AddDbContext(). By configuring it this way in Main, I can create a scoped instance using:

using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

This approach limits the database context’s lifetime to the scope where it’s created and automatically disposes of it after use.

Is there an equivalent approach in Go for creating a scoped database context without having to pass it to every function or struct? And would this be considered good or bad practice in Go applications?

From the docs:

The lifetime of a DbContext begins when the instance is created and ends when the instance is disposed. A DbContext instance is designed to be used for a single unit-of-work. This means that the lifetime of a DbContext instance is usually very short.

I guess I’m not sure what the DbContext is really buying you. Does it handle things like timeouts? In most of my go projects, I create a connection pool with sane defaults for things like max connections. The connection pool is threadsafe. PGX (which I often use) does have a concept of every query having a context:

err = conn.QueryRow(context.Background(), "select * from my_table").Scan(&prop)

You can pass the request context through here if you want. Or set a timeout for that specific query. Is the issue that you’re not sure how to get your connection pool into your handler funcs? If that’s the case, USUALLY I have seen people create some sort of “server” struct (often times they will break them out by domain if the app is large! But that’s an easy refactor as your app grows in size):

package main

import (
    "database/sql"
    "net/http"
)

type Env struct {
    DB *sql.DB
}

func main() {
    db, err := sql.Open("mysql", "user:password@/dbname")
    if err != nil {
        log.Fatal(err)
    }
    e := &Env{DB: db}
    http.HandleFunc("/", seomeHandler)
}

func (e *Env) homeHandler(w http.ResponseWriter, r *http.Request) {
    // Use e.DB to access connection pool
}

This is contrived code obviously and that main isn’t even listening and serving. But still - this is the kind of implementation I’ve seen most.

Coming from ASP.NET, you will have some annoyances where there’s just one way to do things in ASP.NET and the framework “just handles it for you” but in Go it’s more up to the programmer to implement it however you want. There’s a tradeoff where there’s less “magic” and you will know exactly how your application works and what it’s doing. But at first it can be a bit jarring.

1 Like