How to Register and Utilize Custom Dial Functions for Advanced MySQL Connection Setup in Go

Use mysql.RegisterDialContext() to register a global custom dialer referenced by network name in the DSN, or set Config.DialFunc directly for per-connector overrides that don't require global registration.

The go-sql-driver/mysql package provides a pluggable architecture allowing developers to register and utilize custom dial functions for advanced connection setup when standard TCP transports are insufficient. Whether routing through TLS-terminating proxies, establishing SSH tunnels, or injecting connection-level instrumentation, the driver exposes two distinct APIs—global registration and per-connector configuration—for customizing network establishment.

Understanding the Dial Function Architecture

The driver defines two function signatures in driver.go to handle network connections, supporting both modern context-aware workflows and legacy code:

  • DialContextFunc – The modern signature accepting context for cancellation and timeouts (lines 38-39)
  • DialFunc – The legacy signature maintained for backward compatibility (lines 31-35)
// driver.go
type DialContextFunc func(ctx context.Context, addr string) (net.Conn, error)
type DialFunc func(addr string) (net.Conn, error)

Global Registry Architecture

The driver maintains a thread-safe global map named dials that stores custom dialers keyed by network names. Registration functions acquire locks via dialsLock to ensure safe concurrent access across goroutines.

// driver.go – Registration API (lines 46-55, 67-75)
func RegisterDialContext(net string, dial DialContextFunc)
func RegisterDial(network string, dial DialFunc) // Legacy wrapper
func DeregisterDialContext(net string)

Per-Connector Configuration

For scenarios requiring isolation from global state, the Config struct in dsn.go exposes a DialFunc field that supersedes the global registry for individual connectors only (lines 57-58):

// dsn.go
type Config struct {
    DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
    // ... other fields
}

Registering a Global Custom Dialer

To register a custom dial function globally, invoke RegisterDialContext before opening any database connections. The network name you register becomes the protocol identifier in your DSN.

// Register a custom dialer with specific timeout and keepalive settings
mysql.RegisterDialContext("customtcp", func(ctx context.Context, addr string) (net.Conn, error) {
    d := net.Dialer{
        Timeout:   5 * time.Second,
        KeepAlive: 30 * time.Second,
    }
    return d.DialContext(ctx, "tcp", addr)
})

Once registered, reference the custom dialer in your DSN using the format user:pass@networkname(host:port)/dbname:

dsn := "user:password@customtcp(127.0.0.1:3306)/mydb?timeout=30s"
db, err := sql.Open("mysql", dsn)

The driver looks up the network name (customtcp) in the global dials map during connection establishment.

Implementing Per-Connector Dial Overrides

When global registration is inappropriate—such as in multi-tenant applications requiring different transport configurations per connection pool—set Config.DialFunc directly without registering a network name.

cfg, _ := mysql.ParseDSN("user:pass@tcp(127.0.0.1:3306)/mydb")
cfg.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
    d := net.Dialer{Timeout: 5 * time.Second}
    return d.DialContext(ctx, network, addr)
}

connector, _ := mysql.NewConnector(cfg)
db := sql.OpenDB(connector)

This approach requires no special network name in the DSN and affects only connections created through that specific connector, leaving the global dials registry untouched.

Connection Establishment Priority

According to the implementation in connector.go (lines 98-106), the driver resolves dial functions in the following strict priority order when establishing a connection:

  1. Per-connector override: If c.cfg.DialFunc is non-nil, invoke it with c.cfg.DialFunc(dctx, mc.cfg.Net, mc.cfg.Addr)
  2. Global registry lookup: Check the dials map using mc.cfg.Net as the key; if found, call the registered DialContextFunc
  3. Standard net.Dialer: Fallback to the default Go network dialer with default timeouts
// connector.go – dialing logic priority
if c.cfg.DialFunc != nil {
    mc.netConn, err = c.cfg.DialFunc(dctx, mc.cfg.Net, mc.cfg.Addr)
} else {
    dialsLock.RLock()
    dial, ok := dials[mc.cfg.Net]
    dialsLock.RUnlock()
    if ok {
        mc.netConn, err = dial(dctx, mc.cfg.Addr)
    } else {
        nd := net.Dialer{}
        mc.netConn, err = nd.DialContext(dctx, mc.cfg.Net, mc.cfg.Addr)
    }
}

Practical Implementation Examples

Example 1: Global Registration with Custom Socket Options

