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-inventoryThis 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.
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.
Memuat komentar...