Understanding Types
Go is a statically typed language, which means every variable has a type known at compile time. This helps catch errors early and makes programs more reliable.
What is a Type?
A type defines:
- What values a variable can hold
- How much memory it uses
- What operations you can perform on it
Bits and Bytes
Bits
A bit is the smallest unit of data in computing - it’s either 0 or 1.
Bytes
A byte consists of 8 bits. It’s the basic unit used to measure memory and represent data.
// 1 byte = 8 bits
// Can represent values from 0-255 (unsigned)
// Or -128 to 127 (signed)
Understanding bytes helps you choose appropriate types and optimize memory usage.
Predeclared Numeric Types
Go provides several built-in numeric types with different sizes and ranges.
Integer Types
import (
"fmt"
"math"
"unsafe"
)
func main() {
// Signed integers (can be positive or negative)
fmt.Println("int8 :", math.MinInt8, math.MaxInt8) // -128 to 127
fmt.Println("int16 :", math.MinInt16, math.MaxInt16) // -32768 to 32767
fmt.Println("int32 :", math.MinInt32, math.MaxInt32) // -2147483648 to 2147483647
fmt.Println("int64 :", math.MinInt64, math.MaxInt64) // very large range
// Unsigned integers (only positive)
fmt.Println("uint8 :", 0, math.MaxUint8) // 0 to 255
fmt.Println("uint16 :", 0, math.MaxUint16) // 0 to 65535
fmt.Println("uint32 :", 0, math.MaxUint32) // 0 to 4294967295
fmt.Println("uint64 :", 0, uint64(math.MaxUint64))
}
var x int // 32 or 64 bits depending on platform
var y uint // 32 or 64 bits depending on platform
int and uint are the most commonly used integer types. Their size matches your platform architecture (32-bit or 64-bit).
When in doubt, use int for integers. It’s the most idiomatic choice for integer values in Go.
Floating-Point Types
fmt.Println("float32:", math.SmallestNonzeroFloat32, math.MaxFloat32)
fmt.Println("float64:", math.SmallestNonzeroFloat64, math.MaxFloat64)
float32: 32-bit floating-point number
float64: 64-bit floating-point number (default for decimal literals)
Use float64 by default for floating-point numbers. It provides better precision and is the default type for decimal literals.
Memory Costs
Different types use different amounts of memory:
import "unsafe"
fmt.Println("int8 :", unsafe.Sizeof(int8(1)), "bytes") // 1 byte
fmt.Println("int16 :", unsafe.Sizeof(int16(1)), "bytes") // 2 bytes
fmt.Println("int32 :", unsafe.Sizeof(int32(1)), "bytes") // 4 bytes
fmt.Println("int64 :", unsafe.Sizeof(int64(1)), "bytes") // 8 bytes
fmt.Println("float32:", unsafe.Sizeof(float32(1)), "bytes") // 4 bytes
fmt.Println("float64:", unsafe.Sizeof(float64(1)), "bytes") // 8 bytes
Choose smaller types only when memory is a concern and you know the range of values.
Type Safety and Overflow
Go is strongly typed - you cannot mix types without explicit conversion.
Overflow Example
var (
width uint8 = 255 // Maximum value for uint8
height = 255 // int type
)
width++ // Wraps around to 0!
if int(width) < height {
fmt.Println("height is greater") // This will print!
}
fmt.Printf("width: %d height: %d\n", width, height)
// Output: width: 0 height: 255
Integer overflow doesn’t cause errors in Go - it wraps around! A uint8 can only hold 0-255. Adding 1 to 255 results in 0, not an error.
Preventing Overflow
Choose types that can handle your expected range of values:
// Bad - will overflow
var smallNum uint8 = 200
smallNum += 100 // Wraps to 44
// Good - has room to grow
var largeNum int = 200
largeNum += 100 // Correctly becomes 300
Type Conversion
Go requires explicit type conversion - there are no automatic conversions between different types.
Basic Conversion
var i int = 42
var f float64 = float64(i) // Explicit conversion required
var u uint = uint(f)
// This would be an error:
// var f float64 = i // ✗ Error: cannot use i (type int) as type float64
Converting in Expressions
var (
width uint8 = 255
height int = 255
)
// Must convert to compare
if int(width) < height {
fmt.Println("height is greater")
}
Identify type mismatch
Compiler will tell you when types don’t match
Choose target type
Decide which type you need for the operation
Use Type(value) syntax
Explicitly convert: int(x), float64(y), etc.
Defined Types
You can create your own types based on existing types. This adds meaning and type safety to your code.
Creating Defined Types
type (
gram float64 // gram is a new type with underlying type float64
ounce float64 // ounce is a new type with underlying type float64
)
// Equivalent to:
// type gram float64
// type ounce float64
Using Defined Types
func main() {
var g gram = 1000
var o ounce
// Type error: cannot directly assign different types
// o = g * 0.035274 // ✗ Error: cannot use gram as ounce
// Must convert between defined types
o = ounce(g) * 0.035274 // ✓ Correct
fmt.Printf("%g grams is %.2f ounce\n", g, o)
// Output: 1000 grams is 35.27 ounce
}
Why Use Defined Types?
- Type Safety: Prevents mixing incompatible values
- Self-Documenting: Code is clearer about what values represent
- Method Attachment: You can add methods to your types (covered later)
// Clear and type-safe
type userID int
type productID int
var user userID = 100
var product productID = 200
// This would be an error:
// user = product // ✗ Error: cannot assign productID to userID
// Must explicitly convert (usually indicates a mistake):
user = userID(product) // Compiles but probably wrong
Defined types have the same underlying representation and memory layout as their base type, but Go treats them as completely different types.
Underlying Types
Every type has an underlying type:
type gram float64
// gram's underlying type is float64
// float64's underlying type is float64 (predeclared types are their own underlying type)
Types are convertible if they have the same underlying type:
type gram float64
type ounce float64
var g gram = 1000
var o ounce = ounce(g) // ✓ OK: both have underlying type float64
// Can also convert to/from underlying type
var f float64 = float64(g) // ✓ OK
g = gram(f) // ✓ OK
Type Aliases
Type aliases create an alternate name for an existing type:
type MyInt = int // Alias (note the = sign)
var x int = 10
var y MyInt = x // ✓ OK: MyInt and int are the same type
// Compare to defined type:
type MyDefinedInt int // Defined type (no = sign)
var z MyDefinedInt = x // ✗ Error: different types
Type aliases (type A = B) are rarely needed. They’re mainly used for gradual code migration. Prefer defined types (type A B) for creating new types.
Best Practices
Choosing Types
- Use
int for integer values - It’s the idiomatic default
- Use
float64 for floating-point - It’s the default and has better precision
- Use smaller types only when necessary - When memory really matters
- Consider overflow - Choose types with appropriate ranges
Type Safety
- Embrace explicit conversion - It prevents bugs and makes code clear
- Create defined types for domain concepts -
userID, distance, duration
- Don’t mix incompatible types - If conversion feels wrong, it probably is
- Use type aliases sparingly - Mainly for gradual refactoring
Code Organization
// Define domain-specific types at package level
type (
userID int
distance float64
duration int64
)
func getUser(id userID) { /* ... */ }
func travel(d distance) { /* ... */ }
Practical Examples
Distance Units
type (
meter float64
kilometer float64
)
func main() {
var m meter = 1500
var km kilometer = kilometer(m) / 1000
fmt.Printf("%.0f meters is %.1f kilometers\n", m, km)
// Output: 1500 meters is 1.5 kilometers
}
Temperature Conversion
type (
celsius float64
fahrenheit float64
)
func toFahrenheit(c celsius) fahrenheit {
return fahrenheit(c * 9.0 / 5.0 + 32.0)
}
func main() {
temp := celsius(25.0)
fmt.Printf("%.1f°C is %.1f°F\n", temp, toFahrenheit(temp))
// Output: 25.0°C is 77.0°F
}
ID Types for Safety
type (
userID int
productID int
orderID int
)
func getUser(id userID) { /* ... */ }
func getProduct(id productID) { /* ... */ }
func main() {
user := userID(12345)
product := productID(67890)
getUser(user) // ✓ Correct
// getUser(product) // ✗ Error: type mismatch catches bug!
}
Common Pitfalls
Integer Division
// Watch out for integer division
var x int = 5
var y int = 2
fmt.Println(x / y) // 2, not 2.5!
// Fix: convert to float
fmt.Println(float64(x) / float64(y)) // 2.5
Overflow Wrapping
var tiny uint8 = 255
tiny++
fmt.Println(tiny) // 0 (wrapped around)
tiny = 0
tiny--
fmt.Println(tiny) // 255 (wrapped around)
Forgetting Conversion
var i int = 10
var f float64 = 3.5
// result := i * f // ✗ Error: type mismatch
result := float64(i) * f // ✓ Correct
Go’s type system may seem strict at first, but it catches many bugs at compile time that would otherwise cause runtime errors or silent failures.
The type system is one of Go’s strengths. It helps you write correct, maintainable code by making your intentions explicit and catching type-related errors before your program runs.