// Package database owns everything related to talking to MySQL: opening // the connection pool and running startup migrations. Nothing outside this // package (and the repositories in internal/models) should import the // MySQL driver directly - that keeps the database technology an // implementation detail hidden behind a plain *sql.DB. package database import ( "context" "database/sql" "fmt" "time" // Blank import: we never call anything from this package by name. // Importing it purely for its side effect - its init() function // registers "mysql" as a driver name with database/sql, which is what // lets sql.Open("mysql", ...) below work at all. _ "github.com/go-sql-driver/mysql" "git.hamidsoltani.com/hamid/go-simple-api/internal/config" ) // NewMySQL opens a connection pool to MySQL, configures reasonable pool // limits, and verifies connectivity with a real ping before returning - // so a bad connection string / unreachable database fails loudly at // startup instead of silently on the first real query. func NewMySQL(ctx context.Context, cfg config.Config) (*sql.DB, error) { // DSN = Data Source Name. Format: user:password@tcp(host:port)/dbname?options // parseTime=true tells the driver to convert MySQL DATETIME/TIMESTAMP // columns into Go time.Time values automatically. dsn := fmt.Sprintf( "%s:%s@tcp(%s:%s)/%s?parseTime=true", cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName, ) // sql.Open does NOT connect yet - it just validates the DSN and // returns a *sql.DB, which represents a pool of connections managed // for you, not a single live connection. db, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("open mysql: %w", err) } // Pool tuning: // MaxOpenConns - hard ceiling on simultaneous connections, protects // the database from being overwhelmed by this app. // MaxIdleConns - how many connections to keep warm/ready even when // idle, so we don't pay reconnect cost constantly. // ConnMaxLifetime - force connections to be recycled periodically, // useful behind load balancers or if MySQL itself // closes long-lived idle connections. db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) // Give the ping a hard deadline instead of letting it hang forever if // the database host is unreachable. pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := db.PingContext(pingCtx); err != nil { return nil, fmt.Errorf("ping mysql: %w", err) } return db, nil }