Rendering the Mandelbrot Set in Go (because why not?)
So I decided to generate the Mandelbrot set using Go. You know, that famous fractal with the black bulbous shape that looks kinda cool. I've always been curious about how it works, and Go seemed like a good fit for this since it has built-in concurrency support ( i just want to use go because it has cool logo on it).
What even is the Mandelbrot set?
According to wikipedia: It's basically a mathematical set of complex numbers that don't "blow up" when you iterate them through a simple equation: z² + c. You start with z = 0, then keep calculating z² + c over and over. If the value stays bounded (doesn't go to infinity), that point is in the set.
The interesting part? Despite using such a simple formula, you get this infinitely complex fractal shape. Zoom in anywhere on the boundary and you'll find new patterns forever. Pretty wild.
Setting up the basics
First, we need our imports and constants:
package main
import (
"image"
"image/color"
"image/png"
"os"
"runtime"
"sync"
)
const (
width = 2400
height = 1800
maxIter = 1000
xMin, xMax = -2.5, 1.0
yMin, yMax = -1.25, 1.25
)
The constants define our image dimensions and the complex plane coordinates we're rendering. The Mandelbrot set lives roughly between -2.5 to 1.0 on the real axis and -1.25 to 1.25 on the imaginary axis.
Computing the Mandelbrot iteration
This is the core algorithm:
func mandelbrot(cx, cy float64) int {
var x, y, xx, yy float64
for i := 0; i < maxIter; i++ {
xx = x * x
yy = y * y
if xx+yy > 4 {
return i
}
y = 2*x*y + cy
x = xx - yy + cx
}
return maxIter
}
For each point (cx, cy), we iterate the equation z = z² + c. The variables x and y represent the real and imaginary parts of z. If the magnitude squared (xx + yy) exceeds 4, we know the point will escape to infinity, so we return how many iterations it took. Points that survive all iterations are in the set.
Mapping iterations to colors
Now we need to turn those iteration counts into actual colors:
func getColor(iter int) color.RGBA {
if iter == maxIter {
return color.RGBA{0, 0, 0, 255}
}
t := float64(iter) / float64(maxIter)
r := uint8(9 * (1 - t) * t * t * t * 255)
g := uint8(15 * (1 - t) * (1 - t) * t * t * 255)
b := uint8(8.5 * (1 - t) * (1 - t) * (1 - t) * t * 255)
return color.RGBA{r, g, b, 255}
}
Points in the set (that hit maxIter) are colored black. Everything else gets a smooth gradient based on how quickly it escaped. I'm using polynomial functions to create a blue-cyan-yellow color scheme.
Rendering rows in parallel
Instead of processing the entire image sequentially, we can render each row concurrently:
func renderRow(img *image.RGBA, y int, wg *sync.WaitGroup) {
defer wg.Done()
for x := 0; x < width; x++ {
cx := xMin + (xMax-xMin)*float64(x)/float64(width)
cy := yMin + (yMax-yMin)*float64(y)/float64(height)
iter := mandelbrot(cx, cy)
img.Set(x, y, getColor(iter))
}
}
Each pixel coordinate is mapped to a point in the complex plane, then we compute the Mandelbrot iteration and set the pixel color. The WaitGroup ensures we don't finish before all rows are done.
Putting it all together
Finally, the main function orchestrates everything:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
img := image.NewRGBA(image.Rect(0, 0, width, height))
var wg sync.WaitGroup
for y := 0; y < height; y++ {
wg.Add(1)
go renderRow(img, y, &wg)
}
wg.Wait()
f, err := os.Create("mandelbrot.png")
if err != nil {
panic(err)
}
defer f.Close()
if err := png.Encode(f, img); err != nil {
panic(err)
}
println("Generated: mandelbrot.png")
}
We create the image buffer, spawn a goroutine for each row, wait for them all to complete, then save the result as a PNG. The runtime.GOMAXPROCS(runtime.NumCPU()) line makes sure we're using all available CPU cores.
Running it
Save everything as mandelbrot.go and compile:
go build mandelbrot.go
./mandelbrot
On my 12-core machine, it generates the 2400x1800 image in a few seconds. The parallel rendering makes a huge difference compared to processing row by row.
The Mandelbrot set is one of those things that seems simple on paper but reveals endless complexity when you actually explore it. Pretty satisfying to implement from scratch.