Imaginary Literals
How Go represents imaginary numbers as literals, their syntax rules, and how to use them to form complex constants
An imaginary literal is the text representation of a pure imaginary number in Go source code — a number that, when squared, yields a negative real result. They are not standalone curiosities. Go uses imaginary literals as the building block for complex constants, which the language treats as first‑class values through the complex64 and complex128 types.
Why Imaginary Literals Exist in Go
Most general‑purpose languages leave complex numbers to libraries. Go chose to make them a native part of the type system. The reason is practical: network analysis, signal processing, control theory, and many simulation domains model the world with complex numbers. If the language cannot express them directly, developers end up with ad‑hoc structs, awkward arithmetic, and code that strays far from the mathematical notation it intends to implement.
An imaginary literal is the smallest piece of that support. It lets you write 2.5i and have the compiler understand that as the imaginary constant 2.5 * i, where i is the imaginary unit. Combined with a real part (3 + 2.5i), you get a complex constant that the compiler validates and the runtime operates on with direct hardware support for the underlying floating‑point parts.
How an Imaginary Literal Is Written
The syntax is defined by a single rule: a valid integer or floating‑point literal, immediately followed by a lowercase i.
imaginary_lit = (decimal_digits | int_lit | float_lit) "i" .
That means all of these are legal imaginary literals:
0i
10i
0123i // special: decimal, not octal (see below)
0o123i // octal integer part: 0o123 * i
0xabci // hexadecimal integer part: 0xabc * i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i
0x1p-2i // hexadecimal floating‑point: (0x1 * 2⁻²) * i = 0.25i
The numeric part before the i can be any integer literal (decimal, binary, octal with 0o or 0O, hexadecimal) or any floating‑point literal (decimal or hexadecimal). Since Go 1.13, underscore separators are allowed inside that numeric part, subject to the same rules as in ordinary number literals.
1_000i // valid: underscore between digits
0b_1010i // valid: underscore after binary prefix
1_0.0_5e1_0i // valid: underscores in the floating‑point part
Underscore Rules Still Apply:
You cannot place an underscore next to the trailing i because i is not a digit. 1_i and 1._i are both illegal — the underscore would sit between a digit and a non‑digit. The last character before i must be a legitimate final character of a number literal, which an underscore never is.
The Octal Backward‑Compatibility Rule
There is one subtlety that surprises developers who are used to Go’s normal integer literal rules. The language specification states:
For backward compatibility, an imaginary literal’s integer part consisting entirely of decimal digits (and possibly underscores) is considered a decimal integer, even if it starts with a leading
0.
In regular Go source code, 0123 is an octal literal (value 83 in decimal). But 0123i is not 0o123i. It is the imaginary literal whose magnitude is the decimal number 123.
const (
a = 0123i // decimal 123i, for backward compatibility
b = 0o123i // octal 0o123 * i = 83i
c = 123i // decimal 123i
)
This exception exists because early versions of Go only allowed floating‑point literals before i. An integer literal with a leading zero was not permitted at all. When Go 1.13 extended imaginary literals to accept integer parts, code that already wrote 0123i (expecting 123i) would have broken if the integer part suddenly became octal. The compromise: an all‑decimal integer part before i is always decimal, no matter the leading zero.
The Leading Zero Trap:
Writing 0123i and expecting it to equal 0o123i is the most common error with imaginary literals. If you intend to express an octal magnitude, you must use the explicit 0o prefix: 0o123i. The compiler will not warn you; it will silently interpret 0123i as decimal.
Using Imaginary Literals to Construct Complex Numbers
An imaginary literal alone is a valid constant of kind “imaginary.” On its own it is not a complex number — it is the pure imaginary component. To create a complex constant, combine it with a real part using addition (or subtraction):
const c = 3 + 2.5i // complex constant: real 3.0, imag 2.5
var z complex128 = 1 + 0i
The addition operator between a numeric constant and an imaginary literal is evaluated at compile time, producing an untyped complex constant. You can assign it to any complex variable or use it in constant expressions.
If you assign a bare imaginary literal to a complex variable, the real part defaults to zero:
var z complex128 = 2.5i // same as 0 + 2.5i
That assignment works because 2.5i is an untyped imaginary constant, and Go’s assignability rules allow an untyped imaginary constant to be used wherever a complex type is expected — the missing real part is implicitly zero.
package main
import "fmt"
func main() {
a := 1 + 2i
b := 2i // identical to 0 + 2i
fmt.Println(a) // (1+2i)
fmt.Println(b) // (0+2i)
// Arithmetic works natively:
fmt.Println(a + b) // (1+4i)
}
Complex Constants Are First‑Class:
When you write 1 + 2i in Go, the expression is fully evaluated by the compiler. You are not calling a constructor or relying on a library; the resulting constant is as primitive as an integer. This means you can use it in constant declarations, array sizes (for the real part, since array sizes must be integers), and compile‑time arithmetic.
Common Mistakes and Misconceptions
Several patterns trip up developers who are new to imaginary literals.
Imaginary Literal vs. Complex Number:
An imaginary literal represents a pure imaginary value, not a full complex number. If you store 10i in a complex128, you get (0+10i). That is perfectly fine, but many newcomers assume they must always “wrap” it with a zero real part. The language handles the zero for you.
Mistaking 0123i for octal. This error is silent. If you port code that used 0o‑prefixed octal in integer literals and forget to add the prefix to an imaginary literal, the result will be a decimal magnitude that is wrong by a large margin with no compile error.
Misplacing underscores. Placing an underscore right before the i (1_i) or before a decimal point in a floating‑point part (1._0i) causes a syntax error. The rules that apply to integer and floating‑point literals carry over: underscore must separate digits, not sit at the edge.
Forgetting that hexadecimal floating‑point imaginary literals require an exponent. The literal 0x1p-2i is valid because 0x1p-2 is a complete hexadecimal floating‑point literal (with the mandatory p exponent). Writing 0x1.i is illegal: 0x1. has no exponent, so it is not a valid floating‑point literal, and therefore 0x1.i is not a valid imaginary literal. The p‑exponent is not optional.
Confusing the imaginary unit with a variable name. The i in 10i is not an identifier. It cannot be replaced by any other letter. You cannot write 10j and expect it to work, even if mathematically you are used to j. Go’s imaginary suffix is always the lowercase i.
Treating 10.52i and 10i as comparable with == without understanding complex equality. The expression 10.52i == 10i compares two complex values, not two imaginary parts. The first is (0+10.52i), the second is (0+10i). The result is false because the values differ. This is not a quirk of imaginary literals but of complex number comparison, but it often surprises people who think they are comparing “just the i‑part.”
package main
import "fmt"
func main() {
fmt.Println(10.52i == 10i) // false, real parts both 0 but imag differ
fmt.Println(10i == 0+10i) // true
}
Imaginary Literals in Real‑World Go Code
Outside of textbook examples, imaginary literals appear wherever Go programs do heavy numeric work. The standard library’s math/cmplx package is built entirely around complex128 values, and you will see literals like 1i and 0+1i used in its tests and benchmarks.
Domain‑specific libraries that implement FFTs, digital filters, or complex‑valued linear algebra also rely on imaginary literals for readable, constant‑time initialization. Rather than calling complex(0, 1), authors write 1i — it is shorter, evaluated at compile time, and directly matches the mathematical notation they are translating into code.
When you encounter an unfamiliar Go codebase that uses complex128 and you see constants like 0.7071067811865476i or 1.0+0.5i, recognize that these are exact compile‑time values. They carry no runtime overhead and express the programmer’s intent without indirection.
The imaginary literal is also the only way to create an untyped imaginary constant. The built‑in complex function returns a typed complex value, which cannot be used in constant declarations. For compile‑time constant complex numbers, realPart + imagPart*i is the mandatory idiom.
Summary
Imaginary literals are a small language feature that anchors Go’s native complex number support. The syntax is intentionally narrow — a numeric literal followed by i — but carries a handful of precise rules about leading zeros, underscore placement, and hexadecimal floating‑point exponents. The most consequential rule is the octal backward‑compatibility exception: a decimal‑only integer part before i is always decimal, even with a leading zero, unless you explicitly use the 0o prefix.
Writing 1 + 2i in Go is not a library call; it is a constant expression that the compiler understands as a complex number with no runtime cost. This property sets Go apart from languages that relegate complex numbers to constructors, and it means you can reason about complex constants as confidently as you reason about integers and floats.