How to share database connection in Go Discord bot handlers without using globals?

I’m building a Discord bot using Go that needs to query a MySQL database when users send commands. The problem I’m facing is with the handler function that processes user messages.

func handleError(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func SetupDatabase() (*sql.DB, error) {
    connection, err := sql.Open("mysql", DatabaseURL)
    return connection, err
}

func StartBot() {
    // initialize discord session
    session, err := discordgo.New("Bot " + Token)
    handleError(err)
    
    mysql, dbErr := SetupDatabase()
    defer mysql.Close()
    handleError(dbErr)
    
    repository = NewUserRepository(mysql)
    
    // register message handler - this is where my issue occurs
    session.AddHandler(messageHandler)
    
    session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged
    
    err = session.Open()
    handleError(err)
    defer session.Close()
    
    fmt.Println("Bot is online...")
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
    <-stop
}

The issue is that session.AddHandler(messageHandler) only accepts specific function signatures like:

func(session *discordgo.Session, message *discordgo.MessageCreate)
func(session *discordgo.Session, update *discordgo.PresenceUpdate)

This means I can’t pass my database connection as a parameter to the handler. I’ve considered two approaches but both seem problematic:

  1. Making the database connection global (I really want to avoid this)
  2. Creating a new database connection inside each handler call (this would be inefficient)

I’ve looked into using repository patterns but haven’t found a clean solution. What’s the best way to make my database connection available to the message handler without using global variables?

Another approach that works well is using closures to capture the database connection. Create a function that returns your handler with the database connection already bound to it.

func createMessageHandler(db *sql.DB, repo *UserRepository) func(*discordgo.Session, *discordgo.MessageCreate) {
    return func(session *discordgo.Session, message *discordgo.MessageCreate) {
        // Access db and repo directly here
        user, err := repo.GetUser(message.Author.ID)
        // your handler logic
    }
}

func StartBot() {
    session, err := discordgo.New("Bot " + Token)
    handleError(err)
    
    mysql, dbErr := SetupDatabase()
    handleError(dbErr)
    
    repository := NewUserRepository(mysql)
    
    messageHandler := createMessageHandler(mysql, repository)
    session.AddHandler(messageHandler)
    // continue with your setup
}

This keeps your code functional instead of object-oriented if that’s what you prefer. I’ve used both approaches and they work equally well - just depends on your coding style.

Dependency injection works great for this. Just make a simple container to hold your db connection and inject it into your handlers. Something like type Container struct { DB *sql.DB } then pass it around. Way cleaner than closures and much easier to test.

I hit this exact problem building my Discord bot last year. The cleanest fix was creating a struct that holds your database connection and implements methods matching the handler signature.

type BotHandler struct {
    db *sql.DB
    repository *UserRepository
}

func (b *BotHandler) MessageHandler(session *discordgo.Session, message *discordgo.MessageCreate) {
    // Now you can access b.db and b.repository here
    user, err := b.repository.GetUser(message.Author.ID)
    // handle your logic
}

func StartBot() {
    session, err := discordgo.New("Bot " + Token)
    handleError(err)
    
    mysql, dbErr := SetupDatabase()
    handleError(dbErr)
    
    handler := &BotHandler{
        db: mysql,
        repository: NewUserRepository(mysql),
    }
    
    session.AddHandler(handler.MessageHandler)
    // rest of your code
}

This keeps everything encapsulated without globals and maintains a single database connection throughout your bot’s lifecycle. Been using this in production for over a year with zero issues.