Skip to main content
Go Language Fundamentals for Beginners to Advanced
CHAPTER 24 Beginner

Working with Databases in Go

Updated: May 17, 2026
5 min read

# CHAPTER 24

Working with Databases in Go

1. Introduction

A backend system is essentially a bridge between a user interface and a database. Go provides a standard, unified interface for interacting with relational databases via the database/sql package. Whether you are using MySQL, PostgreSQL, or SQLite, the Go code looks exactly the same; you just change the "Driver".

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Download and import a database Driver.
  • Connect to a MySQL database safely.
  • Execute basic CRUD operations (Insert, Update, Delete).
  • Query data and map rows into Go Structs.
  • Prevent SQL Injection.

3. Installing the Driver

The database/sql package provides the commands, but it relies on external, community-built Drivers to translate those commands to specific databases.

To connect to MySQL, run this in your terminal:

bash
1
go get -u github.com/go-sql-driver/mysql

4. Connecting to the Database

To connect, we need a Data Source Name (DSN) string containing the username, password, host, and database name.
go
1234567891011121314151617181920212223242526272829
package main

import (
    "database/sql"
    "fmt"
    "log"
    // The underscore imports the driver silently so database/sql can use it!
    _ "github.com/go-sql-driver/mysql" 
)

func main() {
    // Format: username:password@tcp(127.0.0.1:3306)/dbname
    dsn := "root:password123@tcp(127.0.0.1:3306)/school_db"

    // sql.Open establishes the configuration (it doesn't actually connect yet)
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close() // Always clean up!

    // db.Ping() actually tests the connection to the server
    err = db.Ping()
    if err != nil {
        log.Fatal("Cannot connect to Database:", err)
    }

    fmt.Println("Successfully connected to MySQL!")
}

5. Executing Queries (Insert / Update / Delete)

For operations that modify data (and don't return rows), we use db.Exec().

CRITICAL RULE: Prevent SQL Injection! Never concatenate user input directly into an SQL string ("INSERT INTO users VALUES (" + name + ")"). Hackers will destroy your database. Instead, use ? placeholders. Go will safely sanitize the data.

go
12345678910111213141516
func createUser(db *sql.DB, name string, age int) {
    // The ? symbols are safe placeholders
    query := "INSERT INTO students (name, age) VALUES (?, ?)"
    
    // Exec safely binds the variables to the ? placeholders
    result, err := db.Exec(query, name, age)
    
    if err != nil {
        log.Println("Error inserting:", err)
        return
    }

    // Get the generated ID
    id, _ := result.LastInsertId()
    fmt.Println("Created new student with ID:", id)
}

6. Reading Data (db.Query)

To fetch data (SELECT), we use db.Query(). It returns "Rows" which we iterate over, scanning the columns into a Go Struct.
go
123456789101112131415161718192021222324252627282930
type Student struct {
    ID   int
    Name string
    Age  int
}

func getStudents(db *sql.DB) {
    // 1. Execute Query
    rows, err := db.Query("SELECT id, name, age FROM students")
    if err != nil {
        log.Fatal(err)
    }
    // CRITICAL: Prevent memory leaks by closing rows when done
    defer rows.Close()

    var students []Student

    // 2. Iterate through results
    for rows.Next() {
        var s Student
        // 3. Scan the current row's columns into the Struct's memory addresses
        err := rows.Scan(&s.ID, &s.Name, &s.Age)
        if err != nil {
            log.Fatal(err)
        }
        students = append(students, s)
    }

    fmt.Println("Fetched Students:", students)
}

7. Reading a Single Row (db.QueryRow)

If you are searching for an exact user by ID, use db.QueryRow().
go
12345678910111213
func getSingleStudent(db *sql.DB, id int) {
    var name string
    // QueryRow only expects 1 result. 
    err := db.QueryRow("SELECT name FROM students WHERE id = ?", id).Scan(&name)
    
    // Check if no rows were found
    if err == sql.ErrNoRows {
        fmt.Println("Student not found.")
        return
    }
    
    fmt.Println("Found:", name)
}

8. Common Mistakes

  • Forgetting defer rows.Close(): When you query multiple rows, the database connection is held hostage until you finish iterating. If an error occurs inside your loop and the function returns early without closing the rows, your database will run out of connection limits and crash.
  • Forgetting the Underscore Import: If you forget "github.com/go-sql-driver/mysql", the program will panic with sql: unknown driver "mysql".

9. Best Practices

  • Connection Pooling: sql.Open() creates a Connection Pool, not a single connection. You should only call sql.Open ONCE when your server starts, and pass that *sql.DB pointer to all your API handlers. Do not open and close the database on every single web request!

10. Exercises

  1. 1. Set up a local MySQL instance (or use SQLite).
  1. 2. Connect to it using sql.Open.
  1. 3. Create a table using db.Exec("CREATE TABLE IF NOT EXISTS test (id INT AUTOINCREMENT PRIMARY KEY, msg VARCHAR(50))").

11. MCQs with Answers & Explanations

Question 1

Which standard library package handles database interactions in Go?

Question 2

Why do we import the MySQL driver with a blank identifier (underscore _)?

Q3. Does sql.Open() actively test the connection to the database server? a) Yes b) No, it just sets up the configuration. You must call db.Ping() to actually test the connection. Answer: b) No, you must call db.Ping().
Question 4

Which method is used for SQL commands that DO NOT return rows (like INSERT or UPDATE)?

Question 5

How do you prevent SQL Injection in Go?

Question 6

Which method is used for SQL commands that return multiple rows (like SELECT)?

Question 7

What critical keyword must be used after db.Query() to prevent severe database connection leaks?

Question 8

Which method scans a specific row's column data into Go variables?

Q9. Should you call sql.Open inside every single HTTP Handler function? a) Yes b) No, you should call it once at startup and share the *sql.DB connection pool across the application. Answer: b) No, share the connection pool.
Question 10

How do you specifically check if a user wasn't found using db.QueryRow?

12. Interview Preparation

Interview Questions:
  1. 1. Explain how Connection Pooling works natively in Go's database/sql package.
  1. 2. What is the difference between db.Query and db.Exec?
  1. 3. How does Go mitigate SQL injection attacks?

13. Summary

Go's approach to databases is highly abstract and safe. By utilizing a shared *sql.DB connection pool, Go web servers can handle thousands of simultaneous database queries with low overhead. Remember to always use parameterized queries (?) for security, and rigorously close your rows to preserve memory!

14. Next Chapter Recommendation

We have built REST JSON APIs, but Go can also render full HTML pages directly. In Chapter 25: Web Development with Go, we will explore Go's built-in html/template package to build server-side rendered websites.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·