Conversion vs Assignability
Understand when Go requires explicit type conversion and when assignment is allowed automatically.
The Core Distinction
In Go, the mechanism for changing a value from one type to another is the conversion expression: T(x). You write it when you need a value of type T and you have a value x of a different type. But you do not always need to write that expression. If the value's type is assignable to the target type, you can assign it directly — no conversion appears in the code.
Assignability is not an operation; it is a compile-time permission. The compiler checks a fixed set of rules. If they pass, the assignment is legal. If they fail, the code won't compile unless you insert an explicit conversion. So the question this page answers is: when can I skip the conversion, and when must I write one?
Conversion Always Copies:
Every explicit conversion in Go makes a copy of the value. It does not merely reinterpret the same bits. This is why Go uses the term “conversion” rather than “cast” — to make the cost and semantics explicit.
How Assignability Works in Go
The Go language specification defines the exact conditions under which a value x can be assigned to a variable of type T without a conversion. These rules apply to assignments, function arguments, return statements, and any other place where a value is moved to a typed location.
Identical Types
When the type of x and the target type T are literally the same type, assignment is always allowed. Type aliases count as identical. For instance, rune is int32, and byte is uint8.
var a int = 42
var b int = a // assignable — same type
var c rune = 'x'
var d int32 = c // assignable — rune is int32
A type defined with type MyInt int is a new, distinct type. Even though its underlying representation is int, it is not identical to int.
One Unnamed Type with Same Underlying Type
Two types with the same underlying type are assignable if at least one of them is unnamed. An unnamed type is a type literal such as []int, struct{ x int }, or *int. A named type is one introduced by a type declaration.
type MySlice []int
var s []int = []int{1, 2, 3}
var ms MySlice = s // assignable — []int is unnamed, MySlice shares its underlying type
type MyInt int
type YourInt int
var a MyInt = 5
var b int = int(a) // conversion required — both MyInt and int are named
var c MyInt = a // assignable — same named type
var d int = 10
var e MyInt = d // compile error — d is int (named), MyInt is named
The last line fails because int and MyInt are both named, even though they share the same underlying type. The compiler demands the explicit conversion MyInt(d).
Channel Direction Compatibility
A bidirectional channel can be assigned to a send-only or receive-only channel variable. The underlying element type must match.
ch := make(chan int)
var send chan<- int = ch // assignable — bidirectional to send-only
var recv <-chan int = ch // assignable — bidirectional to receive-only
The reverse is never true: you cannot assign a send-only channel to a variable of type chan int.
Interface Assignability
A value of a concrete type can be assigned to an interface variable if the concrete type implements the interface. This is the central mechanism of Go’s polymorphism.
var w io.Writer = os.Stdout // os.Stdout's type implements io.Writer
No conversion syntax appears — the assignment relies purely on the interface contract. The compiler verifies at compile time that the method set of the concrete type satisfies the interface.
Nil Assignability
The predeclared identifier nil can be assigned to any variable of type slice, map, channel, function, pointer, or interface. This is why var s []int = nil compiles without a conversion.
Untyped Constants
An untyped constant has no fixed type; it carries a kind and a mathematical value. It can be assigned to any compatible type as long as the value can be represented in that type without overflow or precision loss. This often looks like an automatic conversion but is still governed by assignability, not by a conversion operation.
const c = 42
var i int = c // OK — 42 fits in int
var b byte = c // OK — 42 fits in byte
var f float64 = c // OK
var s string = c // compile error — cannot use untyped int constant as string
Untyped constants are the only reason you can write var f float64 = 10 without writing float64(10). The constant 10 is not an int; it is an untyped integer that the compiler directly accepts for any integer or floating-point type that can hold it.
When Explicit Conversion Is Required
As soon as the assignability rules are not satisfied, the compiler refuses the assignment and demands an explicit conversion. This is the normal state of affairs when numeric types differ in size or kind, when you want to treat a string as a byte slice, or when you have two distinct named types that share an underlying type.
var i int = 100
var i64 int64 = i // compile error — different types
var i64 int64 = int64(i) // explicit conversion required
var f float64 = 3.14
var i2 int = int(f) // conversion truncates to 3; not assignable
type Celsius float64
type Fahrenheit float64
var c Celsius = 20
var fahr Fahrenheit = Fahrenheit(c) // conversion needed — both named, different types
String and byte slice conversions are equally explicit:
s := "hello"
b := []byte(s) // conversion, not assignable
int and int64 Are Different Types:
Developers coming from languages that silently widen integers often write var i int = 5; var i64 int64 = i. Go rejects that. Always write int64(i). The compiler is protecting you from unintentional size mismatches.
The conversion rules are broader than assignability. For instance, you can convert between all numeric types, between strings and byte or rune slices, and between types that share the same underlying type ignoring struct tags. The price is that the operation is always explicit and always creates a new value.
Untyped Constants and the Boundary Between Conversion and Assignability
Untyped constants deserve closer attention because they create the illusion of implicit conversion. When you write var f float64 = 10, you are not converting an int to float64. The 10 is an untyped integer constant, and the assignability rule for untyped constants allows it to be stored directly into a float64 if the value is representable.
If you instead define var i int = 10 and then try var f float64 = i, the compiler rejects it. The type of i is int, a named type, and the assignability rules do not permit assigning an int to a float64 — a conversion float64(i) is required.
const million = 1_000_000
var i int = million // OK
var i8 int8 = million // compile error: 1000000 overflows int8
This distinction matters because untyped constants give you "free" assignment to any compatible numeric type, while typed variables lock you into requiring explicit conversions whenever the target type differs. The design eliminates accidental precision loss and forces deliberate decisions when converting between concrete types.
Leveraging Untyped Constants for Cleaner Code:
Defining numeric constants without a type (e.g., const defaultTimeout = 30) lets you assign them to int, float64, time.Duration, or any compatible type later without writing a conversion each time. This keeps code readable and reduces noise.
Common Pitfalls and Misconceptions
- Assuming
intandint64are interchangeable: They are distinct types. Aninton a 64-bit platform has the same size asint64, but the types are still different. Assignment always requires an explicit conversion. - Confusing type assertion with conversion:
v.(T)extracts a concrete value from an interface. It is not a tool for converting afloat64to anint. Using it on a non-interface value is a compile error. - Thinking conversion is free: Every conversion copies the value. Numeric conversions may produce entirely different bit patterns —
int64(5)andfloat64(5)share no bits in common. The copy is small, but it is not a reinterpretation of existing memory. - Overlooking truncation:
int(3.9)yields3, not4. If you need rounding, callmath.Roundfirst. - Expecting named types with identical underlying representation to be assignable:
type Age int; var a Age = 25; var i int = afails. This protects against accidentally mixing semantically different values, like using an age where a count was intended.
Data Loss from Truncation:
Converting a floating-point number to an integer discards the fractional part silently. A calculation that relies on the decimal portion will produce subtly wrong results if you forget to round. Always verify whether truncation is what you actually want.
Summary
The boundary between conversion and assignability is not just a language-lawyer detail — it directly shapes how you write Go every day.
- Assignability is a compile-time permission. It lets you assign a value without a conversion when types are identical, when an unnamed type shares the underlying type, when channels match direction, when interfaces are implemented, when
nilis appropriate, or when an untyped constant fits the target type. - Conversion is the explicit operation
T(x). It creates a copy and may change the value’s representation. You reach for it whenever assignability does not hold, which is the majority of cross-type assignments. - Untyped constants blur the line, offering direct assignment to many types without a conversion — but only because they are not yet typed.
A mental model that serves well: if the value must change its representation to fit the target, Go requires a conversion. If the value already fits as is according to the language’s rules, assignability may apply. Internalising this distinction will eliminate a large fraction of compile errors and help you write code whose intent is clear at a glance.