Hello, and welcome to learning Go! It's great to have you here.
This is a set of introductory exercises in Go programming, suitable for complete beginners. If you don't know anything about Go yet, or programming, but would like to learn, you're in the right place! It will help if you have some experience in writing tests in Go, and if you'd like to find out more about that first, try the For the Love of Go: Fundamentals exercises. Then come back and try these!
(If you do already know something about Go, you should find these exercises relatively straightforward, but still worth doing.)
Absolutely. For the Love of Go: Data is a downloadable ebook which guides you through every step of the process, from installing Go to explaining Go's basic data types, struct types, slices, and so on. It will guide you through solving each of these exercises in detail.
You don't need to buy the book to work through these exercises, though it'll definitely help! But if you do buy it, you'll be helping support me and my work, including developing new exercises and books, and mentoring people from non-traditional backgrounds to help them into tech and software development careers. Which would be amazing! You can also sponsor me on GitHub.
In this set of exercises, you'll learn:
- What we mean by 'data' and why it's so important in programming
- All about Go's built-in data types
- How to define and use structured data types using the
struct
keyword - How to work with collections of data using slices
- How to store and retrieve data by key using Go's map type
Because we'll be building a real software project, you'll also learn a few useful techniques that we can use for all programs, not just Go:
- How to write user stories to guide the design of a project
- How to prioritise your user stories using a Minimum Viable Product (MVP) strategy
Each exercise is described in a separate section of this README file. Each section briefly explains what the exercise is about, and outlines what you'll be doing. There'll be one or more goals for you to achieve, marked with GOAL: in bold. (Don't worry if you're not sure how to do it. There'll usually be some helpful suggestions following the goal description.)
If you're finding the goals easy, or want a bit more of a challenge, there are some stretch goals, too. These are optional; if you're not interested or not ready to tackle them, just skip over them.
So let's get started!
Welcome aboard! It's your first day as a Go developer at Happy Fun Books, a publisher and distributor of books for people who like cheerful reading in gloomy times. You'll be helping to build our new online bookstore using Go!
First, let's talk a little about data and data types. By 'data', we really just mean some piece of information, like the title of a book, or the address of a customer. So can we distinguish usefully between different kinds of data?
Yes. For example, some data is in the form of text (like a book title). We call this string data, meaning a sequence of things, like "a string of pearls", only in this case it's a sequence of characters.
Then there's data expressed in numbers, like the price of a book; let's call it numeric data.
There's also a third kind of data which consists of 'yes or no', 'true or false', 'on or off' information. We call this boolean data, after the mathematician George Boole, who worked out a lot of the theory for dealing with it.
So what are "data types"? The word "type" in programming means what you think it means, but with a special emphasis. It means "kind of thing", but in a way which allows us to automatically make sure that our programs are working with the right kind of thing. For example, if we declare that a certain variable will only hold string data (we say the variable's type is string
), then the Go compiler can check that we didn't accidentally assign a number to it.
Let's distinguish first between a value (a piece of data, for example the string "Hello, world"
), and a variable (a name identifying a particular place in the computer's memory which can store some specific value).
Values have types; the type of "Hello, world"
is named string
. So we call this a string value, because it's of type string
. A variable in Go also has a type, which determines what values it can hold. If we intend it to store string values, we will give it type string
, and we refer to it as a string variable.
So a string variable can hold only string values. That's straightforward, isn't it? Make sure you feel comfortable in your understanding of types, values, and variables before continuing.
GOAL: Create a new folder in this repo. Write a Go program in it that declares a variable of a suitable type to hold the name of a book's author. Assign it the name of your favourite author. Print out the value of the variable.
GOAL: Add a variable to your program to represent the current edition number of the book: first edition, second edition, and so on. Assign it a suitable value.
Here at Happy Fun Books, we deal in many different kinds of data. Books are one obvious example: a book has various kinds of information associated with it, including the title, author, price, ISBN code, and so on. In our online store, we'll also need to deal with data about customers (name, address, and so on), and orders (which customers have ordered which books, for example).
Instead of dealing with each piece of book data separately, as we did in the previous chapter, we'd prefer to deal with a 'book' as a single piece of data in the system, so that we can (for example) pass a 'book' to a function, or store it in some kind of database.
A data type made up of several values that you can treat as a single unit is called a composite type (as in, "composed of several parts"). One of the most useful composite data types in Go is called a struct, short for 'structured data'.
A struct groups together related pieces of data, called fields. For example, a struct describing a book might have a string field for the title, and a bool
field to say whether or not it's currently in stock (or perhaps an int
field for the number of copies in stock).
Each of these fields has names, so that we can reference them individually. Here's a definition of a simple Book
type:
// Book represents information about a book.
type Book struct {
Title string
Author string
Copies int
}
GOAL: Write a Go program which defines the type Book
, as given here, then declares a variable of type Book
, assigns it a value, and prints out the value of the variable.
STRETCH GOAL: Add suitable fields to the Book
struct to represent a book that is a member of a series. For example, this book is the second in a series called "For the Love of Go". How would you represent that? Your solution should make it possible to list a book series in order, for example.
Since we can't afford to give these books away (much as we'd love to), they'll need a price, too. To eliminate currency issues, let's assume the price is always in the same currency (dollars, for example).
There are excellent reasons, which we won't go into here, why you shouldn't use floating-point values to represent money, so let's make the price an integer field which represents the price in a whole number of cents, rather than dollars.
Here's the updated definition of the Book
struct, with the PriceCents
field added:
// Book represents information about a book.
type Book struct {
Title string
Author string
Copies int
PriceCents int
}
GOAL: Extend your Go program to add a price to your Book
literal, and add a new field DiscountPercent
to the Book
struct declaration, representing a percentage discount (for example, DiscountPercent: 10
would represent a 10% discount off the cover price). Add a suitable value for this field to the struct literal assigned to the b
variable. Print out your book information in a suitable format for a book catalog, including price and discount data.
STRETCH GOAL: Write a function NetPrice
which takes a Book
value, including price and discount information, and returns the net price of the book with discount applied. Add this data to your book catalog output.
TRY: Suppose you declare a certain field in a struct (for example, Edition int
, representing the edition number of the book), but then you don't supply a value for that field in your struct literal. What happens? Can you refer to that field of the variable? What value does it have? See if you can guess the answer before trying it out in your program. Try omitting the value for various different field types. What value do they get if you don't explicitly assign one?
We're pretty close to being able to start work on our bookstore project, but there's something we're still missing. We can create a single value of the Book
type and assign it to a variable, but a decent bookstore is going to need more than one book. We don't yet have a way of dealing with collections of things (books, for example).
Just as we grouped a bunch of values of different types together into a struct, so that we can deal with them as a unit, we would like a way to group together a bunch of values of the same type. In Go this kind of type is called a slice (it's a slightly weird name, I know, but just roll with it for now).
For example, a value representing a group of books would be a slice of Book
(that's how we say it). We write it using empty square brackets before the element type: []Book
. Just as each individual struct
type in Go is a distinct type, so is each slice.
Can we declare variables of a slice type? Yes, we can:
var catalog []Book
Just as with any other Go type, this says "Please create a variable named catalog
of type 'slice of Book
'".
We've seen how to write struct literals; is there such a thing as a slice literal? Yes:
catalog = []Book{}
GOAL: Write a Go program that declares a variable of type []string
, assigns it a slice literal of strings, and prints it out.
STRETCH GOAL: Some books have multiple authors (for example, Strunk and White's "The Elements of Style"). Modify your Book
struct to allow it to store information about multiple authors for the same book. A slice of strings might be a good choice of data type!
There's something else we can do with a slice that we couldn't do with our other types: we can add a new element to it. We use the built-in append
function for this:
b := Book{ Title: "The Grapes of Mild Irritation" }
catalog = append(catalog, b)
GOAL: Extend your Go program to add a catalog
variable of type []Book
, assign it a slice literal of books, and then append one book to it.
STRETCH GOAL: Write a function AddToCatalog
which takes a book as a parameter, and adds it to the catalog using append
.
Nifty! This sounds like just what we need for our bookstore. The only thing missing is being able to iterate (that is, do something repeatedly, once for each element) over a slice of books. How can we do that?
The for ... range
statement will do exactly that for us. Here's what it looks like:
for i := range catalog {
fmt.Println(i, ": ", catalog[i].Title)
}
In fact, it's so common to want the element at position i
that Go can supply it for you automatically, without you having to refer to catalog[i]
inside the loop. We can do this by 'receiving' two values from the range
statement instead of just one:
for i, b := range catalog {
fmt.Println(i, ": ", b.Title)
}
Now i
is the index value, as before, and b
is the element value, that is, the Book
value at position i
. If we don't happen to need the index value at all, just the element, we can use the blank identifier _
to ignore it:
for _, b := range catalog {
fmt.Println(b.Title)
}
Since iterating over slices is very common in Go programs, and we usually don't care about the index value, just the element, you'll see this kind of construction a lot.
GOAL: Extend your Go program to loop over all the books in your catalog, printing out the author and title of each book. Finally, have it print out the number of books in the catalog.
STRETCH GOAL: Add a boolean field Featured
to your Book
struct, to identify books which are featured as 'Pick of the Month' for special display. Write a function FeaturedBooks
which returns a slice of Book
containing only the books in the catalog which are featured.
So, you just learned about a few of Go's basic data types, and how to use structs and slices, and you think you're ready to start building an online bookstore? I've got some great news for you: you absolutely are!
As you know, the package is Go's way of organising chunks of related code. With most projects, there's some kind of core package which implements the most basic functionality relating to the problem we're solving. We might layer other packages on top of that, such as an API or a web application, or a command-line tool, and we might add other packages 'below' it, like a database layer to store persistent data. But it makes sense to start with the core package. What should we name it?
A single word is best, ideally a short word, and one that completely describes what the software is for. Let's call our core package bookstore
.
There's usually some data type which is central to our core package; it's the thing that the whole project is about. In our case, that would be Book
. So let's think for a minute about our core Book
type. What fields does it need to be useful in a bookstore application?
All books have a title (even George Jean Nathan's "A Book Without a Title"), and let's assume they also have an author (maybe you!). Let's also include a description field to tell people what it's about, and an integer PriceCents
field so they know how much it costs.
If you've read the first book in this series, For the Love of Go: Fundamentals, you'll know that we'll be using a test-driven development (TDD) workflow. If you haven't read that book, and you're new to testing in Go, I recommend you go and read it now, because it'll be really helpful for what's coming next.
TDD means writing a test before writing any production code, so we seem to have a problem right away: we know we want a core Book
type in our bookstore
package, but how can we create one if we're not allowed to write any production code yet?
Let's write a test which can't pass unless our Book
type exists with the fields we decided on. Here's one (in the file bookstore_test.go
):
package bookstore_test
import (
"bookstore"
"testing"
)
func TestBook(t *testing.T) {
_ = bookstore.Book{
Title: "Spark Joy",
Author: "Marie Kondō",
Description: "A tiny, cheerful Japanese woman explains tidying.",
PriceCents: 1199,
}
}
We have a go.mod
file identifying the bookstore
module, and a bookstore.go
containing a package bookstore
declaration, so the import should work. But we know that the Book
struct isn't yet defined, so we should see a compilation failure when running the test.
If we run go test
now, that's exactly what we see:
# bookstore_test [bookstore.test]
./bookstore_test.go:9:6: undefined: bookstore.Book
FAIL bookstore [build failed]
GOAL Make this test pass.
STRETCH GOAL: Extend your test to include all the various Book
fields we've discussed in previous chapters (multiple authors, discounts, series membership, pick of the month, and so on). Make the extended test pass.
Let's implement our first MVP user story for the bookstore: listing all books. Suppose we implement this with a function called GetAllBooks
. How shall we write a test for it?
Virtually all tests in Go follow a structure like this:
- Decide beforehand what you want to get from calling the function with a given input.
- Call the function and compare what you want with what you got.
So, what's our want
here? That's easy: given a catalog of books—a slice of Book
—the GetAllBooks
function should return exactly that slice.
But we don't have a catalog yet. Let's create one in the simplest way that could possibly work: adding a slice variable to package bookstore
:
var Catalog = []Book{}
Each test needs to know something about the 'world' in which it's running; usually, by setting up that world itself. So for TestGetAllBooks
, we might set up the world as follows:
book1 := Book{Title: "This is Book 1"}
book2 := Book{Title: "This is Book 2"}
bookstore.Catalog = []Book{book1, book2}
Now we have some books for GetAllBooks
to return. And we also know our want
; it's just bookstore.Catalog
. So the return value from GetAllBooks
(which clearly doesn't need to take any parameters) should be exactly equal to that.
There's just one problem. Usually in a test we compare want
with got
, to see if it was as expected. But we can't actually compare two slices in Go using the ==
operator:
invalid operation: want != got (slice can only be compared to nil)
The standard library function reflect.DeepEqual
can do this comparison for us, but there's an even better solution. There's a package called go-cmp
which is really clever at comparing all kinds of Go data structures, and it's especially useful for tests.
You're now ready to write the test for GetAllBooks
!
GOAL: Write TestGetAllBooks
. Now write GetAllBooks
so that the test passes.
STRETCH GOAL: Write a test for a function AddBookToCatalog
, which takes a Book
value and adds it to the catalog. The test should verify that the book is in the catalog after the function has been called. Make the test pass.
Not too difficult, was it? In fact, it's so easy that you might wonder if it's worth having a GetAllBooks
function at all; we could just refer to bookstore.Catalog
instead!
Well, let's see what happens when we try to write the test for the second user story: viewing a book's details. Our want
here will probably be some kind of formatted string, like this:
Title: Right Ho, Jeeves
Author: P.G. Wodehouse
Description: ...
And let's call the function that produces this string GetBookDetails
. What input should it take? Well, that's a bit of a puzzle, isn't it? How do we uniquely identify a particular book? Not by its title, because there are many books with the same title. Clearly not by its author, price, or any of the other fields on Book
. There is such a thing as an International Standard Book Number (ISBN), but not every book has one; this one doesn't, for example!
So we're going to need some unique identifier (ID) for each book. Let's use a string, rather than a number, for flexibility. (ISBNs, for example, can contain not just digits, but also the letter X
, weirdly enough.)
GOAL: Update your TestBook
function to add a string ID
field to the Book literal, and then update the Book
struct definition so that this test passes.
STRETCH GOAL: Write a test for a function NewID
which returns a string that can be used as a book ID. Every time you call the function it should return a different strings, though it doesn't matter what the strings actually are (and the test should make no assumptions about them other than that they're unique). You should generate at least ten IDs and none of them should be the same as any other. Make this test pass by implementing the NewID
function. (There are many ways to solve this problem, and you can use whichever one you like. If it passes the test, it's correct, by definition.)
Now that we have unique IDs for books, we're in a better position to write TestGetBookDetails
.
GOAL: Decide what format you want the book details to take, and write TestGetBookDetails
so that it checks the result from the function against this expected output. Write a minimal GetBookDetails
implementation which just returns an empty string so that you can compile the test, and see it fail.
Now it should be relatively easy to write the real GetBookDetails
function. If we had the Book
value corresponding to the ID, we could use fmt.Sprintf
to construct a details string from its fields. But how can we find the book in the catalog that has the ID we want?
TRY: Think about this a little and see if you can work out a way to do it, before reading on. It doesn't have to be a good way; this is a minimum viable product, after all. We just need to get the test passing.
Well, the simplest way that could possibly work is to iterate over every book in the slice using for ... range
, checking its ID to see if it's the one we want. It's not elegant, or scalable, but it'll work for now.
GOAL: Implement GetBookDetails
as quickly and simply as possible.
STRETCH GOAL: Write a test for, and implement, a function GetAllByAuthor
which takes a string identifying an author, and returns all the books in the catalog by that author (including the cases where the author is one of multiple authors of the same book).
STRETCH GOAL: Write a test for a function GetCatalogDetails
which uses both GetAllBooks
and GetBookDetails
to produce a string containing information for all the books in the catalog. Make the test pass.
A successful bookstore, though, will have lots of books, and it'll take a long time to search through all of them checking all their IDs. Can't we do better?
What we want is a data structure that, given a book ID, returns the corresponding book directly, without looping. It would take the form of a mapping between IDs and books, or to put it another way, a map of IDs to books.
It turns out that Go has exactly the data type we want! And, for reasons that may now be clear to you, it's called a map.
GOAL: Refactor the bookstore
package and its tests to use a map for the catalog, instead of a slice.
STRETCH GOAL: To make it easy to find books by author, add a map to the bookstore
package which relates author's names to book IDs. For example, the entry for Charles Dickens
might contain the book IDs corresponding to Nicholas Chuckleby
, Easy Times
, and A Christmas Pudding
. Update your AddBookToCatalog
function so that it updates this author index as well as adding the book to the main catalog. Extend your tests to cover this functionality.
You have nearly everything you need now for your bookstore MVP, except payments. Here are some challenges you might like to try tackling on your own. If you feel up to implementing some of them, go ahead! If you're not that confident yet, you might like to just think about them, make some notes, and try to figure out how it might work. You can always come back to these challenges later.
Assume you have available some third-party payment processing service which, given a book ID and a price, returns either true
(payment succeeded) or false
(payment failed for whatever reason). Write a dummy implementation of this service for testing purposes, which can either succeed or fail on demand, depending on what the test needs.
Use it to design a test for a BuyBook
function, and implement the function. Return a suitable message to the user according to whether the payment succeeded or failed. You'll need a way to have that function use the dummy payments service when you call it from the test, but some (currently hypothetical) real service when it's called in production, that is to say, in your real live bookstore. What would this look like? (The technical term for this is dependency injection, which sounds painful, but it really just means "passing in the thing you need".)
Assuming the payment succeeds, we need to do something that actually causes the book to get shipped to the user (or they won't be too happy). Think about this problem. What could we do? It will probably be useful to create some kind of Order
value that identifies the book and the customer who bought it. Assume that once an Order
is created in the system, this will trigger the shipping somehow (maybe somebody just lists the unfulfilled orders every morning, and processes them). There's a lot of fun design work for you to do here, and it definitely involves data!
It may not have escaped your notice that, while you've developed a useful core engine for managing the bookstore, there's as yet no way for customers (or staff) to actually interact with it. What would an MVP user interface look like? What are we still missing that we'd need before we can take orders?
This doesn't need to be a web-based application (you could take orders by phone or email, for example, and have the bookstore staff use some kind of command-line tool to process them). What would that look like? What would a web-based bookstore look like?
You know how to design these things now: by writing user stories, using an MVP to prioritise them, and by building the functionality test-first. Use what you've learned to make Happy Fun Books a reality! And be happy and have fun yourself.
John Arundel is a Go teacher and mentor of many years experience. He's helped literally thousands of people to learn Go, with friendly, supportive, professional mentoring, and he can help you too! Find out more:
Yes. You can buy the book, which covers all the exercises in much more detail, including a complete step-by-step guide to solving them, here:
It's priced at just $4.99, which is the least I can possibly charge for it and still keep the lights on and food on the table. Because it's self-published, you can buy it in the knowledge that every penny goes directly to support my writing more Go books! (Well, 45 pennies go to Stripe for the card transaction, or 68 pennies if you pay by PayPal, but you know what I mean.)
If you'd like to hear about it first when I publish new books, or even join my exclusive group of beta readers to give feedback on drafts in progress, you can subscribe to my mailing list here:
You can find more Go tutorials and exercises here:
I have a YouTube channel where I post occasional videos on Go, and there are also some curated playlists of what I judge to be the very best Go talks and tutorials available, here:
You might like to try another set of exercises in this series:
- The G-machine guides you through the development of a simplified virtual CPU in Go, complete with its own machine language. Along the way you'll learn some useful computer science fundamentals.
Cover image by Andy Pearson, of London Parkour.
'Structure' image by Giorgiogp2, licensed under Creative Commons.
Gopher images by the magnificent egonelbre.