skip to content

Your units package declares Celsius and Fahrenheit as separate float64 types, yet the compiler now accepts mixing them. What changed?

level: seniorimportance: should knowfreq 30%

answer

  1. the declarations changed, not the call sites
  2. one character can delete a guarantee
  3. two names, one type
  4. look for the equals sign in the type declaration

basics

~10 s

Almost certainly a declaration became an alias: type Fahrenheit = Celsius. An alias makes the two names one identical type, so every unit mismatch type-checks. Look for the equals sign in the type declarations.

solid answer

~50 s

Start at the declarations, not the call sites. The overwhelmingly likely cause is that a refactor turned one type definition into an alias: `type Fahrenheit = Celsius`, or both names aliased onto a shared `type Temp = float64`. An alias creates no type, so `Fahrenheit` and `Celsius` become one identical type and every mismatch compiles cleanly — the safety the package existed for is gone, silently, with no error to grep for. Confirm it in seconds: `go doc yourpkg` prints an alias verbatim, with the equals sign, and go-to-definition in an editor jumps straight through the alias to the type it really names. A second, rarer cause is that the shared helper was made generic with a `~float64` constraint, or its parameter widened to `float64`, so both units now instantiate it happily. Fix by restoring the definitions, then lock it down with a test that asserts the two types are distinct, so the next refactor fails loudly instead of quietly.

code

go · 8 lines
go
type Celsius float64
type Fahrenheit = Celsius // alias, not a definition: one type, two names

func Report(c Celsius) { /* ... */ }

func demo(f Fahrenheit) {
	Report(f) // compiles: Fahrenheit IS Celsius
}

go deeper

for a junior

Know where to look first: the type declarations. An alias is written with an equals sign and means the two names are the same type, so no mismatch can be reported.

for a middle

Explain why identity is decided at the declaration and not at the call site, and name the checks - reading the declaration, go doc output, %T on a value of each type.

for a senior

Walk the diagnosis end to end, rank the alternative causes such as a widened parameter or a ~float64 constraint, and then make the guarantee executable so the next refactor fails loudly.

for a principal

Own the standard: which domain distinctions must be compiler-enforced, and the rule that an exported alias in a domain package needs a stated purpose, an owner and a removal date.

## The symptom and what it really means A units package exists to make one class of bug impossible: adding a temperature in Fahrenheit to one in Celsius, passing a sensor reading in the wrong scale to a controller. The mechanism is a *type definition* per unit — `type Celsius float64`, `type Fahrenheit float64` — which gives each unit its own named type, so the compiler rejects mixing them. When that stops working, nothing at the call sites changed. Type identity is decided entirely by the declarations, so that is where you look. ## The likely cause: a definition became an alias ```go type Celsius float64 type Fahrenheit = Celsius // one character; the whole distinction is gone ``` An alias creates no type. After this, `Fahrenheit` and `Celsius` are one type under two names: assignable in both directions, indistinguishable in a type switch, indistinguishable to `reflect`, and identical in every signature. Code that reads as if it is unit-safe now type-checks whatever you pass it. Why would anyone write it? Nearly always a package move. Someone relocated the temperature types to a shared module and left aliases behind so importers kept compiling — the right instinct — but pointed one alias at the wrong target, or collapsed two units onto one during the shuffle. It is a class of change that reviews wave through, because the diff is small and the build stays green. ## Confirming it fast - **Read the declarations.** The equals sign is the whole tell. This is the fastest and most reliable check. - **`go doc yourpkg`** prints the exported surface with declarations verbatim, so an alias appears as `type Fahrenheit = Celsius`. Handy when the type lives in a dependency you have not cloned. - **Go-to-definition** in an editor jumps *through* an alias to the type it names, which is a good hint when the two names resolve to one declaration. - **`%T` or `reflect`** on a value of each type will report the same type name when an alias is in play, and different names when the definitions are intact. ## The other explanations, in order of likelihood 1. **The function was widened.** A helper that used to take `Celsius` now takes `float64`. Nothing is aliased; the check simply moved out of the way, and callers convert at the boundary. Read the signature. 2. **The helper was made generic** with a constraint like `~float64`, whose approximation element admits every type whose underlying type is `float64` — both units included. Convenient and, for genuinely unit-agnostic maths, correct; a silent hole when the function is unit-specific. 3. **Values are being carried as `float64` somewhere in the middle** — in a struct field, a map value, a JSON round trip — and re-typed on the way out. The type system cannot help across that gap. ## Fixing it, and keeping it fixed Restore the definitions: `type Fahrenheit float64`, distinct again. If the type genuinely moved, the alias should point at the *moved* type, not at a sibling unit. Then make the property testable rather than reviewable. A tiny test comparing runtime types fails the moment the two collapse: ```go if reflect.TypeFor[Celsius]() == reflect.TypeFor[Fahrenheit]() { ... } ``` Even cheaper is a compile-time assertion in a `_test.go` file that only compiles while the types are distinct. Either way, the point is the same: the guarantee the package sells is a *type identity* guarantee, and identity guarantees deserve a test, because the change that breaks them is a one-character diff that no linter flags. Finally, a review rule that pays for itself in packages like this: every exported alias carries a comment saying why it exists and when it is deleted. An alias with no stated purpose in a domain-types package is a defect waiting to be discovered by a controller sending steam-temperature values to something expecting Celsius.

  • Why would anyone have introduced that alias in the first place?
    Almost always a package move: the types were relocated and aliases left behind so importers kept compiling, which is the correct technique. The defect is aiming an alias at the wrong target or collapsing two units onto one name during the shuffle — the build stays green, so only a test or a careful reviewer catches it.
  • How do you stop this regressing after you fix it?
    Make the guarantee executable: a test asserting the two runtime types differ, or a compile-time assertion that only builds while they are distinct. Add a review rule that every exported alias states why it exists and when it dies, and diff the package's `go doc` output in CI so a changed declaration is visible in the review.
  • Would rewriting it as type Fahrenheit Celsius restore the safety?
    Yes for identity — that is a definition, so the two are distinct types again — but it also starts with an empty method set, so any methods declared on `Celsius` are gone from `Fahrenheit` and must be redeclared. If the units are meant to be siblings rather than one built on the other, define both directly on `float64`.

saying these in an interview costs you the question

  • Hunts through call sites instead of reading the type declarations
  • Thinks an alias still gives a separate type identity
  • Assumes a green build proves the units are still distinct
  • Fixes the one call site found instead of the declaration
  • Cannot name a way to test that two types stay distinct