JavaRush /Courses /Go SELF /Explicit type conversions and overflows in Go

Explicit type conversions and overflows in Go

Go SELF
Level 7, Lesson 4
Available

1. Incompatible types and the syntax T(x)

When you are just starting to program, you really want the language to “figure out” what you meant. “I just added two numbers—what difference does it make whether it’s int or int64?”—sounds logical… until the program starts doing expensive dumb things. Go is deliberately designed so that in ambiguous places you make the decision explicitly: which type, which range, and which result you expect.

Imagine you have two measuring cups in the kitchen: one in milliliters, the other in ounces. By eye they “seem about the same”, but in a cake recipe this suddenly turns into “bread cake”. Types in Go are like those measuring cups. The compiler says: “I won’t guess—pick the measurement system yourself.” That’s why there is almost no “auto-casting”, and the conversion is written directly in the code.

What the compiler forbids in expressions

Go forbids mixing different numeric types in a single expression without an explicit conversion. This is not “picky behavior”, but protection from silent bugs that can live for years in other languages.

The most common example is int and int64. Both look like integers, but from the compiler’s point of view they are different types, and it does not have to guess which one is more important.

package main

import "fmt"

func main() {
	a := 10          // int
	var b int64 = 20 // int64

	fmt.Println(a + b) // not allowed: different types
	fmt.Println(int64(a) + b) // 30
}

It is important to catch the idea here: the compiler is not saying “you can’t add numbers”, it is saying “you can’t add values of different types without explaining the rules to me”.

A similar story happens with float64 and int. If you write financial or measurement calculations, you often need fractions. But if an int sneaks in somewhere, Go will not “automatically” turn everything into float64, because that can change the meaning.

package main

import "fmt"

func main() {
	price := 99.99 // float64
	qty := 2       // int

	total := price * qty // not allowed
	total := price * float64(qty)
	fmt.Println(total) // 199.98
}

And this rule applies not only to arithmetic. For example, len(s) returns int, and if you store a counter in int64, the compiler will again ask you to decide.

Explicit conversion T(x): how to read it and how to “think” with this syntax

Conversion syntax in Go looks like this: T(x), where T is the target type and x is the value. It’s important to see this not as magic, but as an explicit command: “take the value x and represent it as type T”.

Psychologically, it’s convenient to read it like this:

  • int64(a) — “now we treat a as int64”
  • float64(qty) — “convert quantity to a fractional number, because we’ll have fractions next”
  • int(x) — “keep only the integer part”

For debugging (and for training your brain), it is very helpful to print types:

package main

import "fmt"

func main() {
	var n int64 = 42
	m := int(n)

	fmt.Printf("n=%v (%T)\n", n, n) // n=42 (int64)
	fmt.Printf("m=%v (%T)\n", m, m) // m=42 (int)
}

At this stage, beginners often ask: “Why can’t I just write m := n?” Answer: because then the language would have to decide whether this conversion is allowed, whether it’s safe, what platform you’re running on, and whether you’ll lose data. Go puts responsibility on the programmer: if you want it—do it, but write it explicitly.

3. Overflows: during conversion and during arithmetic

Overflow when converting integers

The most treacherous situation is when you convert a number to a narrower type. For example, int64 to int32, or even just a large int to int8. Data loss (range overflow) is possible here, and the program is not required to crash—it often continues running with the wrong number.

There are two different cases: constants and variables.

If you try to assign a constant/literal that does not fit into the type, the compiler will complain. This is useful: it catches the bug immediately at build time. Go checks quite strictly whether constants “fit” into the target type.

But if the value is stored in a variable, the compiler will often allow the conversion—because formally you asked for it—and from that point on the responsibility is yours: do not lose the meaning.

Let’s look at a “silent break” example:

package main

import "fmt"

func main() {
	var big int64 = 130
	var small int8 = int8(big)

	fmt.Println(big)   // 130
	fmt.Println(small) // -126
}

Why -126? Because int8 can store only the range -128..127. The number 130 does not fit there, and during conversion only the “lower bits” remain (roughly speaking, the number “wraps around” within the range). This is not random; it is predictable math of binary representation.

To keep the picture in mind, a simple table helps:

Situation Example What the compiler does What happens to the data
A constant does not fit into the type
var x int8 = 130
Compile error No data is lost because the code will not build
A variable can be “too large”
x := int8(big)
Compiles Data loss is possible (wrap/truncation)
Arithmetic in a narrow type
var b uint8=255; b++
Compiles Overflow “wraps around”

And this is where an adult habit is born: do calculations in a sufficiently wide type, and convert to a narrow type only if you really need to (for example, to store in a specific data format).

Overflow during arithmetic

Overflow happens not only during type conversion, but also during normal arithmetic. And it can be even more dangerous, because the code looks completely harmless.

For clarity, it is convenient to use narrow types like uint8 and int8 (as a learning tool, not as a “default type”).

package main

import "fmt"

func main() {
	var x uint8 = 255
	x++
	fmt.Println(x) // 0
}

And similarly for int8:

package main

import "fmt"

func main() {
	var y int8 = 127
	y++
	fmt.Println(y) // -128
}

Why does Go allow this? Because this is the normal model of how integers work in hardware: machine registers are finite, and there is nowhere to put the “extra bits”. Some languages try to “save” you with an exception (an analog of a “panic” in Go), and some do not. Go chooses predictability and performance: overflow follows the rules of binary arithmetic.

The practical takeaway is simple: if you are counting money, sizes, lengths, quantities—choose a type with headroom. Also try to think about what the maximum of your result can be, especially in multiplications.

4. Transitions between signed and unsigned

Moving between signed (int, int64) and unsigned (uint, uint64) types is a separate area where “the compiler allowed it” does not mean “everything is fine”.

