| title | Go Trainer Bootcamp | ||
|---|---|---|---|
| sub_title | Build your Pokédex. Master concurrency. Catch 'em all with Go! | ||
| author | Zak, Selim, Vincent & Pickachu | ||
| theme |
|
Welcome to your mission: help Prof. Oak build the ultimate digital Pokédex!
-
After each theory block, you will work on a practical coding challenge.
-
More experienced Gophers are encouraged to team up with newcomers.
- Collaboration is key to success!
-
Setup Instructions:
# Install Go: https://go.dev/doc/install
cd ~/Downloads
sudo rm -rf /usr/local/go && tar -C /usr/local -xzf <go-version.tar.gz>
export PATH=$PATH:/usr/local/go/bin
# Check installation
go version
# Get the code skeleton
git clone https://github.com/iptch/go-techbier.gitAbout Go ...
- created in 2009 by R. Griesemer, R. Pike, and K. Thompson at Google
- statically typed and compiled, including to standalone binaries
- features memory safety, garbage collection, structural typing
- built for simplicity and efficiency, i.e. no classes or inheritance
- built-in support for concurrency through
goroutinesandchannels - powerful standard library
- great and supportive tooling, e.g.
go test - backbone of cloud technology like Kubernetes
✨ In our Pokédex project, Go helps us make fast, portable code that runs anywhere!
- The full language specification can be found at https://go.dev/ref/spec
- Types in Go include:
bool- Numeric types:
int,float64, etc. string- Composite types:
array,slice,map,struct function,interfacepointerchannel
🧠 We’ll use
structto define Pokémon,mapto look them up by name, andsliceto hold a dynamic Pokédex.
package main // Everything belongs to a package
func main() { // Braces are used to delimit scopes
var x int // Declaring a variable of type int
}✨ In Go,
vardeclares a variable. All variables must be declared before use.
package main
import "fmt" // Importing another package
func main() {
var x int
fmt.Println(x) // Default value of int is 0
}📃 Variables have default zero-values if not explicitly initialized.
package main
import "fmt"
func main() {
var x int = 5
y := 7.7
fmt.Printf("%d\n%f\n", x, y)
fmt.Printf("y is of type %T", y)
}📄 Use the shorthand
:=inside functions, includingmain, to declare and initialize in one line.
package main
import "fmt"
func main() {
x := "who"
{
x := "can guess"
x = "this variable?"
fmt.Printf("A: %s\n", x) // What is the value of x for A?
}
fmt.Printf("B: %s\n", x) // What is the value of x for B?
}🔄 Each
{}block introduces a new scope. Variablexinside the block is not the same as outside.
Let’s now look at how Go uses functions. A function in Go is a standalone unit of logic that can take input arguments and return values.
package main
import "fmt"
func increaseLevel(level int) int {
return level + 1
}
func main() {
original := 5
newLevel := increaseLevel(original)
fmt.Printf("Before: %d, After: %d", original, newLevel)
}🧪 This passed a copy of
level. To change the original, we need pointers (see backup slide for more information).
Just like a Pokéball can fail to catch a Pokémon, some operations in Go can fail too — like creating files or opening data.
Go encourages you to check errors explicitly using the if err != nil pattern.
func throwPokeball(pokemon string) error {
p, err := pokeball.Catch(pokemon)
if err != nil {
return err // Oh no! The Pokéball missed!
}
defer p.Close() // Defer the execution of this code to **whenever the function exits**
fmt.Printf("You caught a Pokemon!")
return nil // Returning a nil error means success
}
⚠️ Always check your Pokéballs... err, errors!
In Go, the only looping keyword is for — and it's all you need!
Just like scanning Pokémon one by one, we can loop through a list (slice) of entries.
package main
import "fmt"
func main() {
var pokedex = make([]string, 0)
for i := 1; i <= 3; i++ {
pokedex = append(pokedex, fmt.Sprintf("Pokémon #%d", i))
}
fmt.Printf("Our current Pokedex: %v\n", pokedex)
}🔁 Use
appendto add to a slice — our Pokédex is growing!
You can mimic a while loop using for in Go — great for catching Pokémon until you run out of Pokéballs!
package main
import "fmt"
func main() {
var pokedex = make([]string, 0)
pokeballs := 3
for pokeballs > 0 {
pokedex = append(pokedex, fmt.Sprintf("Caught #%d", pokeballs))
pokeballs--
}
fmt.Printf("Our current Pokedex: %v\n", pokedex)
}🥎 You can use
foras awhile— Go keeps things simple.
Want to add many Pokémon at once? Use append(...slice...)!
package main
import "fmt"
func main() {
pokedex := []string{"Pikachu", "Charmander"}
more := []string{"Bulbasaur", "Squirtle"}
pokedex = append(pokedex, more...)
fmt.Printf("Our current Pokedex: %v\n", pokedex)
}📚 This is like importing a batch of Pokémon entries into your Pokédex.
Loop over a slice using range — get both the index and the value!
package main
import "fmt"
func main() {
pokedex := []string{"Pikachu", "Charmander", "Bulbasaur"}
for index, name := range pokedex {
fmt.Printf("%d: %s\n", index, name)
}
}📖 Use
rangeto loop over your slice like flipping through Pokédex pages.
🔍 Your task is to help Professor Oak fill his Pokédex with Pokémon retrieved from the PokéAPI.
Your mission:
- Implement a function that fetches data from the PokéAPI
- Parse the JSON into Go structs
- Store Pokémon entries in a slice
- Print out the names to verify it works
Open our git repository and check out the branch tasks/1.
Look around the project and check out the file pokeapi/api.go.
You will find instructions in the code.
We’ll regroup in 20 minutes.
The next slide contains some details about for loops and slices, which you will need to solve task 1b.
Every Go file starts by declaring its package — just like assigning a Pokémon to a region.
package kantoLet’s explore how exporting works:
// Only exported if the name starts with a capital letter
var PokeballCount = 42
// Can this be used outside the current package?
var rareCandy = 3
const MaxLevel = 100 // Exported or not?🧳 Capitalized = Public (exported). Lowercase = Private (unexported).
package main
// Define a new type for our Pokédex entries
// Structs group related data together
// Fields starting with a lowercase letter are private
type Pokemon struct {
Name string
Type string
Level int
pokedexID string // unexported field
}
func main() {}🧬 Think of a
structas a blueprint for a Pokémon entry.
package main
import "fmt"
type Pokemon struct {
Name string
Type string
Level int
pokedexID string
}
func main() {
pikachu := Pokemon{"Pikachu", "Electric", 25, "#025"}
fmt.Println(pikachu)
}🐭 We just created our first Pokémon entry! Struct values can be printed directly.
package main
import "fmt"
type Pokemon struct {
Name string
Type string
Level int
pokedexID string
}
func main() {
pikachu := Pokemon{"Pikachu", "Electric", 25, "#025"}
fmt.Println(pikachu)
bulbasaur := Pokemon{
Name: "Bulbasaur",
Type: "Grass",
Level: 12,
pokedexID: "#001",
}
fmt.Println(bulbasaur)
}🧪 Named field initialization makes your code more readable and flexible.
package main
import "fmt"
type Pokemon struct {
Name string
Type string
Level int
pokedexID string
}
func main() {
var charmander Pokemon
charmander.Name = "Charmander"
charmander.Type = "Fire"
charmander.Level = 18
charmander.pokedexID = "#004"
fmt.Println(charmander)
}🧯 You can also set struct fields one by one after declaration.
To distinguish between functions and methods in Go, we have to look at the context in which they are defined:
Methods:
- like a function but contains a receiver, which specifies what type the method belongs to
- receiver can be any type, but in most cases it is a struct or pointer to a struct
package main
import "fmt"
type Pokemon struct {
Name string
Level int
}
// A method with value receiver (copy)
func (p Pokemon) Speak() {
fmt.Printf("%s says hello!\n", p.Name)
}
// A method with pointer receiver (can modify)
func (p *Pokemon) Train() {
p.Level++
}🎓 Use pointer receivers when your method should mutate the struct (like training a Pokémon!).
In the Pokémon world, each species has abilities. In Go, interfaces define what a type can do — not what it is.
Think of an interface like this:
// If it can attack like a Pokemon, it IS a pokemon!
type Pokemon interface {
Growl()
Attack(move string)
}🧠 Interfaces describe capabilities — not inheritance! 🧠 Go follows the approach of composition over inheritance. 🟡 No
implementskeyword needed: implementation is implicit in Go.
If your type has all the required methods, it automatically satisfies the interface. Like how any creature that uses "Attack" and "Growl" is a Pokémon in your team.
package main
import "fmt"
type Pikachu struct{}
func (p Pikachu) Growl() {
fmt.Println("Pika Pika!")
}
func (p Pikachu) Attack(move string) {
fmt.Printf("Pikachu used %s!\n", move)
}Here, Pikachu satisfies the Pokemon interface because it implements all the required methods — no need to declare anything.
We can use an interface to build a Pokédex of battle-ready creatures:
package main
import "fmt"
type Pokemon interface {
Growl()
Attack(move string)
}
type Pikachu struct{}
func (p Pikachu) Growl() {
fmt.Println("Pika Pika!")
}
func (p Pikachu) Attack(move string) {
fmt.Printf("Pikachu used %s!\n", move)
}
func Battle(p Pokemon) {
p.Growl()
p.Attack("Thunderbolt")
}
func main() {
pikachu := Pikachu{}
Battle(pikachu)
}🎮 Pass your Pokémon around as interface values to make your code flexible and extensible.
🧾 Now that you’ve fetched some Pokémon, let’s show them off properly!
Your new task:
- Head over to
ui/item.go - Define a new
Itemtype to display Pokémon nicely in a terminal UI - Implement the required methods (like
Title,Description, etc.) to customize how each entry appears
🎨 Make sure the names, levels, or types are clearly shown
📂 Check the tasks/2 branch in the repo
We’ll regroup in 20 minutes to review your stylish Pokédex entries!
💡 Hint: Remember your structs and methods!
- As stated previously, everything in Go belongs to a package, declared by the
keyword
package - Packages are imported using the
importstatement at the beginning of a file - Imports apply to the entire package, all exported identifiers will become available
- Package management is awesome! Look at the following example:
package main
// Let's import multiple packages at once
import (
"fmt" // Standard library
"math" // Standard library
http "net/http" // Create an alias called http
"github.com/charmbracelet/bubbles/list" // External package we will need
)Compare that to Java
import java.util.*;
import java.util.ArrayList;A map is Go’s built-in way to store key-value pairs. Perfect for Pokémon lookup tables!
package main
func main() {
pokedex := make(map[string]string)
pokedex["025"] = "Pikachu"
pokedex["004"] = "Charmander"
}🔍
map[keyType]valueType— useful for building fast-access Pokédex indexes.
You can also directly instantiate a key-value pair!
package main
func main() {
pokedex := map[string]string{
"025": "Pikachu",
"004": "Charmander",
}
}Check if a key exists in a map using the value, ok := map[key] idiom:
package main
import "fmt"
func main() {
pokedex := map[string]string{
"025": "Pikachu",
"004": "Charmander",
}
code := "007"
name, ok := pokedex[code]
if ok {
fmt.Printf("%s is in the Pokédex!\n", name)
} else {
fmt.Printf("No entry found for code %s.\n", code)
}
}✅
oktells you whether the Pokémon is registered or still hiding in tall grass.
Sometimes, we catch a value of unknown type (interface{}), but want to know what it really is — like scanning a Pokémon in the wild!
package main
import "fmt"
func main() {
var pokeball interface{}
pokeball = "Eevee"
// Assert that it's a string
mon := pokeball.(string)
fmt.Println(mon)
// Use comma-ok to avoid runtime panic
_, ok := pokeball.(int)
if !ok {
fmt.Println("That wasn’t a numeric Pokémon!")
}
}🔍 Type assertions let you safely reveal what’s hiding in your Pokéball (i.e. interface). 🔍 Remember! Go can infer types when we define variables using the := notation.
Go comes with powerful built-in tools to help you on your dev journey. Here are a few essentials:
go mod init– Start a new Go module (project)go get– Catch a dependency (like a Poké Ball!)go install– Install binariesgo fmt– Format your code (style points!)go test– Run your unit tests
🧰 These tools help you build robust, well-organized Go code.
🧪 You’ve trained your Pokémon... now put them to the test!
Your final task:
- Navigate to
pokeapi/api.go - Implement and complete the missing function logic
- Explore the test file in
pokeapi/api_test.go - Run
go test ./...to test your Pokédex logic
🎯 Goal: Make sure the Pokédex works as expected and all tests pass.
💡 Tests are like Gym battles — prove your code is battle-ready!
📂 Check out the tasks/3 branch in the repo
We’ll wrap up in 20 minutes and celebrate with a badge!
In Go, channels and goroutines let you handle multiple tasks at once — just like sending out several Pokémon in a double battle!
- Goroutines are lightweight threads that run with
gokeyword - Channels let goroutines communicate by passing messages
package main
// Send messages over a channel
func sendMoves(moves chan string) {
moves <- "Thunderbolt"
moves <- "Quick Attack"
close(moves)
}
func main() {
// ...
}⚡ Goroutines make Go ideal for building fast and concurrent apps.
package main
import "fmt"
func sendMoves(moves chan string) {
moves <- "Thunderbolt"
moves <- "Quick Attack"
close(moves)
}
func main() {
moves := make(chan string)
// Starts in a new thread
go sendMoves(moves)
// Wait for messages on the channel until closed
for move := range moves {
fmt.Println("Pikachu used:", move)
}
}🔁 Use
rangeto receive until the channel is closed — like watching Pikachu’s turn-based battle!
🎉 You've built a Pokédex, parsed wild Pokémon, and learned to test your code. But maybe you're still thirsty for adventure?
Try this bonus challenge:
- Branch:
tasks/bonus - File:
pokeapi/api.go - Add features like filtering by type, showing top levels, or building a mini CLI Pokédex
💡 Stretch tasks are for trainers aiming to be Pokémon Masters 🏆
📘 Where to continue your journey:
- Official Go docs: https://go.dev/doc/
- Effective Go: https://go.dev/doc/effective_go
- Curated Go libraries: https://github.com/avelino/awesome-go
🧠 Keep learning, keep catching bugs, and keep coding!
.
.-*#: :+:
:*%%#. ... .. .-*%@#
*%%%# .:=+*##= .=#%@@%%-
.#%%%+ .. .:=*#%%%%%+ .+#%%%%%%%*
.#%%%######**+=-=#%%%%%%%#= :+#%%%%%%%%%#:
:#%%%%%%%*#=*%%%%####*+=-. .=#%%%%%%%%%%%%=
.=#%%%%%%%#:::+%%%%##+. .=##########%%%%#=
.-:+%%#%%##%#*##**+*###= -*############*+-.
=**##*+++=+%%%#+++++####- +###########*=:
-+#%%%+---=#%##+++++#####: :########*=:
-+#%%#+=++####****######*. :*###*=:
:+####**##########**####*: :**=.
.:=*##############+****%%%*. .-+*+:
-=*##%%%%%%%#########**#**##%%%*= .-+*++-.
-**#%%%%%%%%%%%%%%############%%%%+. -+++:
.:-=++***#*#%%%%%%%%%%%%%%####%%%%%*. .-=-.
:#%%%%%%%%%%%%%%%%%%%%%%%*-.:-=-.
*%%%%%%%%%%%%%%%%%%%%%%%#*=-:.
-%%%%%%%%%%%%%%%%%%%%%%%#*+-.
:%%%%%%%%%%%%%%%%%%%%%%#**+.
*%%%%%%%#############****+
.+*####**+==-==+********+:
.:-+++: ..:--=++=
.=+ -++.
. .:
⚡ Thanks for joining our Go Pokédex workshop!
In Go, a pointer is a variable that stores the memory address of another variable. When we pass a pointer to a function, we can modify the original value.
package main
import "fmt"
func boostLevel(level *int) { // *int means pointer to an int
*level = *level + 1 // *level dereferences the pointer to access the value
}
func main() {
lvl := 10
fmt.Printf("Before: %d\n", lvl)
boostLevel(&lvl) // Pass the address of lvl
fmt.Printf("After: %d\n", lvl)
}🧠 Use
*to access and change values via a pointer. Use&to get the address of a variable.
Bonus: If you want a function to modify struct fields directly, you’ll use a pointer receiver in a method. We’ll get to that soon!