Go Data Types Explained: int, float64, string, bool, and More
You are building a leaderboard service for a multiplayer game. You store player scores in a variable, serialize the data to JSON, and send it to the client. It works perfectly on your laptop. You deploy to a server running a different architecture, and suddenly scores above two billion wrap around to negative numbers. Or the client crashes because it expected a 64-bit integer and received a 32-bit value.
Go's basic types look simple at first glance. You have int, string, bool, and float64. They feel like the variables you used in Python or JavaScript. But Go is a compiled systems language. Every type carries specific guarantees about memory layout, performance, and portability. The compiler enforces these rules strictly. Understanding the differences between int and int64, or why strings cannot be mutated, prevents subtle bugs that only appear in production.
The building blocks of Go values
Go provides a small, fixed set of basic types. These types are the atoms of the language. Every complex structure you build eventually reduces to these primitives. The language does not have a generic "number" type. It distinguishes between integers, floating-point numbers, text, and booleans with precise boundaries.
Think of int as a standard coffee cup at a local shop. It is big enough for most people, and it is the default choice. However, the exact capacity depends on the shop. In one city, the cup holds 32 ounces. In another, it holds 64 ounces. Go's int type works the same way. Its size matches the natural word size of the target machine. On a 64-bit system, int is 64 bits. On a 32-bit system, int is 32 bits. This design gives you the fastest arithmetic performance for loop counters and local calculations, but it means int is not portable across architectures.
Fixed-size integers like int32 and int64 are like metric bolts. A 10mm bolt is always 10mm, regardless of where you buy it. Use these types when data crosses boundaries: network protocols, database schemas, or file formats.
Strings in Go are immutable sequences of bytes. Imagine a printed label on a shipping box. Once the label is printed, you cannot change the ink. If you need to modify the text, you must print a new label and stick it over the old one. This immutability allows Go to share string data safely between goroutines without locks. It also means string concatenation creates new allocations.
Booleans are straightforward. They hold true or false. Go does not allow implicit conversion between booleans and integers. You cannot treat 1 as true. This forces explicit logic and eliminates a whole class of bugs where a number is accidentally used as a flag.
Minimal example: declaration and inference
Go supports two ways to declare variables. You can specify the type explicitly, or you can let the compiler infer the type from the value. The compiler always knows the exact type of every variable at compile time.
package main
import "fmt"
func main() {
// Explicit declaration. The variable gets the zero value of its type.
// For int, the zero value is 0.
var count int
// Short declaration with type inference.
// Go infers int from the literal 42.
score := 42
// String inference. Go infers string from the quoted text.
name := "Alice"
// Boolean inference.
active := true
// Fixed-size integer for portable data.
// Use int64 when the value must be 64 bits on all platforms.
var id int64 = 9876543210
// Float64 is the default floating point type.
// Go infers float64 from the decimal literal.
rate := 0.99
fmt.Printf("Count: %d, Score: %d, Name: %s\n", count, score, name)
fmt.Printf("Active: %v, ID: %d, Rate: %.2f\n", active, id, rate)
}
The code above demonstrates the zero value behavior. When you declare var count int, the variable is automatically initialized to 0. You do not need to assign a value manually. This applies to all basic types: string becomes "", bool becomes false, and float64 becomes 0.0. The compiler guarantees that variables are never uninitialized.
What happens under the hood
When the compiler processes your code, it assigns memory sizes based on the target architecture and the type rules.
An int occupies either 4 bytes or 8 bytes depending on the build target. The compiler chooses the size that matches the CPU's register width. This ensures arithmetic operations map directly to machine instructions without extra conversion overhead.
A string is not just a pointer to a character array. It is a header containing two fields: a pointer to the underlying byte slice and a length. This structure allows Go to represent empty strings efficiently without a null pointer. The length is stored in the header, so calling len(s) is a fast operation that reads the length field. It does not scan the string.
A bool occupies 1 byte. The compiler does not pack booleans into bits. Each boolean variable gets its own byte for simplicity and alignment.
Floating-point numbers follow the IEEE 754 standard. float64 uses 64 bits to store a value with high precision. The representation includes a sign bit, an exponent, and a mantissa. This format allows a huge range of values but introduces rounding errors for decimal fractions that cannot be represented exactly in binary.
Realistic example: handling data with precision
In real applications, you often parse external data, perform calculations, and return results. This example shows how to use types correctly in a struct, handle string immutability, and respect float precision limits.
package main
import (
"encoding/json"
"fmt"
"math"
)
// Player represents a user profile with fixed-width types for serialization.
// Using int64 for Score ensures the value is consistent across 32-bit and 64-bit systems.
type Player struct {
Name string `json:"name"`
Score int64 `json:"score"`
Active bool `json:"active"`
Rate float64 `json:"rate"`
}
// UpdateName creates a new string with a suffix.
// Strings are immutable, so this function returns a new string rather than modifying the input.
func UpdateName(name string, suffix string) string {
// Concatenation allocates a new string with the combined length.
return name + suffix
}
func processPlayer(data []byte) {
var p Player
// json.Unmarshal maps JSON values to Go types.
// The struct tags guide the mapping.
err := json.Unmarshal(data, &p)
if err != nil {
// Return early on error. Go convention: check errors immediately.
fmt.Println("Failed to parse player:", err)
return
}
// Modify the name by creating a new string.
p.Name = UpdateName(p.Name, " (Verified)")
// Check float precision.
// Direct equality checks on floats can fail due to rounding.
// Use a tolerance or math.IsNaN for special values.
if math.IsNaN(p.Rate) {
fmt.Println("Rate is not a number")
return
}
fmt.Printf("Player %s has score %d and rate %.4f\n", p.Name, p.Score, p.Rate)
}
func main() {
// Sample JSON payload.
// Score is a large number that requires int64.
jsonData := []byte(`{"name": "Bob", "score": 9223372036854775807, "active": true, "rate": 0.9999}`)
processPlayer(jsonData)
}
The struct uses int64 for the score. This choice guarantees that the score can hold values up to 9 quintillion on any platform. If the code used int, the score might overflow on a 32-bit system where int maxes out at 2 billion. The JSON tags ensure the serialization library maps the fields correctly.
The UpdateName function demonstrates string immutability. The function cannot modify the name parameter. It returns a new string. This pattern is safe for concurrent code. Multiple goroutines can call UpdateName with the same input string without race conditions.
The float check uses math.IsNaN. Floating-point arithmetic can produce "Not a Number" results from invalid operations like dividing zero by zero. Checking for NaN prevents propagating invalid values through calculations.
Pitfalls and compiler errors
Go's type system catches mistakes early, but some traps require understanding the semantics.
Assigning a fixed-size integer to int triggers a compiler error. The compiler does not perform implicit narrowing or widening conversions between integer types.
var small int32 = 100
var large int = small
The compiler rejects this code with cannot use small (type int32) as type int in assignment. You must convert explicitly using int(small). This rule prevents accidental data loss when converting between sizes.
Strings are indexed by bytes, not characters. If you index a string containing multi-byte UTF-8 characters, you get the byte value, not the Unicode code point.
s := "café"
// The character 'é' is two bytes in UTF-8.
// Indexing s[3] returns the first byte of 'é', which is not a valid character on its own.
b := s[3]
fmt.Printf("%d\n", b) // Prints 195, the byte value
Attempting to modify a string causes a compile error.
s := "hello"
s[0] = 'H'
The compiler complains with cannot assign to s[0]. To change a string, convert it to a byte slice, modify the slice, and convert back. This conversion allocates a new copy of the data.
Floating-point equality checks are unreliable.
a := 0.1 + 0.2
b := 0.3
fmt.Println(a == b) // Prints false
The result is false because 0.1 and 0.2 cannot be represented exactly in binary floating-point. The sum has a tiny rounding error. Use a tolerance check for comparisons, or use integer arithmetic for values like currency.
Decision: choosing the right type
Select types based on portability requirements, performance needs, and data semantics.
Use int for loop counters, slice indices, and local calculations. It matches the platform's word size for optimal performance.
Use int64 when you need a fixed width for serialization, database IDs, or values that might exceed 2 billion on 32-bit systems.
Use int32 for network protocols or file formats that specify 32-bit integers.
Use uint only when you need unsigned arithmetic and the platform word size is sufficient. Unsigned integers are rare in Go code.
Use string for text data. Remember it is immutable. Build strings once using strings.Builder if you need to concatenate many parts.
Use bool for flags and conditions. Write if active instead of if active == true. The idiomatic style is shorter and clearer.
Use float64 for scientific calculations, statistics, and graphics. Avoid it for money or exact decimal arithmetic.
Use byte as an alias for uint8 when handling raw bytes. Use rune as an alias for int32 when iterating over Unicode code points.
Trust int for local math. Lock down int64 for data that leaves the process. Strings are cheap to read, expensive to change. Floats are approximations, not exact values. The compiler is your type guardian. Read the error message; it tells you exactly what went wrong.