Package Explorer

Exercise: Package Explorer

Difficulty - Beginner

Learning Objectives

  • Understand package structure and organization
  • Master import statements and aliases
  • Learn about package visibility
  • Practice creating modular code
  • Understand package initialization

Problem Statement

Create a multi-package application that demonstrates proper package organization and usage. You'll build a simple library management system with separate packages for different concerns.

Package Structure

libraryapp/
├── go.mod
├── main.go
├── models/
│   └── book.go
├── storage/
│   └── library.go
└── utils/
    ├── formatter.go
    └── validator.go

Package: models

 1// models/book.go
 2package models
 3
 4// Book represents a book in the library
 5type Book struct {
 6	ISBN   string
 7	Title  string
 8	Author string
 9	Year   int
10	price  float64 // unexported field
11}
12
13// NewBook creates a new book with validation
14func NewBook(isbn, title, author string, year int, price float64)
15
16// GetPrice returns the book's price
17func GetPrice() float64
18
19// SetPrice sets the book's price with validation
20func SetPrice(price float64) error
21
22// String returns a formatted string representation
23func String() string

Package: storage

 1// storage/library.go
 2package storage
 3
 4import "libraryapp/models"
 5
 6// Library manages a collection of books
 7type Library struct {
 8	books map[string]*models.Book
 9	name  string
10}
11
12// NewLibrary creates a new library
13func NewLibrary(name string) *Library
14
15// AddBook adds a book to the library
16func AddBook(book *models.Book) error
17
18// GetBook retrieves a book by ISBN
19func GetBook(isbn string)
20
21// RemoveBook removes a book by ISBN
22func RemoveBook(isbn string) error
23
24// ListBooks returns all books in the library
25func ListBooks() []*models.Book
26
27// GetName returns the library name
28func GetName() string

Package: utils

 1// utils/formatter.go
 2package utils
 3
 4import "libraryapp/models"
 5
 6// FormatBookList returns a formatted string of books
 7func FormatBookList(books []*models.Book) string
 8
 9// FormatPrice formats a price with currency symbol
10func FormatPrice(price float64) string
 1// utils/validator.go
 2package utils
 3
 4// ValidateISBN checks if an ISBN is valid
 5func ValidateISBN(isbn string) bool
 6
 7// ValidateYear checks if a year is reasonable
 8func ValidateYear(year int) bool
 9
10// ValidatePrice checks if a price is valid
11func ValidatePrice(price float64) bool

Requirements

  1. Package Organization:

    • Each package should have a clear responsibility
    • Use proper naming conventions
    • Maintain package-level documentation
  2. Visibility:

    • Book price should be unexported
    • Provide getter/setter methods for price
    • All validation functions should be exported
  3. Imports:

    • Use proper import paths
    • Demonstrate import aliases if needed
    • Avoid circular dependencies
  4. Validation:

    • ISBN must be 10 or 13 characters
    • Year must be between 1000 and current year + 1
    • Price must be positive
  5. Error Handling:

    • Return errors for validation failures
    • Use custom error messages
    • Handle nil pointers appropriately

Example Usage

 1// main.go
 2package main
 3
 4import (
 5	"fmt"
 6	"log"
 7
 8	"libraryapp/models"
 9	"libraryapp/storage"
10	"libraryapp/utils"
11)
12
13func main() {
14	// Create a library
15	lib := storage.NewLibrary("City Library")
16	fmt.Printf("Welcome to %s\n", lib.GetName())
17
18	// Create books
19	book1, err := models.NewBook(
20		"978-0134190440",
21		"The Go Programming Language",
22		"Alan Donovan",
23		2015,
24		44.99,
25	)
26	if err != nil {
27		log.Fatal(err)
28	}
29
30	book2, err := models.NewBook(
31		"978-1617291609",
32		"Go in Action",
33		"William Kennedy",
34		2016,
35		39.99,
36	)
37	if err != nil {
38		log.Fatal(err)
39	}
40
41	// Add books to library
42	lib.AddBook(book1)
43	lib.AddBook(book2)
44
45	// List all books
46	books := lib.ListBooks()
47	fmt.Println("\nLibrary Collection:")
48	fmt.Println(utils.FormatBookList(books))
49
50	// Get specific book
51	book, err := lib.GetBook("978-0134190440")
52	if err != nil {
53		log.Fatal(err)
54	}
55	fmt.Printf("\nFound: %s - %s\n", book.Title, utils.FormatPrice(book.GetPrice()))
56
57	// Update price
58	err = book.SetPrice(34.99)
59	if err != nil {
60		log.Fatal(err)
61	}
62	fmt.Printf("Updated price: %s\n", utils.FormatPrice(book.GetPrice()))
63
64	// Remove book
65	err = lib.RemoveBook("978-1617291609")
66	if err != nil {
67		log.Fatal(err)
68	}
69
70	fmt.Println("\nRemaining books:")
71	fmt.Println(utils.FormatBookList(lib.ListBooks()))
72}