A beginner’s intuitive mistake looks like this: “uint is just ‘without minus’, so if I have an int somewhere, I can convert it to uint and live peacefully.” The problem is that negative numbers under such a conversion turn into large positive ones, because the binary pattern is preserved while the interpretation changes.

package main

import "fmt"

func main() {
	i := -1
	u := uint(i)

	fmt.Println(i) // -1
	fmt.Println(u) // a very large number (depends on uint bit width)
}

Here “a very large number” is not a joke, but a real risk: if you expected an “error” or “absence” somewhere and got a gigantic index/size instead, the program may start behaving strangely.

It is also important to understand a subtle point: the compiler cannot forbid this completely, because sometimes such conversions are needed (for example, when working with low-level data). But in typical application code written by a beginner, this is almost always a signal: “stop and think—are you sure you want uint?”

5. float ↔ int: truncation and precision

Now let’s move on to conversions between integers and floating-point numbers. There are two classic surprises here: truncating the fractional part and losing precision for large values.

The first surprise: int(float64) does not round; it discards the fractional part (truncates toward zero).

package main

import "fmt"

func main() {
	x := 3.99
	n := int(x)

	fmt.Println(n) // 3
}

The second surprise (a bit more “advanced”, but useful to know): float64 cannot exactly store all large integers, and with int -> float64 -> int you can theoretically lose the “exact value”. In practice problems you rarely hit such scales, but it’s important to keep the principle in mind: float is an approximation, and a round-trip conversion may not be perfectly invertible.

And one more practical point: to get a fractional division result, you need to convert operands to float64 before division, because int / int remains integer division.

package main

import "fmt"

func main() {
	a := 7
	b := 2

	fmt.Println(a / b)                   // 3
	fmt.Println(float64(a) / float64(b)) // 3.5
	fmt.Println(float64(a/b) + 0.0)      // 3 (integer division first, then to float)
}

This last line is a typical mistake: the conversion is done, but too late.

6. Example: calculating an order total in kopeks

Now let’s assemble a small piece of our learning app. Let it be a simple utility to “calculate an order total”, where we read the price per item and quantity, and then compute the total. The key point: it is better to count money as integers (for example, in kopeks) so you don’t run into 0.1 + 0.2.

We will agree on this: the user enters the price in rubles (as an integer) and the quantity (as an integer). And inside we will store the total in kopeks as int64, so we have some range headroom.

package main

import "fmt"

func main() {
	var priceEuro int
	var qty int

	fmt.Scan(&priceEuro, &qty)

	totalCent := int64(priceEuro) * 100 * int64(qty)
	fmt.Println(totalCent) // e.g.: 19900
}

Two important things happen here at once. First, we explicitly convert priceEuro and qty to int64, so the multiplication happens in a wide type. Second, we avoid float64 for money (not because float64 is “bad”, but because money usually requires exact integer arithmetic).

Now let’s add “type diagnostics” to make sure we are really computing where we should:

package main

import "fmt"

func main() {
	priceEuro := 199
	qty := 3

	totalCent := int64(priceEuro) * 100 * int64(qty)

	fmt.Printf("priceEuro=%v (%T)\n", priceEuro, priceEuro) // priceEuro=199 (int)
	fmt.Printf("qty=%v (%T)\n", qty, qty)                // qty=3 (int)
	fmt.Printf("totalCent=%v (%T)\n", totalCent, totalCent) // totalKop=59700 (int64)
}

Notice the style: we are not “appeasing the compiler”; we are locking in the meaning. Price and quantity are normal int for simple input. The total is int64, because the total can potentially grow faster (especially if later there are discounts, sums across multiple items, and so on).

If you ever see that your total suddenly became negative or “suspiciously small”, the first thing to check is the types of intermediate computations and possible overflow.

7. Common mistakes with conversions and overflows

Mistake #1: “They’re both numbers, so they’ll add up by themselves”.
The most common pain is trying to add int and int64, or int and float64, and expecting the compiler to “sort it out”. Go intentionally does not sort it out for you; otherwise the language would have to pick a conversion strategy, and that is almost always a source of hidden bugs. The right habit is to choose the “primary” computation type and explicitly convert the other values to it.

Mistake #2: the conversion is done, but too late.
A classic: float64(a/b) instead of float64(a) / float64(b). In the first case, the division already happened as integer division, the fractional part was lost, and you honestly converted “3” into “3.0”. The compiler won’t help here, because formally you performed allowed operations, but the meaning drifted away.

Mistake #3: int(x) is perceived as rounding.
When you write int(3.99), Go does not round; it discards the fractional part. This is truncation toward zero. Because of that, int(-3.99) becomes -3, which is sometimes especially surprising. If you need rounding, that is a separate operation, and it’s important to do it consciously, not “through int”.

Mistake #4: narrow types are used “out of habit” and start overflowing.
int8 and uint8 are useful for demonstrations and some special tasks, but as a “default type” they are dangerous: overflow happens instantly and often looks like “random numbers”. If you’re not sure you need a narrow type, it usually means you don’t need it.

Mistake #5: converting to uint is used to “remove the minus”, and negative values turn into gigantic ones.
If your code contains uint(someInt) just for type compatibility, stop and check whether someInt can be negative. If it can, you risk getting a huge positive number instead of a clear “-1”, and the bug becomes very non-obvious: the program formally runs, but does strange things.

1
Task
Go SELF, level 7, lesson 4
Locked
Cash register total
Cash register total
1
Task
Go SELF, level 7, lesson 4
Locked
Dividing the Report
Dividing the Report
1
Task
Go SELF, level 7, lesson 4
Locked
Overflow check
Overflow check
1
Task
Go SELF, level 7, lesson 4
Locked
Sum of bytes
Sum of bytes
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION