5.3 Complex Numbers: complex64 and complex128
Right, complex numbers. You probably remember these from that one math class you took and swore you’d never use again. Well, surprise. They’re not just academic ghosts; they’re the absolute backbone of entire fields like electrical engineering, signal processing, quantum physics, and computer graphics. And Go, being a practical language for building real systems, has them baked right in as first-class citizens. No need to import some clunky external library; they’re just there, waiting for you.
Go offers two flavors, because of course it does: complex64 and complex128. The naming convention is brutally consistent: the size is the total memory footprint. A complex64 is two float32 numbers (one for the real part, one for the imaginary) stitched together. A complex128 is, you guessed it, two float64 numbers. You’ll use complex128 about 99% of the time for the same reason you use float64—the precision is almost always worth the trivial memory cost on modern hardware.
Literals and Construction
You don’t just add real and imaginary numbers together with a +; the language needs to know you’re building a single complex value. You do this by appending a literal i (the mathematical notation for the imaginary unit) to the imaginary part.
// The most common way: a literal with the 'i' suffix
z := 3 + 4i // Underlying type is complex128
fmt.Printf("z is %v (type %T)\n", z, z) // z is (3+4i) (type complex128)
// Using the built-in complex constructor function
// func complex(r, i FloatType) -> complexType
var w complex64 = complex(1.5, -2.0) // explicit complex64
Notice that 3 + 4i defaults to complex128. This is Go being sensible. If you need the smaller version, you must be explicit, either with a type conversion or by using the complex() function with float32 arguments.
Pulling Them Apart and Basic Arithmetic
To deconstruct a complex number back into its constituent real and imaginary parts, use the built-in real() and imag() functions. And yes, the arithmetic works exactly as your math teacher promised.
z := 3.5 + 2.1i
w := 1.0 + 1.0i
fmt.Println("Real part:", real(z)) // 3.5
fmt.Println("Imag part:", imag(z)) // 2.1
// Addition
sum := z + w // (3.5+1.0) + (2.1+1.0)i => (4.5 + 3.1i)
// Multiplication: (3.5 + 2.1i) * (1.0 + 1.0i)
// = 3.5*1.0 + 3.5*1.0i + 2.1i*1.0 + 2.1i*1.0i
// = 3.5 + 3.5i + 2.1i + 2.1*(i²)
// = 3.5 + 5.6i + 2.1*(-1) because i² = -1
// = (3.5 - 2.1) + 5.6i
// = 1.4 + 5.6i
product := z * w
fmt.Println("Product:", product) // (1.4+5.6i)
The Quirks and The Gotchas
Here’s where I stop being a cheerleader and start being your brilliant friend who points out the wet paint. The biggest “feature” is that the basic comparison operators (== and !=) work, but the ordered operators (<, >, <=, >=) are completely forbidden. This isn’t a Go limitation; it’s a mathematical one. Complex numbers simply don’t have a natural ordering. Is 1+2i greater than 2+1i? There’s no sane answer, so Go refuses to let you ask the question. You’ll get a compile-time error if you try.
The other, more subtle, pitfall is that you’re inheriting all the floating-point baggage. Equality comparisons are a notorious minefield with float32 and float64, and that danger transfers directly to complex numbers. Because complex128 is built on float64, you’re subject to all its rounding errors and precision issues.
a := complex(1.0/3.0, 2.0) // 0.333... + 2i
b := complex(0.3333333333333333, 2.0) // An approximation
// This might be false! Never use == for exact floating-point comparisons.
fmt.Println("Are they equal?", a == b) // Likely false
// Instead, compare the absolute difference (the magnitude) against a small epsilon.
diff := a - b
magnitude := math.Hypot(real(diff), imag(diff))
fmt.Println("Magnitude of difference:", magnitude)
if magnitude < 1e-12 { // Use an appropriate epsilon for your problem
fmt.Println("They are effectively equal.")
}
Useful Functions: The math/cmplx Package
The built-in operations are basic. For anything more advanced—exponents, trigonometry, logarithms—you need the math/cmplx package. It’s your one-stop shop for all complex math needs, mirroring the regular math package but for the complex domain.
import "math/cmplx"
z := 3 + 4i
// Get the absolute value (modulus/magnitude): sqrt(3² + 4²) = 5
abs := cmplx.Abs(z)
fmt.Println("Absolute value:", abs) // 5
// Get the phase angle (argument) in radians
phase := cmplx.Phase(z)
fmt.Println("Phase:", phase) // ~0.927 radians
// Get the complex conjugate (same real part, negated imaginary part)
conjugate := cmplx.Conj(z)
fmt.Println("Conjugate:", conjugate) // (3-4i)
// Other goodies: Exp, Log, Sqrt, Sin, Cos, etc.
root := cmplx.Sqrt(z)
fmt.Println("Square root of z:", root) // (2+i)
So there you have it. Complex numbers in Go are a powerful, native feature that Just Work™, provided you remember they’re built on the inherently imperfect foundation of floating-point math. Use them when you need them, which is more often than you think, and lean on math/cmplx to do the heavy lifting. Just, for everyone’s sake, don’t try to sort a slice of them.