Custom types¶
Go lets you define your own named types. There are two declaration forms — they look almost identical but mean completely different things.
type Celsius float64 // type definition — a new, distinct type
type Temp = float64 // type alias — just another name for float64
The single = is the entire difference. The consequences are big.
Why bother creating custom types?¶
Three reasons, in order of importance:
- The compiler stops you from mixing things that shouldn't mix. A
Celsiusvalue cannot be added to aFahrenheitvalue without you explicitly saying so. - You can attach methods. Only defined types (not aliases) can have methods — covered in a later topic, but it's the main reason to reach for
type Foo Bar. - Names communicate intent.
UserIDis more self-documenting than yet anotherint64in a function signature.
Type definition: type Foo Bar¶
From the Go spec:
"A type definition creates a new, distinct type with the same underlying type and operations as the given type [...]. It is different from any other type, including the type it is created from."
type Celsius float64
type Fahrenheit float64
var c Celsius = 20
var f Fahrenheit = 68
sum := c + f // compile error: mismatched types Celsius and Fahrenheit
Celsius and Fahrenheit are different types. Both happen to be laid out in memory exactly like a float64 — that shared layout is their underlying type — but the compiler treats them as distinct because they have different names.
You can convert between them with the usual T(x) syntax (covered in 03-type-conversions.md):
Common patterns¶
Semantic wrappers around primitives:
type UserID int64
type Email string
type Money int64 // store cents to avoid float
func sendEmail(to Email, user UserID) error { /* ... */ }
var id UserID = 42
var addr Email = "ada@example.com"
sendEmail(addr, id) // ok — argument order matches types
sendEmail(id, addr) // compile error: order is wrong, compiler catches it
This is a free safety net. Without UserID as its own type, both args would be int64/string and the wrong order would compile silently.
Named function types:
type Handler func(req string) string
func register(h Handler) { /* ... */ }
register(func(req string) string {
return "echo: " + req
})
Named slice or map types:
Enum-like constants — typically combined with iota:
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
func isWeekend(d Weekday) bool {
return d == Sunday || d == Saturday
}
isWeekend(Monday) // ok — type-checked
isWeekend(5) // ok — 5 is an untyped int constant, becomes Weekday
isWeekend(int(5)) // compile error — int is not Weekday
Type alias: type Foo = Bar¶
From the spec:
"An alias declaration binds an identifier to the given type. Within the scope of the identifier, it serves as an alias for the given type."
An alias is not a new type — it's a second name for the same type.
type Celsius = float64 // alias
var c Celsius = 20
var f float64 = c // ok — c IS a float64
var sum = c + f // ok — same type, no conversion needed
Compare to the definition form above: with type Celsius float64 the compiler would reject var f float64 = c. With the alias, it accepts it.
When to use aliases (rarely)¶
The vast majority of code wants type definitions, not aliases. Aliases exist mainly for:
- Gradual refactoring across packages — temporarily expose
oldpkg.Fooasnewpkg.Foowhile callers migrate. - Shortening long type names —
type list = []*linkedListNode[T]for readability inside one file. - The standard library uses them sparingly —
byte = uint8andrune = int32are aliases. That's whybyteanduint8are interchangeable everywhere.
var b byte = 65
var u uint8 = b // ok — alias means same type
var c uint32 = uint32(b) // ok — explicit conversion needed (different types)
Key restriction: you cannot attach methods to an alias. The alias has no separate identity to attach them to.
If you want methods, use a type definition.
Methods — the headline feature of type definitions¶
A quick preview (methods get their own topic later). You attach methods using a receiver:
type Celsius float64
// Method with a value receiver — operates on a copy.
func (c Celsius) Fahrenheit() float64 {
return float64(c)*9/5 + 32
}
func main() {
var c Celsius = 100
fmt.Println(c.Fahrenheit()) // 212
}
Two rules to know now:
- The receiver type must be a defined type (or pointer to one). You cannot write
func (f float64) ...— you'd have to definetype MyFloat float64first. - The receiver type must be defined in the same package as the method. You cannot add a method to
time.Durationfrom your own package. Workaround: define your own type withtime.Durationas its underlying type, attach methods there, convert when crossing boundaries.
"Underlying type" — the precise rule¶
From the spec, paraphrased into a process:
- Predeclared types (
int,string,bool,float64, ...) — their underlying type is themselves. - Type literals (
[]int,map[string]int,struct{...},func(...) ...) — their underlying type is themselves. - Any other type — follow the chain of definitions until you reach one of the above.
type A = string // underlying type: string
type B string // underlying type: string (alias chain: B → string)
type C B // underlying type: string (chain: C → B → string)
type D struct { // underlying type: struct{Name string} (a type literal)
Name string
}
You can check at runtime with reflect.TypeOf(x).Kind() — it returns the underlying kind, not the named type.
Type identity in one paragraph¶
Two types are identical in Go's eyes when:
- They are the same defined type (same name, same package), OR
- They are both type literals with structurally matching underlying types.
A defined type is never identical to its underlying type or to any other defined type, even if the layouts match. That's the entire reason Celsius and Fahrenheit don't mix.
Quick reference¶
| Form | New type? | Methods? | Mixes with original? |
|---|---|---|---|
type Foo Bar (definition) |
yes | yes | no — needs explicit conversion |
type Foo = Bar (alias) |
no | no | yes — same type |
Sources¶
- Type declarations — go.dev/ref/spec#Type_declarations
- Type definitions — go.dev/ref/spec#Type_definitions
- Alias declarations — go.dev/ref/spec#Alias_declarations
- Underlying types — go.dev/ref/spec#Types
- Type identity — go.dev/ref/spec#Type_identity
- Method declarations — go.dev/ref/spec#Method_declarations