Expected Output:

Welcome to City Library

Library Collection:
1. The Go Programming Language by Alan Donovan - $44.99
2. Go in Action by William Kennedy - $39.99

Found: The Go Programming Language - $44.99
Updated price: $34.99

Remaining books:
1. The Go Programming Language by Alan Donovan - $34.99

Test Cases

 1package models_test
 2
 3import (
 4	"libraryapp/models"
 5	"testing"
 6)
 7
 8func TestNewBook(t *testing.T) {
 9	book, err := models.NewBook("978-0134190440", "Test Book", "Author", 2020, 29.99)
10	if err != nil {
11		t.Fatalf("NewBook failed: %v", err)
12	}
13
14	if book.ISBN != "978-0134190440" {
15		t.Errorf("Expected ISBN 978-0134190440, got %s", book.ISBN)
16	}
17}
18
19func TestBookPriceEncapsulation(t *testing.T) {
20	book, _ := models.NewBook("1234567890", "Test", "Author", 2020, 29.99)
21
22	// Test getter
23	if price := book.GetPrice(); price != 29.99 {
24		t.Errorf("Expected price 29.99, got %f", price)
25	}
26
27	// Test setter
28	err := book.SetPrice(39.99)
29	if err != nil {
30		t.Errorf("SetPrice failed: %v", err)
31	}
32
33	if price := book.GetPrice(); price != 39.99 {
34		t.Errorf("Expected price 39.99, got %f", price)
35	}
36
37	// Test invalid price
38	err = book.SetPrice(-10)
39	if err == nil {
40		t.Error("Expected error for negative price")
41	}
42}
 1package storage_test
 2
 3import (
 4	"libraryapp/models"
 5	"libraryapp/storage"
 6	"testing"
 7)
 8
 9func TestLibrary(t *testing.T) {
10	lib := storage.NewLibrary("Test Library")
11
12	book, _ := models.NewBook("1234567890", "Test Book", "Author", 2020, 29.99)
13
14	// Test add
15	err := lib.AddBook(book)
16	if err != nil {
17		t.Fatalf("AddBook failed: %v", err)
18	}
19
20	// Test get
21	retrieved, err := lib.GetBook("1234567890")
22	if err != nil {
23		t.Fatalf("GetBook failed: %v", err)
24	}
25
26	if retrieved.Title != "Test Book" {
27		t.Errorf("Expected title 'Test Book', got %s", retrieved.Title)
28	}
29
30	// Test list
31	books := lib.ListBooks()
32	if len(books) != 1 {
33		t.Errorf("Expected 1 book, got %d", len(books))
34	}
35
36	// Test remove
37	err = lib.RemoveBook("1234567890")
38	if err != nil {
39		t.Fatalf("RemoveBook failed: %v", err)
40	}
41
42	books = lib.ListBooks()
43	if len(books) != 0 {
44		t.Errorf("Expected 0 books after removal, got %d", len(books))
45	}
46}

Solution

Click to see the complete solution

models/book.go

 1package models
 2
 3import (
 4	"fmt"
 5)
 6
 7// Book represents a book in the library
 8type Book struct {
 9	ISBN   string
10	Title  string
11	Author string
12	Year   int
13	price  float64 // unexported field
14}
15
16// NewBook creates a new book with validation
17func NewBook(isbn, title, author string, year int, price float64) {
18	if isbn == "" || title == "" || author == "" {
19		return nil, fmt.Errorf("ISBN, title, and author cannot be empty")
20	}
21
22	if len(isbn) != 10 && len(isbn) != 13 {
23		return nil, fmt.Errorf("ISBN must be 10 or 13 characters")
24	}
25
26	if year < 1000 || year > 2100 {
27		return nil, fmt.Errorf("year must be between 1000 and 2100")
28	}
29
30	if price < 0 {
31		return nil, fmt.Errorf("price must be positive")
32	}
33
34	return &Book{
35		ISBN:   isbn,
36		Title:  title,
37		Author: author,
38		Year:   year,
39		price:  price,
40	}, nil
41}
42
43// GetPrice returns the book's price
44func GetPrice() float64 {
45	return b.price
46}
47
48// SetPrice sets the book's price with validation
49func SetPrice(price float64) error {
50	if price < 0 {
51		return fmt.Errorf("price must be positive")
52	}
53	b.price = price
54	return nil
55}
56
57// String returns a formatted string representation
58func String() string {
59	return fmt.Sprintf("%s by %s - ISBN: %s",
60		b.Title, b.Author, b.Year, b.ISBN)
61}

