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
-
Package Organization:
- Each package should have a clear responsibility
- Use proper naming conventions
- Maintain package-level documentation
-
Visibility:
- Book price should be unexported
- Provide getter/setter methods for price
- All validation functions should be exported
-
Imports:
- Use proper import paths
- Demonstrate import aliases if needed
- Avoid circular dependencies
-
Validation:
- ISBN must be 10 or 13 characters
- Year must be between 1000 and current year + 1
- Price must be positive
-
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
- Package Organization: Group related functionality into packages for better modularity
- Visibility Control: Use capitalization to control what's exported from packages
- Encapsulation: Use unexported fields with getter/setter methods for data protection
- Import Paths: Understand module paths and how to import local packages
- Package Documentation: Always document exported types and functions
Additional Challenges
- Add a
catalogpackage with genre categorization - Implement package-level initialization using
init()function - Add internal package for implementation details
- Create a
configpackage with environment-based settings - Implement circular dependency resolution with interfaces
- Add vendor directory management
- Create multiple modules with inter-module dependencies
Related Topics
- Packages and Modules - Main tutorial on package management
- Structs and Methods - Defining types and methods
- Interfaces - Interface-based design
- Package Management - Advanced module management