# Lesson 4 — User Model & Repository Pattern > **New Go concepts in this lesson:** applying pointers and pointer > receivers for real (not just toy examples), sentinel errors in > practice, `QueryRowContext` vs `QueryContext`. Make sure you've done > the pointers section of `00-go-basics-2-functions-structs-pointers.md` > and the errors section of `00-go-basics-3-...md` before this lesson — > everything here depends on both. ## Quick pointer refresher, applied Two rules from Go Basics you'll use constantly in this lesson: 1. If a function needs to **write a result back** into the caller's variable, it must take a pointer (`*Book`), and write through it (`b.ID = ...`). 2. If a struct wraps something stateful/shared (like a database connection pool), methods on it should use a **pointer receiver** (`func (r *BookRepository) ...`), so every call operates on the same underlying resource instead of an accidental copy. Keep those two rules in mind as you read the code below — they explain almost every `*` you'll see in this lesson. ## Part A — standalone playground We'll practice the **repository pattern**: separating "how do I talk to the database" from "what does my business logic do." Reuse the MySQL container from Lesson 3, or start fresh: ```bash docker run --name mysql-demo2 -e MYSQL_ROOT_PASSWORD=devpass -e MYSQL_DATABASE=demo -p 3306:3306 -d mysql:9 mkdir ~/go-playground/repo-demo && cd ~/go-playground/repo-demo go mod init repo-demo go get github.com/go-sql-driver/mysql@latest ``` **`main.go`** ```go package main import ( "context" "database/sql" "errors" "fmt" "log" "time" _ "github.com/go-sql-driver/mysql" ) // 1. The domain model - a plain struct representing one "thing" in your // app. No database code here at all - this is just data. type Book struct { ID int Title string Author string CreatedAt time.Time } // 2. The repository - a struct that wraps *sql.DB and knows how to turn // SQL rows into Book structs, and Book structs into SQL writes. type BookRepository struct { db *sql.DB } // Constructor function - Go convention: NewXxx returns a *Xxx func NewBookRepository(db *sql.DB) *BookRepository { return &BookRepository{db: db} } var ErrNotFound = errors.New("book not found") // 3. Pointer receiver: (r *BookRepository) - because we don't want to // copy the struct (it holds a *sql.DB) on every single method call. func (r *BookRepository) Create(ctx context.Context, b *Book) error { res, err := r.db.ExecContext(ctx, "INSERT INTO books (title, author, created_at) VALUES (?, ?, ?)", b.Title, b.Author, time.Now(), ) if err != nil { return fmt.Errorf("insert book: %w", err) } id, err := res.LastInsertId() if err != nil { return fmt.Errorf("get last insert id: %w", err) } b.ID = int(id) // write the new ID back into the caller's Book return nil } func (r *BookRepository) FindByID(ctx context.Context, id int) (*Book, error) { var b Book err := r.db.QueryRowContext(ctx, "SELECT id, title, author, created_at FROM books WHERE id = ?", id, ).Scan(&b.ID, &b.Title, &b.Author, &b.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("find book: %w", err) } return &b, nil } func main() { db, err := sql.Open("mysql", "root:devpass@tcp(127.0.0.1:3306)/demo?parseTime=true") if err != nil { log.Fatal(err) } defer db.Close() ctx := context.Background() if _, err := db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS books ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, author VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL )`); err != nil { log.Fatal(err) } repo := NewBookRepository(db) b := &Book{Title: "The Go Programming Language", Author: "Donovan & Kernighan"} if err := repo.Create(ctx, b); err != nil { log.Fatal(err) } log.Printf("created book with id %d", b.ID) found, err := repo.FindByID(ctx, b.ID) if err != nil { log.Fatal(err) } log.Printf("found: %+v", found) _, err = repo.FindByID(ctx, 999999) if errors.Is(err, ErrNotFound) { log.Println("correctly got ErrNotFound for missing book") } } ``` Run it: ```bash go run . ``` What's new here: - **`Book` struct has zero database knowledge** — it's pure data. Your handlers/business logic will work with `Book`, never with raw SQL rows directly. - **`BookRepository` wraps `*sql.DB`** and is the *only* place SQL queries live. Swap MySQL for Postgres later, and you change this one file, not every handler. - **`NewBookRepository(db)` constructor** — Go has no classes/constructors as a language feature; `NewXxx` returning `*Xxx` is purely a naming convention, but the entire ecosystem follows it, so you should too. - **`func (r *BookRepository) Create(...)`** — pointer receiver, per the refresher above. `r` is the repository itself; inside, `r.db` accesses the wrapped connection pool. - **`b.ID = int(id)`** — since `Create` takes `b *Book` (a pointer), it can write the newly generated ID directly back into the caller's struct. This is rule #1 from the refresher, applied for real. - **`QueryRowContext(...).Scan(...)`** — new: `QueryRowContext` (singular `Row`) is for when you expect exactly one result, like a lookup by ID. It skips the `rows.Next()`/`rows.Close()` dance from Lesson 3 since there's at most one row. - **`errors.Is(err, sql.ErrNoRows)`** — `sql.ErrNoRows` is the driver's own sentinel error for "query matched zero rows." We translate it into our own `ErrNotFound` so callers of `FindByID` don't need to know or care that the underlying storage is SQL at all — this is the sentinel error pattern from Go Basics Part 3, put to real use. - **`var ErrNotFound = errors.New(...)`** — a package-level sentinel error, so callers can check `errors.Is(err, ErrNotFound)` without caring what's underneath. Try inserting a second book, querying it, then calling `FindByID` with an ID you know doesn't exist and confirm you get `ErrNotFound`, not a crash. ## Part B — apply it to the project **`internal/models/user.go`** — the domain struct: ```go package models import "time" type User struct { ID int Email string PasswordHash string GoogleID string // empty if the user registered with a password CreatedAt time.Time } ``` `PasswordHash`, not `Password` — we will **never** store or handle plaintext passwords beyond the brief moment they're hashed (Lesson 5). `GoogleID` is here now so Lesson 7 (Google OAuth) doesn't require restructuring this struct later. **`internal/database/migrate.go`** — creates the table on startup (fine for a learning project; a real project would use a dedicated migration tool): ```go package database import ( "context" "database/sql" "fmt" ) func Migrate(ctx context.Context, db *sql.DB) error { _, err := db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL DEFAULT '', google_id VARCHAR(255) NOT NULL DEFAULT '', created_at DATETIME NOT NULL )`) if err != nil { return fmt.Errorf("migrate users table: %w", err) } return nil } ``` **`internal/models/user_repository.go`** — same pattern as `BookRepository`: ```go package models import ( "context" "database/sql" "errors" "fmt" "time" ) var ErrUserNotFound = errors.New("user not found") type UserRepository struct { db *sql.DB } func NewUserRepository(db *sql.DB) *UserRepository { return &UserRepository{db: db} } func (r *UserRepository) Create(ctx context.Context, u *User) error { res, err := r.db.ExecContext(ctx, "INSERT INTO users (email, password_hash, google_id, created_at) VALUES (?, ?, ?, ?)", u.Email, u.PasswordHash, u.GoogleID, time.Now(), ) if err != nil { return fmt.Errorf("create user: %w", err) } id, err := res.LastInsertId() if err != nil { return fmt.Errorf("get last insert id: %w", err) } u.ID = int(id) return nil } func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*User, error) { var u User err := r.db.QueryRowContext(ctx, "SELECT id, email, password_hash, google_id, created_at FROM users WHERE email = ?", email, ).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.GoogleID, &u.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrUserNotFound } if err != nil { return nil, fmt.Errorf("find user by email: %w", err) } return &u, nil } func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) { var u User err := r.db.QueryRowContext(ctx, "SELECT id, email, password_hash, google_id, created_at FROM users WHERE id = ?", id, ).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.GoogleID, &u.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrUserNotFound } if err != nil { return nil, fmt.Errorf("find user by id: %w", err) } return &u, nil } ``` `FindByEmail` is used during login (users log in with an email, not an ID). `FindByID` is used later once sessions store just the user's ID (Lesson 6+). **Update `cmd/api/main.go`** — run the migration and construct the repository on startup: ```go if err := database.Migrate(ctx, db); err != nil { logger.Error("failed to migrate database", "error", err) os.Exit(1) } logger.Info("database migrated") userRepo := models.NewUserRepository(db) _ = userRepo // used starting Lesson 5 - silences "declared but not used" for now ``` (Add `"git.hamidsoltani.com/hamid/go-simple-api/internal/models"` to the import block.) `_ = userRepo` — remember from Go Basics Part 1, the blank identifier `_` discards a value so the compiler doesn't complain about an unused variable. We're not wiring `userRepo` into any handler yet (that's Lesson 5), so this line is a temporary placeholder — delete it once `userRepo` is actually passed into `router.New(...)`. ## Try it ```bash go run ./cmd/api ``` Check your logs for `"database migrated"`, then confirm the table exists: ```bash docker exec -it mysql-api mysql -uroot -pdevpass go_simple_api -e "DESCRIBE users;" ``` Once both parts run, move to Lesson 5 — password-based register/login with bcrypt, which is where `userRepo` finally gets used for real.