BeforeConnect Callback in go-sql-driver/mysql: Use Cases and Implementation

The BeforeConnect callback allows you to modify MySQL connection configuration dynamically right before the network handshake, enabling per-connection database selection, runtime credential injection, and session variable configuration.

The go-sql-driver/mysql package provides this hook to address scenarios where static DSN strings are insufficient. By intercepting the connection establishment process, applications can adjust parameters based on request context, external secret stores, or routing logic without rebuilding the entire driver configuration.

How BeforeConnect Works in the MySQL Driver

The callback operates as a functional option that the driver invokes during the final stage of physical connection establishment. It receives a cloned copy of the configuration, ensuring that modifications apply only to the current connection attempt while preserving the immutability of the base Config.

Configuration Storage and Registration

The option is defined in dsn.go, where the Config struct stores the callback in a private beforeConnect field. The BeforeConnect function returns an Option that assigns your callback to this field during configuration construction.

Execution During Connection Establishment

When the connector.Connect method in connector.go creates a new physical connection, it checks for the presence of the callback. If configured, the driver clones the current configuration and invokes the callback with the clone before proceeding with the TCP handshake and MySQL protocol authentication.

// From connector.go - connector.Connect method
if c.cfg.beforeConnect != nil {
    cfg = c.cfg.Clone()  // Work on a copy to avoid mutating shared state
    err = c.cfg.beforeConnect(ctx, cfg)
    if err != nil {
        return nil, err
    }
}
// Proceed with handshake using potentially modified cfg

Practical Use Cases for BeforeConnect

The ability to mutate configuration after initialization but before the network handshake enables several critical operational patterns:

  • Dynamic database selection: Override cfg.DBName to route connections to different logical databases based on request context or sharding logic, eliminating the need to parse and rebuild DSN strings for every connection.
  • Runtime credential injection: Fetch passwords from secret management systems (HashiCorp Vault, AWS Secrets Manager) by assigning cfg.Passwd within the callback, keeping sensitive data out of static configuration.
  • Session variable pre-configuration: Inject specific SET SESSION parameters by modifying cfg.Params, ensuring variables like sql_mode or time_zone are established immediately after authentication.
  • Connection attributes for auditing: Populate cfg.ConnectionAttributes with application metadata (version, environment, request ID) that appears in MySQL 5.7+ connection attribute tables for monitoring and compliance.
  • Context-aware TLS and timeouts: Adjust cfg.TLS configurations or cfg.Timeout values based on the context passed to QueryContext or ExecContext, enabling different security postures for internal vs. external requests.

Implementation Examples

Dynamic Database Selection

This example demonstrates routing to a specific database based on context, mirroring the test case in driver_test.go:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "github.com/go-sql-driver/mysql"
)

func main() {
    // Base DSN with placeholder database name
    cfg, _ := mysql.ParseDSN("user:pass@tcp(127.0.0.1:3306)/_?parseTime=true")
    
    // Configure BeforeConnect to set the actual database name
    cfg.Apply(mysql.BeforeConnect(func(ctx context.Context, c *mysql.Config) error {
        c.DBName = "sales_data"
        return nil
    }))
    
    connector, _ := mysql.NewConnector(cfg)
    db := sql.OpenDB(connector)
    defer db.Close()
    
    // Verify connection targets the correct database
    var currentDB string
    db.QueryRow("SELECT DATABASE()").Scan(&currentDB)
    fmt.Println("Connected to:", currentDB) // Output: sales_data
}

Injecting Session Variables

Use the callback to ensure specific system variables are set for every new connection:

cfg.Apply(mysql.BeforeConnect(func(ctx context.Context, c *mysql.Config) error {
    // Ensure strict SQL mode for this connection
    if c.Params == nil {
        c.Params = make(map[string]string)
    }
    c.Params["sql_mode"] = "'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION'"
    
    // Set connection attributes for monitoring
    c.ConnectionAttributes = "app:payment_service,version:2.1.0"
    return nil
}))

Context-Aware Credential Retrieval

Fetch passwords from a secret manager using the request context:

cfg.Apply(mysql.BeforeConnect(func(ctx context.Context, c *mysql.Config) error {
    // Extract tenant from context
    tenant, ok := ctx.Value("tenant_id").(string)
    if !ok {
        return fmt.Errorf("missing tenant context")
    }
    
    // Fetch credentials for specific tenant
    secret, err := vaultClient.GetMySQLCredentials(ctx, tenant)
    if err != nil {
        return err
    }
    
    c.User = secret.Username
    c.Passwd = secret.Password
    c.DBName = tenant + "_db"
    return nil
}))

Summary

  • The BeforeConnect callback in go-sql-driver/mysql enables dynamic modification of connection parameters immediately before the network handshake.
  • Defined in dsn.go and invoked in connector.go, the callback receives a cloned Config pointer, ensuring thread-safe mutations without affecting the base configuration.
  • Primary use cases include dynamic database routing, runtime credential injection from secret managers, pre-configuring session variables, setting connection attributes for auditing, and context-aware TLS adjustments.
  • The callback runs once per physical connection, making it ideal for initialization logic that must execute before MySQL protocol authentication begins.

Frequently Asked Questions

What is the difference between BeforeConnect and other connection hooks?

BeforeConnect executes specifically after the driver decides to create a new physical connection but before any network packets are exchanged with the MySQL server. This differs from sql.DB connection pooling logic, which operates at the standard library level, and from post-connection initialization that would require executing SQL statements after the handshake completes.

Can BeforeConnect modify the DSN string directly?

No, the callback receives a *Config struct pointer, not the raw DSN string. You must modify the exported fields of the Config struct (such as DBName, User, Passwd, or Params). The driver converts this modified struct into the wire protocol format internally; the original DSN string is not reconstructed.

Is BeforeConnect called for every query or just once per connection?

BeforeConnect runs exactly once per physical connection, not per query. When using sql.DB connection pooling, the standard library may reuse existing connections for multiple queries. The callback only fires when the pool needs to establish a new TCP connection to the MySQL server, making it ideal for per-connection initialization rather than per-request logic.

How does BeforeConnect handle connection pooling in database/sql?

The database/sql package manages connection pooling independently of the driver. When sql.DB needs a new connection and calls the driver's Connector.Connect method, the MySQL driver invokes BeforeConnect at that moment. Because the driver clones the Config before calling your callback, concurrent connection attempts from the pool receive independent configuration copies, preventing race conditions while allowing each new physical connection to have customized parameters.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →