How to Use complex64 and complex128 in Go

Use complex64 for float32 precision and complex128 for float64 precision when defining complex numbers in Go.

Complex numbers in Go: precision, literals, and the cmplx trap

You are building a signal processor for audio analysis. You need to transform a stream of samples from the time domain into the frequency domain using a Fast Fourier Transform. The math requires numbers that carry both a real component and an imaginary component. You reach for a struct with two float fields, then remember Go has built-in support for complex numbers. You also realize you have a choice between two types: complex64 and complex128. The names look like they refer to the precision of the imaginary part, but they actually describe the total storage size. Picking the wrong one wastes memory or silently truncates your data.

How complex types map to memory

Go provides two complex number types. complex64 stores a real part and an imaginary part, each as a float32. The total size is 64 bits. complex128 stores both parts as float64. The total size is 128 bits. The number in the type name is the sum of the bits for both parts, not the precision of a single part.

This naming convention matches C and many other systems languages. It also means complex128 is not "double precision complex" in the sense of having 128 bits per part. It has 64 bits per part. If you need higher precision than float64, you cannot use the built-in types. You would need a third-party arbitrary-precision library.

For most applications, complex128 is the default choice. It matches the precision of standard float64 arithmetic and avoids casting headaches. complex64 shines in memory-constrained environments or when you are processing millions of values where bandwidth matters more than the last few bits of accuracy.

Precision is a trade-off. Pick the size that matches your math, not your ego.

Creating complex numbers

Go supports complex literals and a built-in complex function. Literals use the i suffix for the imaginary part. The complex function takes two arguments: the real part and the imaginary part. The result type depends on the argument types.

Here is the simplest way to create complex numbers and see how the compiler infers types.

package main

import "fmt"

func main() {
	// Untyped constants default to complex128 when assigned to a variable.
	// The compiler picks the largest type to preserve precision.
	c1 := 3 + 4i
	fmt.Printf("Type of c1: %T\n", c1)

	// The complex() built-in infers the return type from its arguments.
	// Passing float32 arguments produces a complex64.
	c2 := complex(float32(3), float32(4))
	fmt.Printf("Type of c2: %T\n", c2)

	// Mixing float types in complex() causes a compile error.
	// The compiler rejects this with: cannot mix float types in complex call.
	// c3 := complex(float32(3), 4.0) // Error: 4.0 is float64 by default.

	// Explicit casting fixes the type mismatch.
	c4 := complex(float32(3), float32(4.0))
	fmt.Printf("Type of c4: %T\n", c4)
}

The complex function is a built-in. It does not live in a package. You call it just like len or make. The compiler checks the argument types at compile time. If you pass a float32 and a float64, the compiler stops you. You get cannot mix float types in complex call. You must cast one argument to match the other. This rule prevents accidental precision loss inside the constructor.

Untyped constants behave flexibly. The literal 3 + 4i has no type until you assign it or use it in a context that requires a type. When you assign it to a variable with no explicit type, the compiler defaults to complex128. This default ensures you get maximum precision unless you explicitly ask for less.

Complex numbers are first-class values. You can store them in slices, return them from functions, and pass them to methods. The type system treats them like any other numeric type, with the added constraint that real and imaginary parts must share the same underlying float type.

Arithmetic and rotation

Go supports the standard arithmetic operators on complex numbers. You can add, subtract, multiply, and divide complex64 or complex128 values directly. The operators work component-wise for addition and subtraction. Multiplication and division follow the standard complex algebra rules.

Here is how you use multiplication to rotate a point in the complex plane. Multiplication by a complex number with magnitude 1 rotates the vector by the angle of the multiplier.

package main

import "fmt"

// rotatePoint multiplies a point by a rotation factor.
// Multiplication by a complex number with magnitude 1 rotates the vector.
func rotatePoint(point, angle complex128) complex128 {
	// The * operator performs complex multiplication.
	// This handles both rotation and scaling automatically.
	return point * angle
}

func main() {
	// Start with a point at (1, 0) on the complex plane.
	p := 1 + 0i

	// Create a rotation factor for 90 degrees counter-clockwise.
	// cos(90) + i*sin(90) equals 0 + 1i.
	rotation := 0 + 1i

	result := rotatePoint(p, rotation)
	fmt.Println(result) // prints: (0+1i)

	// Rotate again by 90 degrees.
	result2 := rotatePoint(result, rotation)
	fmt.Println(result2) // prints: (-1+0i)
}

The * operator implements the formula (a+bi) * (c+di) = (ac-bd) + (ad+bc)i. You do not need to write this formula manually. The compiler generates the correct instructions. This makes complex arithmetic concise and readable.

Multiplication is also how you scale vectors. If the multiplier has a magnitude greater than 1, the result scales up. If the magnitude is less than 1, the result scales down. Rotation and scaling happen in a single operation.

Multiplication rotates and scales. Use it to transform coordinates without trigonometric functions.

Extracting parts and the cmplx package

You often need to extract the real or imaginary part of a complex number. Go provides the built-in functions real and imag. These functions return the underlying float type. real returns the real part. imag returns the imaginary part.

For advanced math operations like logarithms, exponentials, and square roots, you need the math/cmplx package. This package provides functions that handle the complex plane correctly, including branch cuts and special values.

Here is how you extract components and use the cmplx package for magnitude and phase calculations.

package main

import (
	"fmt"
	"math/cmplx"
)

func main() {
	// complex128 is the default type for complex literals.
	z := 3 + 4i

	// real() and imag() are built-in functions to extract parts.
	// They return the underlying float type of the complex number.
	r := real(z)
	i := imag(z)
	fmt.Printf("Real: %f, Imag: %f\n", r, i)

	// cmplx.Abs calculates the magnitude of the complex number.
	// The math/cmplx package only supports complex128.
	mag := cmplx.Abs(z)
	fmt.Printf("Magnitude: %f\n", mag)

	// cmplx.Phase returns the angle in radians.
	// The angle is in the range [-pi, pi].
	phase := cmplx.Phase(z)
	fmt.Printf("Phase: %f radians\n", phase)
}

The math/cmplx package has a critical limitation. All functions in cmplx accept and return complex128. There are no complex64 variants. If you are working with complex64, you must convert to complex128, call the function, and convert back. This round-trip costs precision and CPU cycles.

This limitation affects your choice of type. If your algorithm relies heavily on cmplx functions, complex64 becomes expensive. You pay for conversion overhead on every operation. Keep this in mind when profiling performance-critical paths.

The cmplx package is the ceiling. Convert up if you need advanced math.

Pitfalls and compiler errors

Complex numbers share many pitfalls with floating-point arithmetic. Precision loss, NaN, and Inf values appear in complex calculations. The compiler catches type mismatches, but runtime errors require careful handling.

Conversion between complex64 and complex128 is allowed but truncates data. Casting complex128 to complex64 drops bits from both the real and imaginary parts. The compiler allows this conversion. The data loss happens silently. If you need exact precision, keep the larger type throughout the calculation.

Comparison with == works for complex numbers, but NaN breaks equality. If either part of a complex number is NaN, the comparison returns false, even if both sides are NaN. This behavior matches float comparison rules. It breaks equality checks in maps or loops. Use cmplx.IsNaN to detect NaN values explicitly.

The compiler enforces type consistency. You cannot add a complex64 to a complex128. You get invalid operation: operator + not defined on mixed types. You must cast one operand to match the other. This rule prevents accidental mixing of precisions.

Complex numbers are values. Treat them like floats: watch the precision, fear the NaN.

When to use complex types

Go's complex numbers are useful for signal processing, graphics, physics simulations, and any domain where 2D vectors or phase information appear naturally. The built-in operators and literals make the code readable. The type system prevents mixing precisions.

Use complex128 when you need maximum precision for scientific calculations or when your input data is already float64. Use complex64 when memory bandwidth is the bottleneck, such as processing millions of samples in real-time audio or image filters on embedded devices. Use a custom struct with two float fields when you need to attach metadata, like a timestamp or a label, to each complex value. Use the math/cmplx package functions when you need logarithms, exponentials, or square roots that handle the complex plane correctly.

Pick the type that fits the math. The compiler handles the rest.

Where to go next