Go Bookshelf - HeyItWorked/babel-shelf GitHub Wiki
Go Bookshelf
Port: 8080 | Framework: stdlib
net/http| DB driver:lib/pq
Go is a compiled, statically-typed language designed at Google for backend services. It's intentionally simple — no generics until recently, no exceptions, no inheritance. The philosophy is: "clear is better than clever."
Project Structure
go-bookshelf/
├── main.go ← Entry point: connect DB, set up routes, start server
├── models.go ← Book struct + status constants
├── db.go ← SQL queries (translator layer)
├── handlers.go ← HTTP handlers (logic layer)
├── handlers_test.go ← Integration tests (8 cases)
├── test_helpers_test.go ← Test utilities (setup, request helpers)
├── go.mod ← Dependency manifest (like requirements.txt)
├── go.sum ← Lock file (like pip freeze output)
└── Dockerfile
Go convention: everything in one package (package main) for small apps. No src/ folder — files at the root are the source.
Key Concepts — With Python Translations
1. Structs (Go's version of classes, but without methods attached)
// Go — models.go
type Book struct {
Id int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Status string `json:"status"`
}
# Python equivalent
from dataclasses import dataclass
@dataclass
class Book:
id: int
title: str
author: str
status: str
What's different: Go structs have no methods defined inside them. Methods are defined separately:
// Go — methods are declared outside the struct
func (b Book) IsFinished() bool {
return b.Status == "finished"
}
# Python — methods live inside the class
class Book:
def is_finished(self) -> bool:
return self.status == "finished"
The backtick tags (`json:"id"`) are struct tags — metadata that tells the JSON encoder what key names to use. Python's dataclass does this automatically by field name.
2. The if err != nil Pattern (Go's error handling)
Go has no exceptions. Every function that can fail returns an error as a second value. You must check it every single time.
// Go — db.go
book, err := insertBook(book)
if err != nil {
http.Error(w, "could not create book", http.StatusInternalServerError)
return
}
# Python equivalent — try/except
try:
book = insert_book(book)
except Exception as e:
return Response("could not create book", status=500)
Why Go does this: Exceptions create invisible control flow — any function can throw and you won't know unless you read the docs. Go makes errors explicit. You see every error path in the code. The tradeoff is verbosity — you'll write if err != nil hundreds of times.
# What Go's error pattern would look like if Python did it:
book, err = insert_book(book)
if err is not None:
return Response("could not create book", status=500)
3. Multiple Return Values
Go functions can return multiple values — typically (result, error):
// Go
func getBookById(id int) (Book, error) {
var book Book
err := row.Scan(&book.Id, &book.Title, &book.Author, &book.Status)
if err != nil {
return Book{}, err // return zero-value Book + the error
}
return book, nil // return the book + no error
}
# Python — tuple unpacking
def get_book_by_id(id: int) -> tuple[Book | None, str | None]:
try:
book = ...
return book, None
except Exception as e:
return None, str(e)
# But nobody writes Python this way — we just use exceptions
4. Pointers and & (The Scan Pattern)
The & operator appears everywhere in db.go. It means "give me the memory address of this variable" so the function can write into it.
// Go — db.go
err := row.Scan(&book.Id, &book.Title, &book.Author, &book.Status)
Scan fills in book's fields by writing directly to their memory addresses. Without &, Scan would get copies and the original book wouldn't change.
# Python — no equivalent needed
# Python passes objects by reference, so mutation just works:
book.id, book.title, book.author, book.status = row # tuple unpacking
Mental model: &book.Id means "here's my mailbox address — put the data there." Without &, it's "here's a photocopy of my mailbox — whatever you put in it, I'll never see."
5. Package-Level Variables (Shared State)
// Go — main.go
var db *sql.DB // package-level variable — accessible from all files in package main
All files with package main share this db variable. main.go sets it, db.go uses it.
# Python equivalent — module-level variable
# db.py
_connection: Connection | None = None
def set_connection(conn: Connection):
global _connection
_connection = conn
Why this matters for Gleam: Gleam has no mutable global state. The db connection must be passed explicitly as a function argument everywhere. This is actually safer but more verbose.
6. HTTP Handler Signature
Every Go HTTP handler has the same signature — (w http.ResponseWriter, r *http.Request):
// Go
func CreateBook(w http.ResponseWriter, r *http.Request) {
// w = where you write the response (like a notepad you hand back)
// r = the incoming request (what the client sent you)
}
# Python Flask equivalent
@app.post("/books")
def create_book():
# request is implicit (flask.request global)
# response is the return value
return jsonify(book), 201
Key difference: Go separates reading (from r) and writing (to w). Python frameworks typically read from a request object and return a response. TypeScript's Hono uses a Context object that combines both.
7. defer (Cleanup Guarantee)
// Go — db.go
rows, err := db.Query("SELECT ...")
defer rows.Close() // runs when the function exits, no matter what
# Python equivalent — context manager
with db.execute("SELECT ...") as rows:
... # rows.close() called automatically when block exits
defer is Go's version of Python's with statement — it guarantees cleanup even if the function panics.
8. The Blank Import _
// Go — main.go
import _ "github.com/lib/pq" // import for side effects only
This imports the Postgres driver but we never call it directly. The package registers itself with database/sql during init(). It's like a plugin system.
# Python equivalent — importing for side effects
import codecs # registers codecs even if you never call codecs.X directly
Go Conventions Used in This Project
| Convention | Example | Why |
|---|---|---|
| Uppercase = exported | CreateBook |
Other packages can call it (like Python's public) |
| Lowercase = unexported | parseId |
Package-private (like Python's _private) |
_test.go suffix |
handlers_test.go |
Go only runs tests from these files |
TestMain |
test_helpers_test.go:22 |
Runs once before all tests (like pytest's conftest.py) |
| Short variable names | w, r, rr, mux |
Go prefers brevity in local scope |
err shadow |
err := ... then err = ... |
Same name reused for sequential errors |
Adjacent Learning: Things Worth Knowing
Why No Framework?
Go's standard library net/http is production-ready. Unlike Python (where you'd never ship raw http.server), Go's stdlib HTTP server handles:
- Routing with method matching (
"GET /books/{id}"— added in Go 1.22) - Concurrent request handling (goroutines)
- Graceful shutdown
Most Go teams debate whether to use a framework at all. The stdlib often wins.
Goroutines (Not in This Project, But Fundamental)
Go handles concurrency with goroutines — lightweight threads managed by the Go runtime:
go processBook(book) // runs in the background, costs ~2KB of memory
# Python equivalent — but way heavier
import threading
threading.Thread(target=process_book, args=(book,)).start() # ~1MB per thread
Our bookshelf app doesn't use explicit goroutines, but http.ListenAndServe creates one per incoming request automatically.
Zero Values (Why Go Has No null)
Every type has a zero value — int is 0, string is "", bool is false, structs have all fields zeroed. There's no null for value types.
var book Book // {Id: 0, Title: "", Author: "", Status: ""}
This is why getBookById returns Book{} on error — it's the zero value, not null. Pointers can be nil though (*sql.DB can be nil), which is how db starts before main() connects.
go.mod vs requirements.txt
module github.com/HeyItWorked/babel-shelf/go-bookshelf
go 1.22
require github.com/lib/pq v1.10.9
module= this project's import path (like a package name)require= dependencies with exact versionsgo.sum= checksums for verification (like a lock file, but for integrity)
No virtual environment needed — Go downloads packages to $GOPATH/pkg/mod/ globally but resolves per-project.
Common Gotchas (From This Codebase)
1. QueryRow vs Query
QueryRow= one row, auto-cleanup after.Scan()Query= many rows, you mustdefer rows.Close()or you'll leak connections
2. Exec vs QueryRow for DELETE
// DELETE returns nothing, so use Exec (not QueryRow)
result, err := db.Exec("DELETE FROM books WHERE id = $1", id)
rows, _ := result.RowsAffected() // check if anything was actually deleted
3. json.NewDecoder vs json.Unmarshal
Both decode JSON. NewDecoder reads from a stream (like r.Body), Unmarshal reads from a byte slice. For HTTP bodies, NewDecoder is standard.
4. Tests share package state
Because all test files are package main, they share the db variable. TestMain connects once, all tests reuse it. This means tests must not run in parallel if they modify the same rows.