Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
55: Working with Databases via database/sql
A few years ago, I was reviewing a PR for a junior dev who had built a sleek little API for a logistics company. On the surface, the code was clean. However, I noticed they were calling sql.Open() inside their HTTP handler function. In their staging environment with two users, it worked perfectly. But the moment they deployed to production, the app crashed within ten minutes. Why? Because they were opening a new connection pool for every single incoming request, quickly exhausting the database's max connection limit. The database didn't just slow down; it started rejecting every single attempt to connect, effectively DOSing itself.
The biggest mental shift you need when using the database/sql package is realizing that a *sql.DB object is not a single connection to the database. It is a pool of connections. You should create it once when your application starts and pass that same instance around to your services or repositories. Go manages the opening and closing of individual connections under the hood; your job is just to tell it which driver to use and how to connect.
Managing the Connection Pool and Drivers
Go's database/sql package provides a generic interface, but it doesn't actually know how to "talk" to PostgreSQL, MySQL, or SQLite. For that, you need a driver. You'll notice a weird pattern in Go database code where you import a driver with an underscore, like _ "github.com/lib/pq". This is a "blank import"βit tells Go to execute the driver's init() function to register itself with the sql package without you having to call any functions from that package directly.
import (
"database/sql"
_ "github.com/lib/pq" // The driver registers itself here
)
func main() {
// Open doesn't actually connect; it just validates the arguments
db, err := sql.Open("postgres", "user=postgres password=secret dbname=bookstore sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// This is where the first actual connection is attempted
if err := db.Ping(); err != nil {
log.Fatal(err)
}
}
Querying Data and the Scanning Ritual
When you want to get data out of the database, you'll mostly use Query, QueryRow, or Exec. If you expect multiple rows, use Query. If you're looking for a specific record by ID, QueryRow is your friend because it simplifies the logic by returning a single row object.
The part that trips most people up is Scan(). Go cannot magically map a database row to a struct. You have to explicitly tell Go which column goes into which variable, and the order must match your SQL query exactly. I've spent way too many hours debugging a "sql: Scan error" only to realize I had swapped the Author and Title columns in my SELECT statement.
type Book struct {
ID int
Title string
Stock int
}
func getBook(db *sql.DB, id int) (*Book, error) {
var b Book
// QueryRow is perfect for single-record lookups
err := db.QueryRow("SELECT id, title, stock FROM books WHERE id = $1", id).Scan(&b.ID, &b.Title, &b.Stock)
if err != nil {
return nil, err
}
return &b, nil
}
Avoiding the Memory Leak Trap
When you use db.Query(), it returns a *sql.Rows object. This object holds a database connection open until it is closed. If you forget to call rows.Close(), you're leaking connections. Eventually, your app will hang because the pool is exhausted, and you'll be right back in that nightmare scenario I mentioned at the start.
Always use defer rows.Close() immediately after checking the error from Query(). Also, remember that rows.Next() prepares the next row for reading, but you still need to check rows.Err() after the loop finishes. A network glitch could terminate the stream early, and if you don't check the error, you might return a partial list of results as if it were a complete set.
func listLowStockBooks(db *sql.DB) ([]Book, error) {
rows, err := db.Query("SELECT id, title, stock FROM books WHERE stock < 5")
if err != nil {
return nil, err
}
defer rows.Close() // Crucial: prevents connection leaks
var books []Book
for rows.Next() {
var b Book
if err := rows.Scan(&b.ID, &b.Title, &b.Stock); err != nil {
return nil, err
}
books = append(books, b)
}
// Check for errors that happened during iteration
if err = rows.Err(); err != nil {
return nil, err
}
return books, nil
}π Practical Task
Exercise: Build a Bookstore Inventory Manager
Your task is to create a small CLI tool that manages a bookstore's inventory using a database (SQLite is recommended for this exercise via github.com/mattn/go-sqlite3). Your program should implement the following functionality:
- Initialization: Create a table named
bookswith columnsid(INTEGER PRIMARY KEY),title(TEXT), andstock(INTEGER). - Add Book: A function
addBook(db *sql.DB, title string, stock int)that usesdb.Exec()to insert a new book into the database. - Update Stock: A function
updateStock(db *sql.DB, id int, newStock int)that updates the quantity of a specific book. - Low Stock Report: A function
getLowStock(db *sql.DB, threshold int) ([]Book, error)that returns all books with stock below the given threshold, ensuring you properly close the rows and check for iteration errors.
Requirements:
1. Use a single *sql.DB instance throughout the life of the program.
2. Use parameterized queries (e.g., ? or $1) to prevent SQL injection.
3. Ensure rows.Close() is handled correctly in the reporting function.
There are no comments for now.