Skip to main content

Go Type System: Basics and Type Inference

Published: May 8, 2026 Updated: August 29, 2026 Larry Qu 6 min read

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 conversion
  • type 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 to int, float literals to float64
  • 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 int for general integers, float64 for floating-point; reach for specific sizes only when needed

Resources

Comments

👍 Was this article helpful?