The compiler stops you before you lose data
You are writing a command-line tool that reads a configuration value. The flag parser returns a string. Your internal logic needs an integer to calculate a retry count. You pass the string directly to the calculation function. The compiler rejects the program with cannot use countStr (type string) as type int in argument. You stare at the code. The string contains digits. The function needs a number. Why does Go refuse to make this connection?
Go is a statically typed language. The compiler checks types before the code runs. It enforces a strict rule: values do not change type automatically. If you want to treat a string as an integer, you must perform an explicit conversion. This rule prevents silent bugs where data loses precision or meaning without your knowledge. The error message is not a suggestion. It is a block. You must fix the type mismatch to proceed.
Go refuses to guess your intent
In dynamically typed languages, passing a number to a function that expects a string might work because the runtime converts the value on the fly. Go does not do this. Go assumes that type changes carry side effects. Converting a float to an integer truncates the decimal. Converting a large integer to a smaller type might overflow. Converting a string to an integer might fail if the string contains letters.
If Go allowed implicit conversions, these side effects would happen invisibly. You might pass a float to an integer parameter and lose the fractional part without realizing it. The compiler would let the code run, and the bug would hide in production. Go forces you to write the conversion explicitly. This makes the side effect visible in the source code. Anyone reading the code sees the conversion and knows that data might change.
Think of types like physical connectors. You cannot jam a USB-C cable into an HDMI port and expect the monitor to work. You need an adapter. In Go, the type conversion syntax is the adapter. It requires you to acknowledge the conversion. The compiler checks that the adapter exists and that the conversion is valid. If the conversion is impossible, the compiler stops you.
Minimal example: the numeric trap
Numeric conversions are the most common source of this error. Go has distinct types for integers and floating-point numbers. int and float64 are not interchangeable.
package main
import "fmt"
// CalculateDiscount applies a percentage to a price.
// It expects the price in cents as an integer.
func CalculateDiscount(priceCents int, percent float64) int {
// Convert percent to a multiplier.
multiplier := percent / 100.0
// Calculate discount amount.
// int() converts the float result to an integer.
discount := int(float64(priceCents) * multiplier)
return priceCents - discount
}
func main() {
// price is a float64 from a JSON payload.
price := 19.99
// This line causes a compile error:
// cannot use price (type float64) as type int in argument
// fmt.Println(CalculateDiscount(price, 10.0))
// Convert price to int explicitly.
// This truncates the decimal, so 19.99 becomes 19.
priceCents := int(price * 100)
fmt.Println(CalculateDiscount(priceCents, 10.0))
}
The compiler rejects the commented-out line because price is a float64 and CalculateDiscount expects an int. The fix is int(price * 100). The int() call is the conversion. It tells the compiler you accept the risk of truncation. The code compiles and runs.
Go does not guess. You write the adapter.
What the compiler checks
When the compiler encounters a function call, it builds a type graph. It looks at the argument types and the parameter types. It checks if there is a valid edge between them. For identical types, the edge exists. For compatible types with an explicit conversion, the edge exists. For incompatible types, the edge is missing.
The compiler emits cannot use X (type Y) as type Z when the edge is missing. This is a compile-time error. The program does not run. This is a safety feature. It catches type mismatches during development. You fix the error by adding a conversion or changing the variable type.
The compiler also checks for valid conversions. Not all types can be converted to each other. You can convert between numeric types. You can convert between strings and byte slices. You can convert between pointers and interfaces. You cannot convert a struct to an integer. If you try, the compiler rejects the program with cannot convert x (type Struct) to type int. The conversion must make sense according to the language rules.
Realistic scenario: interfaces and pointers
Interfaces are a frequent source of type errors. Go interfaces are satisfied implicitly. A type implements an interface if it has all the methods in the interface. However, the method signatures must match exactly. Pointer receivers and value receivers are distinct.
package main
import "fmt"
// Logger defines a logging behavior.
type Logger interface {
Log(msg string)
}
// AppLogger implements Logger with a pointer receiver.
type AppLogger struct {
Prefix string
}
// Log prints a message with the prefix.
// The receiver is a pointer, so only *AppLogger satisfies Logger.
func (l *AppLogger) Log(msg string) {
fmt.Println(l.Prefix + ": " + msg)
}
// ProcessMessage accepts a Logger.
func ProcessMessage(l Logger, msg string) {
l.Log(msg)
}
func main() {
logger := AppLogger{Prefix: "INFO"}
// This line causes a compile error:
// cannot use logger (type AppLogger) as type Logger in argument
// because AppLogger does not implement Logger (Log method has pointer receiver)
// ProcessMessage(logger, "started")
// Pass a pointer to satisfy the interface.
ProcessMessage(&logger, "started")
}
The error occurs because AppLogger has a Log method with a pointer receiver. Only *AppLogger has that method in the set of methods that satisfy the interface. The value type AppLogger does not implement Logger. The compiler complains with cannot use logger (type AppLogger) as type Logger in argument. The fix is to pass &logger.
The receiver name is usually one or two letters matching the type. The code uses (l *AppLogger) not (this *AppLogger). This convention keeps method signatures short and readable.
Pointer receivers and value receivers are distinct types. Match them or use the address operator.
Realistic scenario: JSON and the interface bucket
JSON unmarshaling often produces interface{} values. The encoding/json package stores unknown types as interface{}. When you unmarshal into a map[string]interface{}, the values are interfaces. You must assert the concrete type before using the data.
package main
import (
"encoding/json"
"fmt"
)
func main() {
data := []byte(`{"count": 5, "name": "Alice"}`)
var result map[string]interface{}
// Unmarshal stores values as interface{}.
json.Unmarshal(data, &result)
// count is an interface{} holding a float64.
count := result["count"]
// This line causes a compile error:
// cannot use count (type interface{}) as type int in argument
// fmt.Println(doubleCount(count))
// Assert the type to float64, then convert to int.
// JSON numbers are always float64.
countFloat := count.(float64)
countInt := int(countFloat)
fmt.Println(doubleCount(countInt))
}
// doubleCount returns twice the input.
func doubleCount(n int) int {
return n * 2
}
The variable count is an interface{}. The function doubleCount expects an int. The compiler rejects the call with cannot use count (type interface{}) as type int in argument. You cannot convert an interface directly to an int. You must first assert the underlying type. JSON numbers are stored as float64. The code asserts count.(float64) to get the float, then converts to int.
JSON unmarshaling produces interface{} values. Assert the type before you use the data.
Pitfalls: truncation, overflow, and size
Type conversions are explicit, but they can still hide bugs. The compiler allows conversions that might lose data. It is your responsibility to understand the consequences.
Converting a float to an integer truncates the decimal. int(3.99) is 3. The compiler does not warn you. The conversion is valid. The data loss happens at runtime. If you need rounding, use the math package.
Converting between integer sizes can overflow. int64 holds larger values than int on 32-bit systems. Converting a large int64 to int wraps the value. The compiler allows this conversion. The result might be wrong. Check the value range before converting.
The compiler complains with cannot use x (type int64) as type int in argument if you pass the wrong type. The fix is int(x). The conversion works, but the value might change. Be careful with size mismatches.
Truncation is silent. The compiler lets you shoot yourself in the foot with int(float). Read the code like a human.
Decision: when to convert and how
Use a type conversion like int(x) when you need to change between basic numeric types and you accept the risk of truncation or overflow.
Use a type assertion x.(T) when you have an interface value and you need to recover the underlying concrete type.
Use a struct field assignment or a helper function when you are mapping between two complex types with different shapes.
Use the address-of operator &x when a function or interface requires a pointer but you hold a value.
Use the dereference operator *x when you have a pointer but the target expects the underlying value.
Use no conversion when the types already match: Go allows implicit assignment between identical types, so check your variable declarations first.
Explicit conversions keep the intent clear. The reader knows exactly where the type changes.