storage/library.go

 1package storage
 2
 3import (
 4	"fmt"
 5	"libraryapp/models"
 6)
 7
 8// Library manages a collection of books
 9type Library struct {
10	books map[string]*models.Book
11	name  string
12}
13
14// NewLibrary creates a new library
15func NewLibrary(name string) *Library {
16	return &Library{
17		books: make(map[string]*models.Book),
18		name:  name,
19	}
20}
21
22// AddBook adds a book to the library
23func AddBook(book *models.Book) error {
24	if book == nil {
25		return fmt.Errorf("cannot add nil book")
26	}
27
28	if _, exists := l.books[book.ISBN]; exists {
29		return fmt.Errorf("book with ISBN %s already exists", book.ISBN)
30	}
31
32	l.books[book.ISBN] = book
33	return nil
34}
35
36// GetBook retrieves a book by ISBN
37func GetBook(isbn string) {
38	book, exists := l.books[isbn]
39	if !exists {
40		return nil, fmt.Errorf("book with ISBN %s not found", isbn)
41	}
42	return book, nil
43}
44
45// RemoveBook removes a book by ISBN
46func RemoveBook(isbn string) error {
47	if _, exists := l.books[isbn]; !exists {
48		return fmt.Errorf("book with ISBN %s not found", isbn)
49	}
50
51	delete(l.books, isbn)
52	return nil
53}
54
55// ListBooks returns all books in the library
56func ListBooks() []*models.Book {
57	books := make([]*models.Book, 0, len(l.books))
58	for _, book := range l.books {
59		books = append(books, book)
60	}
61	return books
62}
63
64// GetName returns the library name
65func GetName() string {
66	return l.name
67}

utils/formatter.go

 1package utils
 2
 3import (
 4	"fmt"
 5	"strings"
 6
 7	"libraryapp/models"
 8)
 9
10// FormatBookList returns a formatted string of books
11func FormatBookList(books []*models.Book) string {
12	if len(books) == 0 {
13		return "No books available"
14	}
15
16	var sb strings.Builder
17	for i, book := range books {
18		sb.WriteString(fmt.Sprintf("%d. %s by %s - %s\n",
19			i+1, book.Title, book.Author, book.Year,
20			FormatPrice(book.GetPrice())))
21	}
22	return sb.String()
23}
24
25// FormatPrice formats a price with currency symbol
26func FormatPrice(price float64) string {
27	return fmt.Sprintf("$%.2f", price)
28}

utils/validator.go

 1package utils
 2
 3import "time"
 4
 5// ValidateISBN checks if an ISBN is valid
 6func ValidateISBN(isbn string) bool {
 7	return len(isbn) == 10 || len(isbn) == 13
 8}
 9
10// ValidateYear checks if a year is reasonable
11func ValidateYear(year int) bool {
12	currentYear := time.Now().Year()
13	return year >= 1000 && year <= currentYear+1
14}
15
16// ValidatePrice checks if a price is valid
17func ValidatePrice(price float64) bool {
18	return price >= 0
19}

go.mod

1module libraryapp
2
3go 1.21

Key Takeaways

  1. Package Organization: Group related functionality into packages for better modularity
  2. Visibility Control: Use capitalization to control what's exported from packages
  3. Encapsulation: Use unexported fields with getter/setter methods for data protection
  4. Import Paths: Understand module paths and how to import local packages
  5. Package Documentation: Always document exported types and functions

Additional Challenges

  1. Add a catalog package with genre categorization
  2. Implement package-level initialization using init() function
  3. Add internal package for implementation details
  4. Create a config package with environment-based settings
  5. Implement circular dependency resolution with interfaces
  6. Add vendor directory management
  7. Create multiple modules with inter-module dependencies