Before You Begin

  • Go installed (version 1.22 or later). Download
  • A text editor (VS Code with Go extension, GoLand, or plain vim+goimports)
  • Terminal comfort (navigating directories, running commands)

No prior Go experience? You'll be fine. The starting point is zero, but the destination is a working CLI tool you'll actually use.

Why Not a Spreadsheet?

Spreadsheets are great for viewing data, terrible for interacting with it programmatically. You can't script a spreadsheet to validate input, auto-calculate prices, or export inventory in JSON without macro hell. A CLI tool in Go solves exactly this: fast startup, no GUI overhead, and composable with other Unix tools.

This tutorial compares Go's data structures to spreadsheet concepts so the mental model sticks. A struct is a row template. A slice is the entire sheet. A method is a custom formula.

Step 1: Project Init – The Empty Sheet

Go modules are like naming your spreadsheet file. They keep dependencies tracked and prevent "it works on my machine" problems.

mkdir gem-inventory
cd gem-inventory
go mod init github.com/yourname/gem-inventory

This creates go.mod. Think of it as the metadata row at the top of your sheet. Now create main.go:

package main

import "fmt"

func main() {
    fmt.Println("Gem Inventory v0.1")
}

Run go run main.go. If you see the message, your Go installation works.

Step 2: Define Columns with Structs

A spreadsheet has columns: Name, Carat, Color, Price. In Go, these become fields in a struct. The struct defines the shape of one row.

type Gemstone struct {
    Name  string
    Carat float64
    Color string
    Price float64
}

Compare this to a spreadsheet row: | Name | Carat | Color | Price | |------|-------|-------|-------| | Ruby | 2.5 | Red | 4500 |

The struct's field names match the column headers. Each field has a type: strings for text, float64 for numbers.

Now populate one row inside main():

ruby := Gemstone{
    Name:  "Ruby",
    Carat: 2.5,
    Color: "Red",
    Price: 4500.00,
}

fmt.Printf("%s: %.1f carat %s - $%.2f\n", ruby.Name, ruby.Carat, ruby.Color, ruby.Price)

Run it. You've just printed your first structured row.

Step 3: Build the Sheet with Slices

A single row is useless. Spreadsheets hold thousands of rows. Go uses slices – dynamic arrays that grow as needed.

var collection []Gemstone

collection = append(collection, Gemstone{
    Name:  "Sapphire",
    Carat: 3.0,
    Color: "Blue",
    Price: 6000,
})
collection = append(collection, Gemstone{
    Name:  "Emerald",
    Carat: 1.8,
    Color: "Green",
    Price: 3200,
})

Now loop over the sheet:

for i, gem := range collection {
    fmt.Printf("%d. %s\n", i+1, gem)
}

But gem prints like {Sapphire 3 Blue 6000} — ugly. We need a custom display formula.

Step 4: Add a Display Formula (Methods)

In spreadsheets, you write formulas like =CONCAT(A2," (",B2," ct, ",C2,") - $",D2). In Go, you attach a method to the struct.

func (g Gemstone) String() string {
    return fmt.Sprintf("%s (%.1f ct, %s) - $%.2f", g.Name, g.Carat, g.Color, g.Price)
}

Now fmt.Printf("%s\n", gem) automatically calls String(). No explicit call needed. This is Go's version of a custom cell format.

Step 5: Interactive Commands – The CLI Loop

A spreadsheet responds to keystrokes. Our CLI responds to typed commands. We'll build a loop that reads user input and dispatches actions.

