The truncation trap
You're building a rate limiter. The configuration file says 0.5 seconds per request. You read that as a float64, cast it to int to get milliseconds, and suddenly your timeout is zero. The rate limiter does nothing. Or you're processing payments. 19.99 becomes 19. You just undercharged by a cent, or worse, you're calculating tax and the truncation errors compound until the audit fails.
Go doesn't guess what you want. It gives you exactly what you ask for, which is often just the integer part, chopped off. Converting a float to an int in Go is a truncation toward zero. It drops the fractional part. It does not round. It does not ask. This behavior catches developers off guard because many other languages round by default, or because the math in your head rounds automatically. Go forces you to be explicit about the data loss.
Go demands explicit conversion
Go treats types as distinct buckets. A float64 is not an int. You cannot mix them in arithmetic without a conversion. If you write var i int = 3.14, the compiler rejects the program. You must write int(3.14). That extra syntax is a checkpoint. It forces you to acknowledge the conversion and decide if the result is correct for your use case.
The conversion syntax is TargetType(Value). For float to int, you write int(floatValue). The compiler checks that the source is numeric and the target is numeric. If you try to convert a string or a struct, the compiler stops you with an error like cannot convert "hello" (untyped string constant) to type int. This check happens at compile time, so you never pay a runtime cost for type safety.
Here's the minimal pattern. The code shows truncation for positive and negative numbers.
package main
import "fmt"
func main() {
// Truncation drops the fraction. 99.99 becomes 99.
price := 99.99
cents := int(price)
fmt.Println(cents) // prints: 99
// Negative numbers move toward zero. -45.8 becomes -45.
temp := -45.8
tempInt := int(temp)
fmt.Println(tempInt) // prints: -45
}
Go doesn't round. You have to ask for it.
What happens under the hood
At compile time, the compiler verifies the types match the conversion rules. At runtime, the conversion is a single CPU instruction. For floats to ints, the processor masks off the fractional bits. There is no division, no multiplication, no branching. It's fast.
The speed comes with a risk. If the float value is larger than the maximum integer the system can hold, the result wraps around. On a 64-bit system, int is 64 bits, so the range is huge. On a 32-bit system, int is 32 bits. If you convert a float larger than 2147483647, you get a negative number. The compiler won't warn you if the value comes from a variable. You have to check the range manually.
The int type size depends on the architecture. This is a convention that trips up beginners. int matches the native word size of the machine. On most modern servers, int is 64 bits. On embedded devices or 32-bit Windows, int is 32 bits. If you serialize an int to a database or a network protocol, the size might change when you deploy to a different machine. The community convention is to use int for loop counters and slice indices, and fixed-width types like int32 or int64 for I/O and storage. Use int64(float) when you need a guaranteed width.
Rounding is a separate step
Truncation is rarely what you want for measurements. If you have 3.9 meters, you probably want 4 meters, not 3. Go provides math.Round, math.Floor, and math.Ceil for this. These functions return a float64. You still need to cast to int.
This two-step process is intentional. It separates the mathematical operation from the type conversion. You can chain them: int(math.Round(floatVal)). This makes the code readable. You see the rounding, then the cast. If Go allowed a single function to round and cast, you might forget the rounding step. The explicit cast acts as a reminder.
Here's a realistic example. A configuration parser reads a timeout in seconds and converts it to milliseconds. Rounding ensures 0.5 seconds becomes 500 milliseconds, not 0.
package main
import (
"fmt"
"math"
)
// ParseTimeout converts seconds to milliseconds, rounding to avoid zero-timeouts.
// Rounding ensures 0.5 seconds becomes 500ms, not 0ms.
func ParseTimeout(seconds float64) int {
// Multiply by 1000 to shift decimal.
ms := seconds * 1000
// Round to nearest integer before casting.
// math.Round returns a float64, so we still need the cast.
return int(math.Round(ms))
}
func main() {
// 0.5 seconds should be 500ms.
timeout := ParseTimeout(0.5)
fmt.Println(timeout) // prints: 500
// 0.49 seconds rounds to 490ms.
short := ParseTimeout(0.49)
fmt.Println(short) // prints: 490
}
Rounding is a business decision. The compiler won't make it for you.
Parsing from strings
Often you don't start with a float. You start with a string from a file, an API, or user input. You use strconv.ParseFloat to get the number. This function returns a float64 and an error. You must handle the error before converting.
The pattern is val, err := strconv.ParseFloat(s, 64). The 64 tells the parser to use 64-bit precision. You can also use 32 for float32. The convention is to check the error immediately. if err != nil { return err }. This boilerplate is verbose, but it makes the failure path visible. You can't accidentally ignore a parse error.
package main
import (
"fmt"
"strconv"
)
// ParseDuration reads a string like "1.5" and returns milliseconds.
// It returns an error if the string is not a valid number.
func ParseDuration(s string) (int, error) {
// Parse the string to float64. The 64 specifies precision.
val, err := strconv.ParseFloat(s, 64)
if err != nil {
// Return early on parse failure.
return 0, err
}
// Convert to milliseconds and round.
return int(val * 1000), nil
}
func main() {
ms, err := ParseDuration("1.5")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(ms) // prints: 1500
}
Handle the parse error before you touch the conversion.
Special values and overflow
Floating point numbers can represent special values: NaN (Not a Number) and Inf (Infinity). Converting these to integers is undefined behavior in the Go specification. In practice, the runtime usually wraps the result or produces zero. This is a bug waiting to happen. If your calculation produces NaN due to 0/0, and you cast it to int, you get a silent corruption.
Always check math.IsNaN and math.IsInf before converting untrusted floats. The compiler won't help you here. The types match, so the code compiles. The bug hides until the data flows downstream.
Overflow is another risk. If you convert a float larger than math.MaxInt, the result wraps. On a 64-bit system, math.MaxInt is 9223372036854775807. If you convert 1e20, you get a negative number. The compiler won't warn you. You have to validate the range.
package main
import (
"fmt"
"math"
)
func safeConvert(f float64) (int, bool) {
// Check for special values.
if math.IsNaN(f) || math.IsInf(f, 0) {
return 0, false
}
// Check range. math.MaxInt is the largest int value.
if f > float64(math.MaxInt) || f < float64(math.MinInt) {
return 0, false
}
return int(f), true
}
func main() {
val, ok := safeConvert(1e20)
if !ok {
fmt.Println("Value out of range")
} else {
fmt.Println(val)
}
}
Validate the range. The type system checks the shape, not the value.
When to use which conversion
Use int(float) when you want truncation and the value fits in the integer range. Use int(math.Round(float)) when you need the nearest integer value. Use int(math.Floor(float)) when you need to round down, especially for negative numbers where truncation moves toward zero. Use int64(float) when the value might exceed 32 bits or you are interfacing with external systems. Use a validation check before conversion when the input comes from users or network requests. Use strconv.ParseFloat followed by a cast when you start with a string.
Pick the conversion that matches the math, not the habit.