This example demonstrates registering a dialer that enforces specific timeout and keepalive settings, then referencing it via DSN:

package main

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

func main() {
    // Register custom dialer globally (driver.go lines 46-55)
    mysql.RegisterDialContext("slownet", func(ctx context.Context, addr string) (net.Conn, error) {
        d := net.Dialer{
            Timeout:   5 * time.Second,
            KeepAlive: 30 * time.Second,
        }
        // Simulate connection latency or proxy overhead
        time.Sleep(2 * time.Second)
        return d.DialContext(ctx, "tcp", addr)
    })
    
    // Use the registered network name in DSN
    dsn := "user:password@slownet(127.0.0.1:3306)/mydb?timeout=30s"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        panic(err)
    }
    defer db.Close()
    
    if err := db.Ping(); err != nil {
        panic(err)
    }
}

Example 2: Per-Connector TLS Wrapper

For connections requiring custom TLS setup without polluting the global registry:

cfg, _ := mysql.ParseDSN("user:pass@tcp(db.example.com:3306)/production")
cfg.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
    d := net.Dialer{Timeout: 10 * time.Second}
    rawConn, err := d.DialContext(ctx, network, addr)
    if err != nil {
        return nil, err
    }
    // Wrap with custom TLS configuration
    // tlsConn := tls.Client(rawConn, tlsConfig)
    // return tlsConn, nil
    return rawConn, nil
}

connector, _ := mysql.NewConnector(cfg)
db := sql.OpenDB(connector)

Example 3: Dynamic Dialer Injection with BeforeConnect

Use the BeforeConnect hook to inject dial logic immediately before connection establishment:

cfg, _ := mysql.ParseDSN("user:pass@tcp(127.0.0.1:3306)/")
cfg.Apply(mysql.BeforeConnect(func(ctx context.Context, c *mysql.Config) error {
    c.DialFunc = func(ctx context.Context, net, addr string) (net.Conn, error) {
        return net.DialTimeout(net, addr, 10*time.Second)
    }
    return nil
}))

connector, _ := mysql.NewConnector(cfg)
db := sql.OpenDB(connector)

Summary

  • RegisterDialContext – Use for global dialer registration referenced by network name in DSNs; stored in thread-safe dials map in driver.go (lines 46-55)
  • Config.DialFunc – Use for per-connector overrides that don't require global state or special DSN network names; defined in dsn.go (lines 57-58) and checked first in connector.go (lines 98-100)
  • Priority Order – Per-connector DialFunc takes precedence over global registry lookups, which take precedence over the standard net.Dialer fallback
  • Legacy Support – RegisterDial wraps RegisterDialContext internally (lines 67-75) to maintain backward compatibility with non-context dial functions
  • DSN Format – Custom network names appear before the parenthesis: user@networkname(host:port)/db

Frequently Asked Questions

What is the difference between RegisterDial and RegisterDialContext?

RegisterDial accepts the legacy DialFunc signature (func(addr string) (net.Conn, error)) and wraps it to implement the modern DialContextFunc interface, effectively ignoring the context parameter. RegisterDialContext accepts the full DialContextFunc signature (func(ctx context.Context, addr string) (net.Conn, error)), allowing proper cancellation and timeout handling through context propagation. According to the source in driver.go lines 67-75, RegisterDial simply adapts the legacy function to call RegisterDialContext internally.

Can I use a custom dialer for just one database connection without affecting others?

Yes. Instead of using RegisterDialContext which affects all connections using that network name globally, create a custom Config and set its DialFunc field directly. As implemented in connector.go lines 98-100, this per-connector function takes precedence over the global registry, ensuring only connections created through that specific configuration use your custom dial logic while other sql.DB instances use standard or globally registered dialers.

How does the driver choose which dial function to use?

The driver follows a strict resolution order defined in connector.go: first checking Config.DialFunc for per-connector overrides (lines 98-100), then looking up the network name in the global dials map populated by RegisterDialContext (lines 101-106), and finally falling back to the standard library's net.Dialer. This hierarchy ensures maximum flexibility while maintaining backward compatibility with standard DSN configurations.

What network name should I use in the DSN when registering a custom dialer?

Any unique string identifier works as the network name (e.g., mydial, proxy, customtcp). This identifier becomes the protocol portion of your DSN: user:pass@mydial(localhost:3306)/dbname. The driver uses this string as the lookup key in the dials map, as shown in connector.go lines 101-106. Choose descriptive names that indicate the transport mechanism or specific configuration being applied.

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 →