func main() {
    collection := []Gemstone{
        {Name: "Amethyst", Carat: 1.2, Color: "Purple", Price: 250},
        {Name: "Topaz", Carat: 0.9, Color: "Yellow", Price: 180},
    }

    fmt.Println("Gem Inventory Manager")
    fmt.Println("Commands: add, list, total, quit")

    scanner := bufio.NewScanner(os.Stdin)
    for {
        fmt.Print("> ")
        if !scanner.Scan() {
            break
        }
        cmd := scanner.Text()

        switch cmd {
        case "add":
            var name, color string
            var carat, price float64

            fmt.Print("Name: ")
            scanner.Scan()
            name = scanner.Text()

            fmt.Print("Carat: ")
            fmt.Scanf("%f\n", &carat)

            fmt.Print("Color: ")
            scanner.Scan()
            color = scanner.Text()

            fmt.Print("Price: ")
            fmt.Scanf("%f\n", &price)

            collection = append(collection, Gemstone{
                Name:  name,
                Carat: carat,
                Color: color,
                Price: price,
            })
            fmt.Println("Added!")

        case "list":
            fmt.Println("\nCurrent Inventory:")
            for i, gem := range collection {
                fmt.Printf("%d. %s\n", i+1, gem)
            }
            fmt.Println()

        case "total":
            var sum float64
            for _, gem := range collection {
                sum += gem.Price
            }
            fmt.Printf("Total inventory value: $%.2f\n", sum)

        case "quit":
            fmt.Println("Goodbye!")
            return

        default:
            fmt.Println("Unknown command. Try: add, list, total, quit")
        }
    }
}

Add imports: "bufio", "os", "fmt". Run it. You now have a working inventory manager.

Step 6: Save and Load – Persistence

Spreadsheets save to disk. Our CLI should too. We'll write the collection to a JSON file on quit and load it on startup.

First, add JSON tags to the struct:

type Gemstone struct {
    Name  string  `json:"name"`
    Carat float64 `json:"carat"`
    Color string  `json:"color"`
    Price float64 `json:"price"`
}

Now save/load functions:

import (
    "bufio"
    "encoding/json"
    "fmt"
    "os"
)

const dataFile = "inventory.json"

func saveCollection(collection []Gemstone) error {
    data, err := json.MarshalIndent(collection, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(dataFile, data, 0644)
}

func loadCollection() ([]Gemstone, error) {
    data, err := os.ReadFile(dataFile)
    if err != nil {
        if os.IsNotExist(err) {
            return []Gemstone{}, nil
        }
        return nil, err
    }
    var collection []Gemstone
    err = json.Unmarshal(data, &collection)
    return collection, err
}

Modify main() to load on start and save before quit:

func main() {
    collection, err := loadCollection()
    if err != nil {
        fmt.Println("Error loading inventory:", err)
        return
    }

    // ... rest of the loop ...

    case "quit":
        err := saveCollection(collection)
        if err != nil {
            fmt.Println("Error saving:", err)
        }
        fmt.Println("Goodbye!")
        return
}

Now your inventory persists between sessions. Run once, add a gem, quit, run again, type list – it's there.

Step 7: Search and Filter – Advanced Queries

Spreadsheets have filters. Let's add a search by color:

case "search":
    fmt.Print("Enter color to search: ")
    scanner.Scan()
    query := scanner.Text()
    fmt.Printf("\nGemstones with color '%s':\n", query)
    found := false
    for i, gem := range collection {
        if gem.Color == query {
            fmt.Printf("%d. %s\n", i+1, gem)
            found = true
        }
    }
    if !found {
        fmt.Println("No matches found.")
    }
    fmt.Println()

Add this to the switch statement. Now you can filter your inventory by color.

Step 8: Error Handling – Bulletproofing

Real spreadsheets don't crash when you type text in a number column. Our CLI should handle bad input gracefully.

Replace the fmt.Scanf calls with safer parsing:

fmt.Print("Carat: ")
for {
    scanner.Scan()
    input := scanner.Text()
    carat, err := strconv.ParseFloat(input, 64)
    if err != nil {
        fmt.Print("Invalid number. Enter carat: ")
        continue
    }
    break
}

This loops until valid input. Add "strconv" to imports.

Common Issues

Issue: go mod init fails lution: Ensure no existing go.mod in parent directories. Use a clean folder.

Issue: json.MarshalIndent returns empty array [] lution: Ensure slice is not nil. Initialize with make([]Gemstone, 0) or use var collection []Gemstone properly.

Issue: fmt.Scanf leaves newline in buffer lution: Prefer bufio.Scanner for user input. Mixing Scanf and Scanner causes skipped lines.

Issue: # command-line-arguments build errors lution: Check package main declaration. All files in main package must share the same package name.

Sponsored Deal

Extensions

  • Sort by price or carat
  • Delete a gemstone by index
  • Export to CSV for spreadsheet import
  • Add categories (cut, clarity) as nested structs

Go's strength is building fast, reliable CLI tools. This inventory manager is a foundation – extend it to manage anything: books, vinyl records, server inventory. The pattern is the same.