How to Use sqlc for Type-Safe SQL in Go

sqlc generates type-safe Go code from your SQL queries, eliminating the need for manual struct mapping and reducing runtime errors caused by mismatched column names.

sqlc generates type-safe Go code from your SQL queries, eliminating the need for manual struct mapping and reducing runtime errors caused by mismatched column names. You define your queries in SQL files, run the generator, and import the resulting Go code to interact with your database using standard Go types.

First, install sqlc and configure a sqlc.yaml file to map your SQL files to a specific database dialect and output directory. This file tells the generator where to find your queries and where to place the generated Go code.

version: "2"
sql:
  - engine: "postgresql"
    queries: "queries/"
    schema: "schema/"
    gen:
      go:
        package: "db"
        out: "db"
        emit_json_tags: true
        emit_prepared_queries: false

Next, write your SQL queries in a file like queries/user.sql. You can use standard SQL with placeholders for parameters. The generator will infer the Go types based on your database schema and the query structure.

-- name: GetUserByID :one
SELECT id, name, email, created_at
FROM users
WHERE id = $1;

-- name: CreateUser :one
INSERT INTO users (name, email, created_at)
VALUES ($1, $2, $3)
RETURNING id;

Run the generator to create the Go code. This command reads your configuration and SQL files, then outputs a db package containing query functions, struct definitions, and type-safe argument builders.

sqlc generate

In your Go application, import the generated package and use the query functions. The generated code handles the sql.Null types and struct mapping automatically. If you change your SQL or schema, simply re-run sqlc generate to update the Go code, ensuring your application always matches your database definition.

package main

import (
    "context"
    "database/sql"
    "log"
    "myapp/db"
)

func main() {
    dbConn, err := sql.Open("pgx", "postgres://user:pass@localhost:5432/mydb")
    if err != nil {
        log.Fatal(err)
    }
    defer dbConn.Close()

    q := db.New(dbConn)
    ctx := context.Background()

    // Type-safe query execution
    user, err := q.GetUserByID(ctx, 1)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("User: %s (%s)", user.Name, user.Email)

    // Insert with automatic type handling
    newUser, err := q.CreateUser(ctx, db.CreateUserParams{
        Name:        "Alice",
        Email:       "alice@example.com",
        CreatedAt:   sql.NullTime{Time: time.Now(), Valid: true},
    })
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Created user ID: %d", newUser.ID)
}

This approach keeps your SQL logic explicit and readable while leveraging Go's static type checking to catch errors at compile time rather than runtime.