Friendly heads-up: This lesson focuses on clean code and composable logic. The domain (products/inventory) is just a vehicle to learn practical Go patterns.
What You’ll Learn
By the end, you’ll be able to:
- Model simple records with
structand build immutable-style transformations. - Write predicate functions and compose them with
And/Or/Nothelpers. - Implement filtering, sorting, limit/offset (pagination) and search.
- Render a neat ASCII table with dynamic column widths.
- Add unit tests and benchmarks for critical paths.
What We’ll Build
A command-line program that reads a small dataset of products, then applies user-provided filters:
--min-price,--max-price--category--q(substring search on name/description)--sort(e.g.,price,name,-pricefor descending)--limit,--offset
Example:
go run ./cmd/catalog \
--min-price 10 --max-price 40 \
--category "snacks" \
--q "protein" \
--sort -price \
--limit 5
Project Structure
smartfilter/
├─ cmd/catalog/main.go # CLI: parse flags, call core logic, print table
├─ internal/catalog/
│ ├─ data.go # sample data & loaders
│ ├─ filter.go # predicates, composition, Filter function
│ ├─ sort.go # sorters and parseSort
│ ├─ paginate.go # limit/offset helpers
│ ├─ render.go # table render utilities
│ └─ catalog_test.go # unit tests & benchmarks
└─ go.mod
Data Model
package catalog
type Product struct {
ID int
Name string
Category string
Price float64
InStock bool
Description string
}
func Seed() []Product {
return []Product{
{1, "Protein Bar", "snacks", 19.90, true, "High-protein bar, 20g protein"},
{2, "Trail Mix", "snacks", 12.00, true, "Almonds, raisins, dark chocolate"},
{3, "Olive Oil", "pantry", 39.50, true, "Extra virgin, cold pressed"},
{4, "Quinoa", "grains", 7.80, false, "Organic white quinoa"},
{5, "Oatmeal", "grains", 5.40, true, "Steel-cut oats"},
{6, "Protein Shake","drinks", 29.00, true, "Ready-to-drink, vanilla"},
{7, "Green Tea", "drinks", 8.50, true, "Sencha blend"},
}
}
Predicate Basics
package catalog
type Pred func(Product) bool
func And(ps ...Pred) Pred {
return func(p Product) bool {
for _, pr := range ps {
if !pr(p) { return false }
}
return true
}
}
func Or(ps ...Pred) Pred {
return func(p Product) bool {
for _, pr := range ps {
if pr(p) { return true }
}
return false
}
}
func Not(p Pred) Pred {
return func(x Product) bool { return !p(x) }
}
// Leaf predicates
func PriceMin(v float64) Pred { return func(p Product) bool { return p.Price >= v } }
func PriceMax(v float64) Pred { return func(p Product) bool { return p.Price <= v } }
func CategoryIs(c string) Pred { return func(p Product) bool { return p.Category == c } }
func InStockOnly() Pred { return func(p Product) bool { return p.InStock } }
func Query(q string) Pred {
ql := strings.ToLower(q)
return func(p Product) bool {
return strings.Contains(strings.ToLower(p.Name), ql) ||
strings.Contains(strings.ToLower(p.Description), ql)
}
}
Filter Function
func Filter(xs []Product, p Pred) []Product {
if p == nil { return append([]Product(nil), xs...) }
out := make([]Product, 0, len(xs))
for _, it := range xs {
if p(it) { out = append(out, it) }
}
return out
}
Sorting
package catalog
import "slices"
func SortBy(xs []Product, key string) {
desc := false
k := key
if strings.HasPrefix(key, "-") { desc, k = true, key[1:] }
less := func(i, j int) bool {
a, b := xs[i], xs[j]
switch k {
case "price":
if desc { return a.Price > b.Price }
return a.Price < b.Price
case "name":
if desc { return a.Name > b.Name }
return a.Name < b.Name
case "category":
if desc { return a.Category > b.Category }
return a.Category < b.Category
default:
if desc { return a.ID > b.ID }
return a.ID < b.ID
}
}
slices.SortFunc(xs, func(a, b Product) int {
if less(0, 1) { // not used directly; we’ll inline comparator below
return 0
}
return 0
})
}
A cleaner approach is to use slices.SortFunc with a comparator:
func SortBy(xs []Product, key string) {
desc := false
k := key
if strings.HasPrefix(key, "-") { desc, k = true, key[1:] }
comp := func(a, b Product) int {
var lt, gt bool
switch k {
case "price": lt, gt = a.Price < b.Price, a.Price > b.Price
case "name": lt, gt = a.Name < b.Name, a.Name > b.Name
case "category": lt, gt = a.Category < b.Category, a.Category > b.Category
default: lt, gt = a.ID < b.ID, a.ID > b.ID
}
if desc { lt, gt = gt, lt }
if lt { return -1 }
if gt { return 1 }
return 0
}
slices.SortFunc(xs, comp)
}
Pagination
func Paginate(xs []Product, offset, limit int) []Product {
if offset < 0 { offset = 0 }
if limit <= 0 { return []Product{} }
if offset >= len(xs) { return []Product{} }
end := offset + limit
if end > len(xs) { end = len(xs) }
return xs[offset:end]
}
Rendering a Clean Table
func RenderTable(xs []Product) string {
if len(xs) == 0 { return "(no results)" }
headers := []string{"ID", "Name", "Category", "Price", "Stock"}
rows := make([][]string, 0, len(xs))
for _, p := range xs {
rows = append(rows, []string{
fmt.Sprintf("%d", p.ID),
p.Name,
p.Category,
fmt.Sprintf("%.2f", p.Price),
map[bool]string{true: "Yes", false: "No"}[p.InStock],
})
}
widths := make([]int, len(headers))
for i, h := range headers { if len(h) > widths[i] { widths[i] = len(h) } }
for _, r := range rows {
for i, cell := range r {
if len(cell) > widths[i] { widths[i] = len(cell) }
}
}
var b strings.Builder
pad := func(s string, w int) string { return s + strings.Repeat(" ", w-len(s)) }
// header
for i, h := range headers {
if i > 0 { b.WriteString(" ") }
b.WriteString(pad(h, widths[i]))
}
b.WriteString("\n")
// separator
for i := range headers {
if i > 0 { b.WriteString(" ") }
b.WriteString(strings.Repeat("-", widths[i]))
}
b.WriteString("\n")
// rows
for _, r := range rows {
for i, c := range r {
if i > 0 { b.WriteString(" ") }
b.WriteString(pad(c, widths[i]))
}
b.WriteString("\n")
}
return b.String()
}
CLI Wiring (cmd/catalog/main.go)
package main
import (
"flag"
"fmt"
"strings"
"example.com/smartfilter/internal/catalog"
)
func main() {
var (
minPrice = flag.Float64("min-price", 0, "minimum price")
maxPrice = flag.Float64("max-price", 0, "maximum price (0 = no max)")
category = flag.String("category", "", "filter by category")
q = flag.String("q", "", "substring query on name/description")
sortKey = flag.String("sort", "", "sort by field (price,name,category,-price,...) ")
limit = flag.Int("limit", 10, "number of rows")
offset = flag.Int("offset", 0, "offset for pagination")
inStock = flag.Bool("in-stock", false, "only items in stock")
)
flag.Parse()
items := catalog.Seed()
// Build predicate
preds := make([]catalog.Pred, 0, 5)
if *minPrice > 0 { preds = append(preds, catalog.PriceMin(*minPrice)) }
if *maxPrice > 0 { preds = append(preds, catalog.PriceMax(*maxPrice)) }
if *category != "" { preds = append(preds, catalog.CategoryIs(strings.ToLower(*category))) }
if *q != "" { preds = append(preds, catalog.Query(*q)) }
if *inStock { preds = append(preds, catalog.InStockOnly()) }
pred := catalog.And(preds...)
filtered := catalog.Filter(items, pred)
if *sortKey != "" { catalog.SortBy(filtered, *sortKey) }
paged := catalog.Paginate(filtered, *offset, *limit)
fmt.Println(catalog.RenderTable(paged))
}
Tests & Benchmarks (internal/catalog/catalog_test.go)
package catalog
import (
"testing"
)
func TestFilterAndSort(t *testing.T) {
xs := Seed()
out := Filter(xs, And(PriceMin(8), PriceMax(20), CategoryIs("snacks")))
if len(out) == 0 { t.Fatalf("expected some results") }
SortBy(out, "-price")
if out[0].Price < out[len(out)-1].Price {
t.Fatalf("expected descending price")
}
}
func TestPaginate(t *testing.T) {
xs := Seed()
page := Paginate(xs, 2, 3)
if len(page) != 3 { t.Fatalf("want 3, got %d", len(page)) }
}
func BenchmarkQuery(b *testing.B) {
xs := Seed()
p := And(Query("pro"), PriceMin(10))
for i := 0; i < b.N; i++ {
_ = Filter(xs, p)
}
}
Run:
go test ./...
go test -bench . ./...
Nice-to-Haves
- CSV loader for real data (
encoding/csv). - Generics for
Filter[T any]and predicates over other types. - Fuzzy search with edit distance (e.g., Damerau–Levenshtein) for
--q. - Color output for stock / category highlights.
Learning Checklist
- I can model records with
struct. - I can write and compose predicates.
- I can apply filter + sort + paginate correctly.
- I can render aligned tables.
- I can write tests and basic benchmarks.
