Introduction
Sometimes, when your program has a task that takes a lot of time, like working with databases, web services, or complex calculations, you might want to let it happen in the background. This way, your program can keep running smoothly without waiting for the time-consuming task to finish. In Go, we can achieve this using goroutines and channels. This article explores a straightforward example involving a virtual windowing system.
Implementation
Let’s assume for the sake of simplicity that resizing windows in our hypothetical system is time-consuming. So we want to do that in a different thread.
We will start by defining a ResizeEvent
:
type ResizeEvent struct {
width int
height int
}
Such an events needs a handler, with the following signature:
type ResizeEventHandler func(event ResizeEvent) (int, int, error)
What is returned is the following:
- The new width
- The new height
- If something has gone wrong, you are trying to resize a closed window for example, you can get an error
Now we need a way to listen to the events:
type ResizeEventListener struct {
events chan ResizeEvent
handler ResizeEventHandler
}
func CreateResizeEventListener(handler ResizeEventHandler) ResizeEventListener {
return ResizeEventListener{make(chan ResizeEvent), handler}
}
func (l *ResizeEventListener) Start(w *Window) {
go func() {
for event := range l.events {
new_width, new_height, error := l.handler(event)
if error != nil {
break
}
w.width = new_width
w.height = new_height
}
}()
}
func (l *ResizeEventListener) Stop() {
close(l.events)
}
func (l *ResizeEventListener) Send(event ResizeEvent) {
l.events <- event
}
Some notes:
- In
ResizeEventListener
we define a channel which can receive events, and a function to handle the events - The Start function gets a
Window
struct, which we will define later, as a parameter. This is the window we want to ro resize. - In the
Stop()
method we close the channel, so no more events can be received. - And we use the
Send()
method to send resize events to the window.
Now we come to the Window
struct itself:
type Window struct {
listener ResizeEventListener
title string
width int
height int
}
func CreateWindow(title string, width int, height int, handler ResizeEventHandler) Window {
return Window{CreateResizeEventListener(handler), title, width, height}
}
func (w *Window) Open() {
w.listener.Start(w)
fmt.Printf("Window %s opened with size %dx%d\n", w.title, w.width, w.height)
}
func (w *Window) Close() {
w.listener.Stop()
fmt.Printf("Window %s closed\n", w.title)
}
func (w *Window) Resize(event ResizeEvent) {
w.listener.Send(event)
}
This is pretty self-explanatory, but a few points:
- We add a
listener
field to the struct, so we can listen to resize events. - In the
Open()
method we start listening to resizing events - When we call the
Close()
method, we stop listening to resizing events, since resizing a closed window makes no sense. - The
Resize()
method is used to send resizing events to the window.
Testing time
Time to test our setup:
func main() {
window := CreateWindow("My Window", 800, 600, func(event ResizeEvent) (int, int, error) {
fmt.Printf("Window resized to %dx%d\n", event.width, event.height)
return event.width, event.height, nil
})
window.Open()
window.Resize(ResizeEvent{1024, 768})
window.Resize(ResizeEvent{644, 484})
window.Resize(ResizeEvent{800, 600})
window.Close()
fmt.Printf("Height is %d\n", window.height)
fmt.Printf("Width is %d\n", window.width)
}
What we do here is create a window, with an initial width, height, title and a resizing handler. Next we open,resize the window three times, and close it, after which we print out the width and height of the resulting window.
Conclusion
Implementing a multi-threaded event handler using channels and goroutines in Go is surprisingly simple. While this article focuses on a specific example, there’s room for enhancements, such as building a more generic event handler, which could be a topic for a future article.