Executing Queries Go
Fast recall
Quick revision
Core answer
Whether you understand Executing Queries Go beyond memorized syntax.
Giving a definition without explaining a practical trade-off.
Connect the concept to a small production example and state its complexity or failure mode.
Interview-ready guide
Interview answer
The three operations, all executed with a real SQLite database:
db, _ := sql.Open("sqlite", ":memory:")
db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
// 1. Exec — INSERT/UPDATE/DELETE (no rows back)
result, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", "Al", "al@x.com")
// ? = parameter — SAFE against injection — verified
// 2. Query — SELECT (iterate rows)
rows, err := db.Query("SELECT id, name, email FROM users")
defer rows.Close() // ALWAYS — rows is a resource
for rows.Next() {
var id int
var name, email string
rows.Scan(&id, &name, &email)
// each row — verified: id=1, name=Al, email=al@x.com
}
// 3. QueryRow — exactly one row
var email string
db.QueryRow("SELECT email FROM users WHERE name = ?", "Al").Scan(&email)
// email = al@x.com — verifiedThe injection rule, the most-tested point:
// BAD — string concatenation
query := "SELECT * FROM users WHERE name = '" + userInput + "'"
// user input: ' OR '1'='1 → everything leaks / '); DROP TABLE users; -- → disaster
// GOOD — parameterized
db.Query("SELECT * FROM users WHERE name = ?", userInput)
// The driver sends the query and the value separately — the value can never become SQLThe lifecycle rules:
- db is a POOL — Open once (main/init), close on shutdown
- rows must Close — always defer
- Scan checks column count/types — Scan into properly typed variables
- errors.ErrNoRows for QueryRow's 'not found' — the sentinel to check
Executing Queries Go
Direct Answer
db.Exec (INSERT/UPDATE/DELETE, no rows returned), db.Query (SELECT, returns *Rows to iterate), db.QueryRow (one row, Scan directly). Always use ? parameters — never concatenate.
Deep Dive & Architecture
The pool and the lazy connection: sql.Open doesn't connect — it validates the config and returns immediately. The first Query/Exec pulls a connection from the pool (creating it if needed). The pool tuning: SetMaxOpenConns (the DB's ceiling — Postgres default 100, your app should take a fraction), SetMaxIdleConns (the kept-open connections), SetConnMaxLifetime (the cutover-safe recycling — DB failovers are invisible to the app).
The Rows mechanics: Query returns a live cursor — each Next() fetches from the connection. The connection is on loan for the iteration's duration (an unclosed Rows holds a pool connection hostage — the production pool-exhaustion bug). Close releases it — ALWAYS deferred.
The NULL discipline: a nullable column can't Scan into a non-pointer — Scan into sql.NullString / sql.NullInt64 (the type with Value + Valid), or use COALESCE in the query (SELECT COALESCE(email, '') ...). The NULL-to-interface{} trap (Scan into *interface{} gets []byte, not string — the driver-specific encoding surprise).
Common Mistakes & Pitfalls
Giving a definition without explaining a practical trade-off.
Check your understanding
Answer these without looking back at the article.
- When would you use Executing Queries Go in a real project?
- How would you compare Executing Queries Go with an alternative?
- What is a common mistake when using Executing Queries Go?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.