Go is statically typed — every value has a type that’s known at compile time. This prevents a whole class of bugs at the cost of being explicit about types. But Go’s type inference means you rarely have to write the type out: := infers it from the right-hand side.
Understanding when types are inferred vs explicit, and the difference between a new named type and a type alias, is the foundation for writing idiomatic Go.
Basic Types
Go’s numeric types are precisely sized. The most commonly used:
| Type | Size | Range/Precision |
|---|---|---|
int |
Platform-size (64-bit on 64-bit OS) | Default integer |
int8 / int16 / int32 / int64 |
8/16/32/64-bit | Fixed-size integers |
uint / uint8 / uint16 / uint32 / uint64 |
Unsigned variants | 0 to 2^N-1 |
float64 |
64-bit IEEE 754 | Default float |
float32 |
32-bit IEEE 754 | Less precise, less memory |
bool |
1 byte | true or false |
string |
Variable | Immutable UTF-8 bytes |
byte |
Alias for uint8 |
Single byte |
rune |
Alias for int32 |
Unicode code point |
Use int for general-purpose integers — the compiler optimizes it for the platform. Use fixed-size types (int32, uint64) only when you need to match a protocol, file format, or external API with specific bit widths.
Use float64 by default for floating-point — float32 has less precision and rarely saves meaningful memory unless you have millions of values.
Type Inference with :=
The short variable declaration := infers the type from the right-hand side:
x := 42 // int (integer literals default to int)
y := 3.14 // float64 (float literals default to float64)
s := "hello" // string
b := true // bool
c := 1 + 2i // complex128
fmt.Printf("%T %T %T %T\n", x, y, s, b)
// int float64 string bool
When the inferred type isn’t what you need — say, you want float32 or int64 — either declare with var or convert explicitly:
// Explicit type with var
var f float32 = 3.14
var n int64 = 1_000_000_000
// Conversion in short declaration
g := float32(3.14)
m := int64(100)
Numeric literals in Go are “untyped constants” — they adapt to whatever type is expected. var x int32 = 42 works because 42 has no committed type yet. But once assigned, the variable has a fixed type.
Named Types: Creating Domain-Specific Types
A named type declaration creates a new distinct type from an existing one. The new type has the same underlying representation but is a separate type — assignment between them requires explicit conversion:
type UserID int
type OrderID int
var uid UserID = 42
var oid OrderID = 42
// uid = oid // compile error: cannot use OrderID as UserID
uid = UserID(oid) // explicit conversion is allowed
This matters for domain modeling. If you use plain int for both user IDs and order IDs, the compiler can’t catch when you accidentally pass one where the other is expected. With named types, it can.
Named types can also have methods, which plain int cannot:
type Status int
const (
StatusPending Status = iota
StatusActive
StatusClosed
)
func (s Status) String() string {
switch s {
case StatusPending: return "pending"
case StatusActive: return "active"
case StatusClosed: return "closed"
default: return fmt.Sprintf("Status(%d)", int(s))
}
}
s := StatusActive
fmt.Println(s) // active — uses String() method
Type Aliases: Just Another Name
A type alias (type A = B) creates an alternative name for the same type. A and B are completely interchangeable — there’s no conversion needed:
type Celsius = float64
type Score = int
var temp Celsius = 37.5
var f float64 = temp // no conversion needed — same type
// Practical use: shorten long type names
type Handler = func(http.ResponseWriter, *http.Request)
Aliases are useful for gradual refactoring (introduce a new name for a type while migrating code) and for shorter names in packages. They don’t add type safety.
The key distinction:
type T int— new type, method-capable, requires explicit conversiontype T = int— alias, same type, no conversion needed
Type Conversion
Go has no implicit numeric conversion. Assignment between numeric types always requires an explicit conversion, even between “compatible” types:
var i int = 42
var f float64 = float64(i) // must be explicit
var u uint = uint(f)
// String conversions use strconv
n, err := strconv.Atoi("123") // string → int
s := strconv.Itoa(456) // int → string
f2, err := strconv.ParseFloat("3.14", 64)
// Byte/rune conversions
b := []byte("hello") // string → []byte (copies)
s2 := string(b) // []byte → string (copies)
r := []rune("hello") // string → []rune (copies, decodes UTF-8)
Converting between numeric types can lose information silently — a float to int truncates, a large int64 to int8 wraps. Always consider the range. The race detector (go test -race) won’t catch numeric overflow; it’s your responsibility.
Zero Values
Every variable in Go has a zero value when declared without initialization. This eliminates undefined behavior from uninitialized variables:
var i int // 0
var f float64 // 0.0
var s string // ""
var b bool // false
var p *int // nil
var sl []int // nil (but len(sl)==0 is safe)
var m map[string]int // nil (reading is safe, writing panics)
var fn func() // nil
The zero value is “ready to use” for most types. A sync.Mutex zero value is an unlocked mutex. A bytes.Buffer zero value is a ready-to-use empty buffer. Design your own types so their zero value is meaningful where possible.
Type Checking at Runtime
%T prints the type name. reflect.TypeOf returns it as a value:
x := 42
fmt.Printf("%T\n", x) // int
fmt.Println(reflect.TypeOf(x)) // int
type MyInt int
var m MyInt = 42
fmt.Printf("%T\n", m) // main.MyInt — includes package name
For interface values, type assertions and type switches check the concrete type at runtime — see Go type assertions and switches.
Choosing the Right Numeric Type
In practice: use int for counters, sizes, indices. Use int64 when values might exceed 32-bit range (timestamps, file sizes, large IDs). Use float64 for math. Use float32 only when memory matters and precision loss is acceptable. Use uint rarely — it’s mainly useful for bit manipulation.
Avoid mixing types in arithmetic — Go requires explicit conversion at every step, which quickly becomes unwieldy. Design functions to work with one numeric type and convert at the boundary.
Summary
:=infers types from right-hand side; integer literals default toint, float literals tofloat64- Named types (
type T int) create distinct types requiring explicit conversion — use for domain modeling (UserID, Status, Duration) - Type aliases (
type T = int) are just alternative names — same type, no conversion needed - Go has no implicit numeric conversion — every conversion must be explicit
- Zero values are always defined:
0,"",false,nil— no undefined behavior - Use
intfor general integers,float64for floating-point; reach for specific sizes only when needed
Comments