- Lab
- Core Tech

Guided: Build a System Monitor in Go
In this Guided Code Lab, you will build a real-time system monitor using the Go language. Your goal is to create a robust system monitor application that displays crucial system information like CPU usage, memory usage, and currently running processes. Throughout the lab, you will gain practical experience in utilizing Go Routines, Channels, and building a Text-based User Interface (TUI).

Path Info
Table of Contents
-
Challenge
Introduction
Welcome!
In this Code Lab lab, you will dive into the world of Go programming and create a real-time system monitor. You will be working with the
main.go
,systeminfo.go
andtui.go
files and writing the code required to build the real-time system monitor.Completing this lab will involve utilizing the following Go concepts:
- Reading System Information: Learn how to access and read system information.
- Go Routines: Understand how to use Go routines to read and display system information concurrently.
- Channels: Learn about channels and how they can be used for communication between Go routines.
- Terminal User Interface (TUI): Learn how to build a text-based user interface in the Terminal. Before you begin, let's review some key points:
- Your task involves implementing code in the
main.go
,systeminfo.go
andtui.go
files. - To execute the program, type
go build . | go run .
in the Terminal and press Enter. - Use CTRL + C to stop the program execution.
- If you encounter any challenges along the way, feel free to consult the
solution
directory. It contains solutions for each step. For example, the solution for Step 2 can be found insolution/Step2_XXX.go
. - For your convenience, imports required at each step are commented at the top of file. You'll need to uncomment those based on the step you are currently working on.
- For simplicity based upon the Step you are on there are comments for you to locate the changes required at that step.
- The
go.mod
file in the project's root is the module definition file. It contains information about the required dependencies and their versions.
-
Challenge
Read System Information
Read System Information Using gopsutil
Let's get started!
in this step, you'll learn about thegopsutil
library and how to use the library to fetch the system information.gopsutil
is a Go package that provides a set of functions to retrieve system and process metrics from various platforms. It serves as a convenient and cross-platform way to gather information about system resources such as CPU usage, memory usage, disk usage, and more.
CPU Metrics
You can retrieve information about CPU usage, including overall usage percentage and per-core usage.
You are going to use the Percent function to get the CPU Utilization %:
cpuUsage, err = cpu.Percent(time.Second, false)
It returns
cpuUsage
anderr
:cpuUsage
: This is a variable that will store the CPU usage percentage. It is an array of Utilization per CPU.err
: This is a variable that will store any error that might occur during the function call.
Here's the function signature for reference:
func Percent(interval time.Duration, percpu bool) ([]float64, error)
interval
: The amount of time over which the percentage is calculated.percpu
: If true, the function returns the percentage for each CPU core. If false, it returns an array with a average CPU percentage across all cores.
*** #### Memory Metrics
You can use the function below to get the memory detalis:
memInfo, err = mem.VirtualMemory()
mem.VirtualMemory()
: This function is part of thegopsutil
library and returns amem.VirtualMemoryStat
structure, which contains information about virtual memory usage, such as total, available, used, and used percentage.err := mem.VirtualMemory()
: This function may return an error, so it's a good practice to check for errors. If there is an error, you can handle it accordingly.memInfo.UsedPercent
: After obtaining the virtual memory information, you can use theUsedPercent
field of themem.VirtualMemoryStat
structure to get the percentage of used memory.
Process
Similar to CPU and Memory , you can also retrieve the running processes in the system using the
process
package ofgopsutil
.Let's see an example:
processes, err = process.Processes() if err != nil { // Handle error } for _, p := range processes { name, err := p.Name() if err == nil { fmt.Println("Process:", name) } }
-
process.Processes()
: This function returns a slice of*process.Process
structures, each representing information about a running process. The information includes process ID (PID), parent process ID (PPID), executable name, and more. -
err := process.Processes()
: As with many functions ingopsutil
, it's a good practice to check for errors. If there is an error, you can handle it accordingly. -
for _, p := range processes { ... }
: This loop iterates over the slice of processes obtained fromprocess.Processes()
. -
name, err := p.Name()
: For each process, theName()
method of theprocess.Process
structure is called to retrieve the name of the executable associated with the process. TheName()
method may return an error, so it's checked. -
if err == nil { fmt.Println("Process:", name) }
: If theName()
method call is successful (i.e., no error occurred), the name of the process is printed to the console. This gives you a list of running processes on the system. Great job!
You were able to successfully retrieve the CPU Usage, Memory Usage and Running Processes.
For more details about
gopsutils
and the available functions and their parameters, you can refer to the official documentation on GitHub : gopsutil Documentation.Next let's learn about how to make the program concurrent using goroutines.
-
Challenge
Goroutines to Read Information Concurrently
In this step, you will enhance the system monitor's concurrency by introducing Goroutines. Goroutines are lightweight threads in Go that enable concurrent execution.
Goroutines are distinct from traditional threads in that they are managed by the Go runtime, which is responsible for their scheduling and execution. They are more lightweight than operating system threads, and many goroutines can run concurrently on a small number of operating system threads. This enables efficient parallelism and concurrency without the overhead associated with traditional threading models.
In Go, you can create a new goroutine by using the go keyword followed by a function call. For example:
func main() { go worker() // Start a worker goroutine // Keep the main goroutine running select {} } func worker() { for { fmt.Println("Working...") time.Sleep(time.Second) } }
In this example, the worker function runs in a separate goroutine, printing "Working..." every second. The
select{}
statement ensures that the main Goroutine continues running indefinitely, preventing the program from exiting immediately, thus allowing the worker goroutine to continue its work concurrently.Alternatively the above code could also be written using an anonymous function:
func main() { // Start the worker goroutine using an anonymous function go func() { for { worker() time.Sleep(time.Second) } }() // Keep the main goroutine running select {} } func worker() { for { fmt.Println("Working...") // You may include additional logic here if needed }
Now that you've learned how goroutines work , update your program to continuously print CPU usage, memory usage, and running processes.
-
Challenge
Channels and TUI Interface
In this step, you'll dive into the dynamic realm of concurrent programming with Go, focusing on the versatile use of channels. These channels serve as communication pathways between different parts of your system resource monitor, allowing seamless data exchange among goroutines. Understanding and effectively implementing channels is key to enhancing the responsiveness and efficiency of your monitor.
Additionally, you'll explore the world of Text User Interfaces (TUI) to craft an engaging terminal-based user interface. Leveraging the
tview
package, you'll learn how to design and implement an interactive display for your system resource monitor.
First let's understand about channels.
Channels
In Go, channels are a powerful and flexible mechanism for communication and synchronization between goroutines (concurrently executing functions). Channels facilitate the safe exchange of data between goroutines by providing a way for one goroutine to send data to another goroutine. They also help to coordinate the execution of goroutines.
1. Channel Creation:
Channels are created using the make function, and you specify the type of data that will be transmitted through the channel. For example:
// Creates an unbuffered channel for transmitting integers ch := make(chan int)
2. Channel Direction:
You can specify the direction of a channel, indicating whether it can only send, only receive, or both. The default is a bidirectional channel. Here is an example:
// Send-only channel func sendData(ch chan<- int) { // code to send data to the channel } // Receive-only channel func receiveData(ch <-chan int) { // code to receive data from the channel }
3. Channel Operations:
- Sending Data (
<-
): Use the<-
operator to send data into a channel:
// Send the value 42 into the channel ch <- 42
- Receiving Data (
<-
): Use the<-
operator to receive data from a channel:
// Receive a value from the channel and store it in 'value' value := <-ch
4. Blocking
- A send operation on an unbuffered channel will block until there is a corresponding receive operation, and vice versa.
- Buffered channels allow a certain number of elements to be stored in the channel without a corresponding receiver. A send operation on a buffered channel will block only when the buffer is full.
5. Closing Channels:
You can close a channel using the
close
function. Once a channel is closed, no more values can be sent on it:close(ch)
Here's a simple example that demonstrates the basic usage of channels in Go:package main import ( "fmt" "time" ) func sendData(ch chan<- int) { for i := 0; i < 5; i++ { ch <- i time.Sleep(time.Millisecond * 500) } close(ch) } func main() { ch := make(chan int) go sendData(ch) for { value, ok := <-ch if !ok { // channel closed break } fmt.Println("Received:", value) } fmt.Println("Main goroutine done") }
In this example, the
sendData
goroutine sends values into the channel, and the main goroutine receives and prints them until the channel is closed.
Now let's understand about Text User Interface (TUI)
TUI Interface
TUI stands for "Text-based User Interface." A Text-based User Interface is an interface that allows interaction with a program through text commands and displays information in a text format, typically in a terminal or console window.
Go, being a versatile programming language, has libraries and frameworks that facilitate the creation of Text-based User Interfaces. One such popular library is
tview
, which is a feature-rich TUI library for Go. Tview provides a variety of components, such as tables, lists, forms, and more, to help developers build interactive and visually appealing terminal applications.Here's a simple example of creating a TUI application using
tview
in Go:package main import ( "github.com/rivo/tview" ) func main() { app := tview.NewApplication() textView := tview.NewTextView(). SetText("Hello Learners!"). SetTextAlign(tview.AlignCenter) if err := app.SetRoot(textView, true).Run(); err != nil { panic(err) } }
This example creates a basic TUI application with a
TextView
component that displays the text "Hello Learners!" in the center of the Terminal window. Thetview
library simplifies the creation of such interfaces and provides functionalities to handle user input and update the display dynamically.
Let's understand it step by step :
- Create a new TUI application using
tview.NewApplication()
:
func main() { // Create a new TUI application app := tview.NewApplication()
- The program creates a
TextView
component with some initial settings:
textView := tview.NewTextView(). SetText("Hello Learners!"). SetTextAlign(tview.AlignCenter)
SetText("Hello Learners!")
: Sets the initial text content of theTextView
.SetTextAlign(tview.AlignCenter)
: Aligns the text to the center within theTextView
.
if err := app.SetRoot(textView, true).Run(); err != nil { panic(err) }
- The program sets the root component of the application to the created
TextView
. The second argument,true
, indicates that theTextView
should take up the entire screen. Finally, theRun()
method starts the TUI application, and any potential errors are handled by panicking.
- Sending Data (
-
Challenge
Integrating Channels and Building a TUI Interface
Now that you've understood about the Channels and TUI , let's look at a example containing both channels and TUI.
This example creates a TUI application that initially shows a loading screen and then periodically updates the TUI with random numbers, providing a simple example of a dynamic console-based user interface:
package main import ( "fmt" "math/rand" "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func main() { // Create a new TUI application app := tview.NewApplication() // Show the loading screen showLoadingScreen(app) // Channel for updating system information updateCh := make(chan *tview.Flex, 1) // Close the update channel when the application is stopped defer close(updateCh) // Periodically update TUI with system information go func() { for { renderInfo(updateCh) time.Sleep(2 * time.Second) } }() // Goroutine to handle TUI updates go func() { for { select { case updatedFlex, ok := <-updateCh: if !ok { return } app.QueueUpdateDraw(func() { app.SetRoot(updatedFlex, true) }) } } }() // Handle keyboard events to exit the application app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape || event.Rune() == 'q' { // Close the update channel to stop goroutines close(updateCh) // Stop the TUI application app.Stop() } return event }) // Keep the application running select {} } func showLoadingScreen(app *tview.Application) { // Create Flex layout for the main container flex := tview.NewFlex().SetDirection(tview.FlexRow) flex.SetBorder(true) // Add TextViews to Flex container infoTextView := tview.NewTextView(). SetText("Loading..."). SetTextAlign(tview.AlignCenter). SetDynamicColors(true) flex.AddItem(infoTextView, 1, 1, false) // Add the Flex layout to the application go func() { if err := app.SetRoot(flex, true).Run(); err != nil { panic(err) } }() } func renderInfo(updateCh chan<- *tview.Flex) { randomInt := rand.Int() randomFloat := rand.Float64() // Create Flex layout for the updated container flex := tview.NewFlex().SetDirection(tview.FlexRow) flex.SetBorder(true) title := tview.NewTextView(). SetText(" Sample Title "). SetTextAlign(tview.AlignCenter) // Create TextViews for updated CPU, Memory, and Processes information box1 := createResourceBox(fmt.Sprintf("Box 1 Random Int : %d ", randomInt)) box2 := createResourceBox(fmt.Sprintf("Box 2 Random Float : %.2f ", randomFloat)) // Add updated TextViews to Flex container flex.AddItem(title, 1, 1, false). AddItem(box1, 1, 1, false). AddItem(box2, 0, 1, false) // Update the channel with the updated Flex layout updateCh <- flex } func createResourceBox(title string) *tview.Flex { textView := tview.NewTextView(). SetText(fmt.Sprintf("%s\n\n", title)). SetTextColor(tcell.Color107). SetDynamicColors(true). SetTextAlign(tview.AlignLeft) flex := tview.NewFlex().SetDirection(tview.FlexColumn). AddItem(textView, 0, 1, false) return flex }
Output
Let's understand it step by step :
-
A new TUI application is created using the
tview.NewApplication()
function. -
The
showLoadingScreen
method is called to display a loading screen on the TUI.
func showLoadingScreen(app *tview.Application) { flex := tview.NewFlex().SetDirection(tview.FlexRow) flex.SetBorder(true) infoTextView := tview.NewTextView(). SetText("Loading..."). SetTextAlign(tview.AlignCenter). SetDynamicColors(true) flex.AddItem(infoTextView, 1, 1, false) go func() { if err := app.SetRoot(flex, true).Run(); err != nil { panic(err) } }() }
The
showLoadingScreen
method creates a flex layout for the main container, adds a text view with the message "Loading...", and sets it as the root of the application.updateCh := make(chan *tview.Flex, 1)
: A channel namedupdateCh
of type*tview.Flex
is created. This channel is used for communication between goroutines to update the TUI.
defer close(updateCh)
: The defer statement ensures that theupdateCh
channel is closed when the main function exits.
- Periodically update the TUI with the following code:
go func() { for { renderInfo(updateCh) time.Sleep(2 * time.Second) } }()
A goroutine is launched to periodically call the
renderInfo
method every 2 seconds.renderInfo
method generates random information and updates the TUI layout.func renderInfo(updateCh chan<- *tview.Flex) { randomInt := rand.Int() randomFloat := rand.Float64() flex := tview.NewFlex().SetDirection(tview.FlexRow) flex.SetBorder(true) title := tview.NewTextView(). SetText(" Sample Title "). SetTextAlign(tview.AlignCenter) box1 := createResourceBox(fmt.Sprintf("Box 1 Random Int : %d ", randomInt)) box2 := createResourceBox(fmt.Sprintf("Box 2 Random Float : %.2f ", randomFloat)) flex.AddItem(title, 1, 1, false). AddItem(box1, 1, 1, false). AddItem(box2, 0, 1, false) updateCh <- flex }
The
renderInfo
function generates random integers and floats, creates a new flex layout with text views displaying this information, and then updates theupdateCh
channel with the updated layout.Let's understand about the
AddItem
method.AddItem(box1, 1, 1, false).
The first argument is the
textView
which has to be added to flex layout. The second argument is thefixedSize
of the tview component.1
means it will occupy one row.0
means it is flexible and can expand to take up all available rows. Third argument is theproportion
of textView in the layout. The last one being thefocus
.
The
renderInfo
function also calls thecreateResouceBox
function:func createResourceBox(title string) *tview.Flex { textView := tview.NewTextView(). SetText(fmt.Sprintf("%s\n\n", title)). SetTextColor(tcell.Color107). SetDynamicColors(true). SetTextAlign(tview.AlignLeft) flex := tview.NewFlex().SetDirection(tview.FlexColumn). AddItem(textView, 0, 1, false) return flex }
The
createResourceBox
function creates a flex layout containing a text view with the specified title. The layout is then returned.
- Goroutine to handle TUI updates:
go func() { for { select { case updatedFlex, ok := <-updateCh: if !ok { return } app.QueueUpdateDraw(func() { app.SetRoot(updatedFlex, true) }) } } }()
Another goroutine is created to handle TUI updates. It listens for updates on the
updateCh
channel and uses theQueueUpdateDraw
method to update the TUI layout with the received information.
- Handle keyboard events:
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape || event.Rune() == 'q' { close(updateCh) app.Stop() } return event })
The
SetInputCapture
method is used to set up a function that captures keyboard events. If the 'q' key or the 'Escape' key is pressed, it closes theupdateCh
channel gracefully and stops the TUI application. Great job ! You have succssfully learned and implemented goroutines, channels, and TUI.There are some more things you could try to improve in this System Monitor:
1. Increase the number of displayed processes. Currently , for simplicy only first three processes are displayed, you can increase it in the function GetRunningProcess located in
systeminfo.go
file.2. Add more system information using other packages of
gopsutil
:- Disk -
"github.com/shirou/gopsutil/v3/disk"
- Network -
"github.com/shirou/gopsutil/v3/net"
3. Enhance the user interface using TUI library. You can make it look better by tweaking and adding more colors to the display.
Happy learning!
-
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.