Why does Go make every numeric conversion explicit, and what does that still not catch?
answer
- no promotion table to memorise
- the change shows up in the diff
- constants are untyped, so literals still work
- the compiler checks you wrote it
- not that the value survived it
basics
~20 sGo performs no implicit numeric conversion: mixing an int with an int64, or an int with a time.Duration, is a compile error until you write the conversion. But the conversion you wrote can still truncate or lose precision silently.
solid answer
~50 sGo deliberately has no integer promotion and no implicit widening, because those rules are invisible, differ between languages, and hide where a value changed shape. Writing `int64(n)` puts the change in the diff, so a reviewer can ask whether it is safe. Untyped constants are the escape hatch that keeps the rule bearable — constants have arbitrary precision and no type until they are used, so `var f float64 = 3` and `2 * time.Second` both work; it is only typed *variables* that refuse to mix. The important limit is that the compiler checks that you wrote a conversion, never that the conversion preserves the value. `int64(f)` truncates toward zero, narrowing an `int` to `int32` discards the high bits, and `float64` stops representing consecutive integers above 2^53, so an int64 identifier can round-trip to a different number. Guard those at the boundary with a table-driven test over the extremes.
code
go · 3 linesvar id int64 = 9007199254740993 // 2^53 + 1
f := float64(id)
fmt.Println(int64(f)) // 9007199254740992 - it compiled, and a bit is gonego deeper
Recall that Go never mixes numeric types for you: assigning an int32 to an int64, or multiplying an int by time.Second, needs a conversion you write yourself. Know that plain literals are exempt because constants are untyped.
Explain both sides of the mechanism — no promotion table means one local interpretation and a visible diff, while untyped arbitrary-precision constants keep literals ergonomic. Be able to say what narrowing and float-to-int conversion actually do.
Demonstrate that you know the compiler only checks the conversion exists. Talk about the 2^53 float64 boundary, truncation toward zero, and defending a conversion path with a table-driven test over extremes rather than by review.
Argue the trade at the level of a codebase: strictness costs conversion noise in numeric code and buys named types that stay meaningful across an API. Decide where a domain type is worth the friction and where the code should stay in plain integers.
## The rule Go has no implicit numeric conversion at all. Two values of different numeric types cannot be mixed in an expression, assigned to one another, or passed where the other is expected, no matter how obviously safe the conversion looks. `var i int32 = 5; var j int64 = i` does not compile. Neither does `n * time.Second` when `n` is an `int`. You write `int64(i)` and `time.Duration(n) * time.Second`, and the compiler is satisfied. Contrast this with C's integer promotions and usual arithmetic conversions, or Java's widening, where the language silently reshapes operands according to a table few people can recite. Those rules are where sign-extension bugs, unexpected unsigned comparisons and lost precision live, and they are invisible in the source. ## Why the absence is defensible Three arguments, in the order an interviewer will find convincing: 1. **The change is in the diff.** A conversion is a token a reviewer can see and question. Under implicit promotion the same change is a non-event. 2. **There is one interpretation.** With no promotion table and no operator overloading, an arithmetic expression in Go has exactly one meaning, derivable locally from the types of its operands. 3. **Named types stay meaningful.** `time.Duration`, `os.FileMode` and your own `type UserID int64` behave as distinct types rather than decaying into their underlying integer at every call site. That is what makes a `Duration` API safe to use — and it is exactly why the `time.Sleep` case is a compile error rather than a wrong-by-a-billion sleep. ## The untyped-constant escape hatch The rule would be unbearable if literals were typed, and they are not. A Go constant is untyped and carries arbitrary precision until it is assigned or passed somewhere that gives it a type. That is why all of these are legal: ```go var f float64 = 3 var d time.Duration = 2 * time.Second const big = 1 << 62 x := someInt64 * 3 ``` The constant `3` becomes an `int64` in the last line without a conversion, because it never had a type to convert from. The compiler still checks the value fits: a constant that overflows the destination type is a compile error, not a wraparound. So the strictness applies to *variables*, and constants remain ergonomic. ## What the compiler does not check This is the half that produces bugs, and the half interviewers actually probe. **Integer narrowing wraps.** `int32(x)` where `x` is a large `int64` keeps the low 32 bits and discards the rest. No panic, no error, no vet complaint. **Float-to-integer truncates toward zero.** `int64(f)` for `f = 2.9` is 2, and for `f = -2.9` it is -2 — not -3. If the float value is out of the integer type's range, the result is implementation-specific, so range-check before converting. **Integer-to-float rounds.** `float64` has a 53-bit mantissa. Every integer up to 2^53 is exact; beyond that, consecutive integers are not all representable, and the conversion rounds to the nearest one that is. `float64(9007199254740993)` is 9007199254740992. An `int64` identifier or a byte count pushed through a float pipeline can come back a different number. **The one conversion `go vet` does flag** is `string(i)` for an integer `i` that is not a rune or byte — the `stringintconv` check. `string(65)` is the one-rune string "A", almost never what the author meant; `strconv.Itoa(65)` gives "65". That check exists precisely because the conversion is legal and silent. ## A worked failure Consider an embedded rules engine that evaluates user-authored expressions: `count` values arrive as `int64` from a counter store, `rate` values are `float64` percentages, and the engine multiplies them. Go forces you to write `float64(count) * rate` — the conversion is explicit and looks harmless in review. It is harmless until a counter passes 2^53, at which point the count silently becomes the nearest representable float and every downstream comparison is off by one or more. The language did its job (you wrote the conversion) and the bug shipped anyway. The defence is a **table-driven test over boundary values**, not more reading of the code. Feed the conversion path 0, 1, `1<<53 - 1`, `1<<53`, `1<<53 + 1`, `math.MaxInt64`, and the negative mirrors, and assert the round trip. That test is cheap, it names the exact boundary where behaviour changes, and it stays honest when someone later changes the intermediate type. Where the value must be exact, keep it in integer arithmetic end to end, or use `math/big`. ## Answering it in an interview Say the rule, say the reason, say the constant exception, and then — this is the part most candidates miss — say what the rule does *not* buy you. The absence of implicit conversion moves the decision to the author; it does not make the decision correct.
- If Go refuses to mix numeric types, why does var f float64 = 3 compile?Because `3` is an untyped constant with arbitrary precision and no type of its own until it is used; it simply becomes a float64 here. The strict rule applies to typed variables. The compiler still verifies the constant fits the destination type, so an overflowing constant is a compile error rather than a wraparound.
- How do you keep a lossy conversion from shipping when the compiler will not flag it?Test the boundary rather than re-read the code. A table-driven test over 0, 1<<53 - 1, 1<<53, 1<<53 + 1, math.MaxInt64 and the negative mirrors pins the exact point where a float64 round trip stops being exact. Where exactness is required, stay in integer arithmetic end to end or use math/big.
- Which numeric conversion does the standard toolchain actually warn about?`go vet`'s stringintconv check flags `string(i)` where i is an integer type other than rune or byte, because it produces the string containing that one code point rather than the decimal text. `string(65)` is "A"; the author almost always wanted `strconv.Itoa(65)`. Lossy numeric-to-numeric conversions have no equivalent check.
- What does int64(f) do when f is -2.9?It yields -2. Float-to-integer conversion truncates toward zero, discarding the fractional part rather than flooring, so negative values round up in magnitude terms. If the float is outside the integer type's range the result is implementation-specific, so range-check before converting rather than relying on a wrap.
saying these in an interview costs you the question
- Says Go widens int to int64 automatically like Java
- Believes an explicit conversion guarantees no data is lost
- Thinks int64(f) rounds to nearest instead of truncating
- Writes string(count) expecting the decimal digits
- Assumes float64 represents every int64 exactly