11 - Putting It All Together
📋 Jump to TakeawaysYou've learned variables, decisions, loops, functions, slices, maps, structs, errors, and packages. This final lesson brings everything together into a real application: a Contact Book.
No new concepts. Just applying everything you already know.
The Project
A CLI contact book that lets you:
- Add contacts (name, phone, email)
- List all contacts
- Search by name
- Save contacts to a file
- Load contacts from a file on startup
Project Structure
contacts/
go.mod
main.go
contact/
contact.go
storage.goTwo packages: contact handles the data and file I/O, main handles the menu and user interaction.
Create the folders and initialize the module:
mkdir contacts
cd contacts
mkdir contact
go mod init contactscontact/contact.go
The Contact struct and all operations on the contact list:
package contact
import (
"fmt"
"strings"
)
type Contact struct {
Name string
Phone string
Email string
}
func (c Contact) Display() string {
return fmt.Sprintf(" %s | %s | %s", c.Name, c.Phone, c.Email)
}
func Add(list []Contact, name, phone, email string) ([]Contact, error) {
if name == "" {
return list, fmt.Errorf("name cannot be empty")
}
c := Contact{Name: name, Phone: phone, Email: email}
return append(list, c), nil
}
func Search(list []Contact, query string) []Contact {
var results []Contact
q := strings.ToLower(query)
for _, c := range list {
if strings.Contains(strings.ToLower(c.Name), q) {
results = append(results, c)
}
}
return results
}This uses:
- Structs:
Contactgroups name, phone, and email - Methods:
Displayformats a contact for printing - Slices: the contact list is
[]Contact - Functions:
AddandSearchoperate on the list - Errors:
Addreturns an error if the name is empty - Strings:
ToLowerandContainsfor case-insensitive search
contact/storage.go
Save and load contacts to a text file:
package contact
import (
"fmt"
"os"
"strings"
)
const separator = "|"
func Save(list []Contact, filename string) error {
var lines []string
for _, c := range list {
line := c.Name + separator + c.Phone + separator + c.Email
lines = append(lines, line)
}
data := strings.Join(lines, "\n")
err := os.WriteFile(filename, []byte(data), 0644)
if err != nil {
return fmt.Errorf("could not save: %s", err)
}
return nil
}
func Load(filename string) ([]Contact, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var list []Contact
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.SplitN(line, separator, 3)
if len(parts) != 3 {
continue
}
c := Contact{
Name: strings.TrimSpace(parts[0]),
Phone: strings.TrimSpace(parts[1]),
Email: strings.TrimSpace(parts[2]),
}
list = append(list, c)
}
return list, nil
}This uses:
- File I/O:
os.WriteFileandos.ReadFile - Error handling: every operation that can fail returns an error
- Strings:
Join,Split,TrimSpacefor parsing - Loops: iterate lines to build the contact list
- Slices: build up the list with
append
main.go
The menu loop and user interaction:
package main
import (
"bufio"
"contacts/contact"
"fmt"
"os"
"strings"
)
const filename = "contacts.txt"
var scanner = bufio.NewScanner(os.Stdin)
func readLine(prompt string) string {
fmt.Print(prompt)
scanner.Scan()
return strings.TrimSpace(scanner.Text())
}
func main() {
list, err := contact.Load(filename)
if err != nil {
list = []contact.Contact{}
} else {
fmt.Printf("Loaded %d contacts.\n", len(list))
}
for {
fmt.Println()
fmt.Println("=== Contact Book ===")
fmt.Println("1. Add contact")
fmt.Println("2. List all")
fmt.Println("3. Search")
fmt.Println("4. Save")
fmt.Println("5. Quit")
choice := readLine("> ")
switch choice {
case "1":
name := readLine("Name: ")
phone := readLine("Phone: ")
email := readLine("Email: ")
var addErr error
list, addErr = contact.Add(list, name, phone, email)
if addErr != nil {
fmt.Println("Error:", addErr)
} else {
fmt.Println("Contact added.")
}
case "2":
if len(list) == 0 {
fmt.Println("No contacts yet.")
} else {
fmt.Printf("Contacts (%d):\n", len(list))
for _, c := range list {
fmt.Println(c.Display())
}
}
case "3":
query := readLine("Search: ")
results := contact.Search(list, query)
if len(results) == 0 {
fmt.Println("No matches found.")
} else {
fmt.Printf("Found %d match(es):\n", len(results))
for _, c := range results {
fmt.Println(c.Display())
}
}
case "4":
saveErr := contact.Save(list, filename)
if saveErr != nil {
fmt.Println("Error:", saveErr)
} else {
fmt.Printf("Saved %d contacts to %s\n", len(list), filename)
}
case "5":
saveErr := contact.Save(list, filename)
if saveErr != nil {
fmt.Println("Warning: could not auto-save:", saveErr)
}
fmt.Println("Goodbye!")
return
default:
fmt.Println("Invalid choice.")
}
}
}Running It
From the contacts folder, run:
go run .go run . compiles and runs all .go files in the current directory. Here's what a session looks like:
Loaded 0 contacts.
=== Contact Book ===
1. Add contact
2. List all
3. Search
4. Save
5. Quit
> 1
Name: Alice
Phone: 555-1234
Email: [email protected]
Contact added.
=== Contact Book ===
> 1
Name: Bob
Phone: 555-5678
Email: [email protected]
Contact added.
=== Contact Book ===
> 2
Contacts (2):
Alice | 555-1234 | [email protected]
Bob | 555-5678 | [email protected]
=== Contact Book ===
> 3
Search: ali
Found 1 match(es):
Alice | 555-1234 | [email protected]
=== Contact Book ===
> 5
Goodbye!Run it again and your contacts are still there, loaded from contacts.txt.
What You Built
Every concept from this course is in this application:
| Concept | Where it's used |
|---|---|
| Variables | Name, phone, email, menu choice, filename |
| Decisions | Menu switch, empty list checks, error checks |
| Loops | Menu loop, iterating contacts, search loop |
| Functions | Add, Search, Save, Load, readLine |
| Slices | Contact list grows with append, search returns a filtered slice |
| Structs | Contact type with fields and a Display method |
| Errors | File I/O errors, validation errors, all checked |
| Packages | contact/ package with contact.go and storage.go |
Ideas to Extend
Now that you have a working app, try adding:
- Delete a contact: search by name, remove from the slice
- Edit a contact: find by name, update fields
- Sort contacts: alphabetically by name using
sort.Slice - Count contacts: show total in the menu header
- Export to CSV: change the save format to comma-separated values
Each extension uses concepts you already know. No new lessons needed.
What to Learn Next
You now know how to write programs. You understand variables, control flow, data structures, functions, error handling, and code organization. That's a real foundation.
Go Essentials (the next course on ByteLearn) picks up where this one leaves off. It covers:
- Pointers and memory (why
&and*exist) - Interfaces (making different types work the same way)
- Concurrency (doing multiple things at once with goroutines)
- More on error handling (wrapping, custom error types)
- A capstone CLI project
Other directions:
- The Go Tour (go.dev/tour) is the official interactive tutorial. Now that you know the basics, it'll make much more sense.
- Go by Example (gobyexample.com) is a quick reference for every Go feature with runnable code.
- Build something. A CLI tool, a web scraper, a simple API. The best way to learn is to build something you actually want.
Key Takeaways
- Everything you learned works together: variables store state, decisions branch logic, loops repeat actions, functions organize code, collections hold data, structs model entities, errors handle failure, packages structure projects
go run .compiles and runs all files in the current directory- Real programs combine all these concepts. No single feature works alone
- Organize code by responsibility: data logic in one package, user interaction in another
- The best way to solidify what you've learned is to build something new on your own
- Go Essentials is the natural next step for deeper Go knowledge