}

How to Use Golang with PostgreSQL - Complete Guide

Introduction

Go (Golang) provides excellent support for working with PostgreSQL through the database/sql package and the pq driver. This tutorial covers everything you need to connect to PostgreSQL, perform CRUD operations, and handle transactions safely.

Installing the pq Driver

First, install the PostgreSQL driver for Go:

go get github.com/lib/pq

The pq driver implements Go's database/sql interface, so you'll primarily work with the standard library's database package.

Connecting to PostgreSQL

Here's how to establish a connection to your PostgreSQL database:

package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
)

func main() {
    connStr := "host=localhost port=5432 user=myuser password=mypassword dbname=mydb sslmode=disable"

    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal("Failed to open database:", err)
    }
    defer db.Close()

    // Verify the connection
    err = db.Ping()
    if err != nil {
        log.Fatal("Failed to connect:", err)
    }

    fmt.Println("Connected to PostgreSQL!")
}

Connection String Formats:

You can also use a URL format:

connStr := "postgres://user:password@localhost:5432/dbname?sslmode=disable"

Basic CRUD Operations

Creating a Table

func createTable(db *sql.DB) error {
    query := `
    CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )`

    _, err := db.Exec(query)
    return err
}

Insert (Create)

func insertUser(db *sql.DB, name, email string) (int, error) {
    var id int
    query := `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id`

    err := db.QueryRow(query, name, email).Scan(&id)
    if err != nil {
        return 0, err
    }
    return id, nil
}

Note: PostgreSQL uses $1, $2, $3 for parameter placeholders, not ? like MySQL.

Select (Read)

Single row:

func getUserByID(db *sql.DB, id int) (string, string, error) {
    var name, email string
    query := `SELECT name, email FROM users WHERE id = $1`

    err := db.QueryRow(query, id).Scan(&name, &email)
    if err == sql.ErrNoRows {
        return "", "", fmt.Errorf("user %d not found", id)
    }
    return name, email, err
}

Multiple rows:

func getAllUsers(db *sql.DB) error {
    rows, err := db.Query("SELECT id, name, email FROM users")
    if err != nil {
        return err
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name, email string
        if err := rows.Scan(&id, &name, &email); err != nil {
            return err
        }
        fmt.Printf("ID: %d, Name: %s, Email: %s\n", id, name, email)
    }
    return rows.Err()
}

Update

func updateUserEmail(db *sql.DB, id int, newEmail string) error {
    query := `UPDATE users SET email = $1 WHERE id = $2`

    result, err := db.Exec(query, newEmail, id)
    if err != nil {
        return err
    }

    rowsAffected, _ := result.RowsAffected()
    if rowsAffected == 0 {
        return fmt.Errorf("user %d not found", id)
    }
    return nil
}

Delete

func deleteUser(db *sql.DB, id int) error {
    query := `DELETE FROM users WHERE id = $1`

    result, err := db.Exec(query, id)
    if err != nil {
        return err
    }

    rowsAffected, _ := result.RowsAffected()
    if rowsAffected == 0 {
        return fmt.Errorf("user %d not found", id)
    }
    return nil
}

Prepared Statements

Prepared statements improve performance for repeated queries and protect against SQL injection:

func insertMultipleUsers(db *sql.DB, users []User) error {
    stmt, err := db.Prepare("INSERT INTO users (name, email) VALUES ($1, $2)")
    if err != nil {
        return err
    }
    defer stmt.Close()

    for _, user := range users {
        _, err := stmt.Exec(user.Name, user.Email)
        if err != nil {
            return err
        }
    }
    return nil
}

Transactions

Use transactions when you need multiple operations to succeed or fail together:

func transferCredits(db *sql.DB, fromID, toID, amount int) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    // Rollback if we return early due to error
    defer tx.Rollback()

    // Deduct from sender
    _, err = tx.Exec("UPDATE accounts SET credits = credits - $1 WHERE id = $2", amount, fromID)
    if err != nil {
        return err
    }

    // Add to receiver
    _, err = tx.Exec("UPDATE accounts SET credits = credits + $1 WHERE id = $2", amount, toID)
    if err != nil {
        return err
    }

    // Commit the transaction
    return tx.Commit()
}

Error Handling

Always handle database errors properly. Common patterns:

import "github.com/lib/pq"

func insertUser(db *sql.DB, name, email string) error {
    _, err := db.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", name, email)
    if err != nil {
        // Check for unique constraint violation
        if pqErr, ok := err.(*pq.Error); ok {
            if pqErr.Code == "23505" { // unique_violation
                return fmt.Errorf("email already exists")
            }
        }
        return err
    }
    return nil
}

Connection Pool Settings

Configure the connection pool for production:

db.SetMaxOpenConns(25)                 // Maximum open connections
db.SetMaxIdleConns(5)                  // Maximum idle connections
db.SetConnMaxLifetime(5 * time.Minute) // Connection max lifetime

Conclusion

You now have a solid foundation for working with PostgreSQL in Go. The database/sql package combined with the pq driver provides a clean, efficient interface for database operations. Remember to always use parameterized queries to prevent SQL injection, handle errors appropriately, and use transactions for operations that must be atomic.

For more advanced use cases, consider ORMs like GORM or query builders like sqlx that provide additional features while still supporting PostgreSQL.