Type Definitions and Aliases

type Celsius float64 creates a new, distinct type the compiler keeps apart; type X = Y creates only a second name for one type.

Syntaxtype Name Underlying (new distinct type) | type Name = Underlying (alias, same type)

You can give a type a name of your own. There are two forms and they do different things.

A definition makes a new type

type Celsius float64 is a type definition. The specification says it creates a new, distinct type with the same underlying type and the same operations as the one it was built from. So a Celsius can be added, compared and printed exactly like a float64. But it is a different type from float64, and a different type from type Fahrenheit float64. Assigning one to the other is a compile error until you write a conversion, as Fahrenheit(x). That refusal is the point. The compiler will not let you add a temperature in Celsius to one in Fahrenheit, or pass a user id where an order id belongs. You can also attach your own functions, called methods, to a type you define; the methods lessons cover that.

An alias makes a second name

type Temp = float64, with an equals sign, is an alias declaration. It creates no new type. Temp and float64 are the same type under two names, so no conversion is ever needed and %T prints float64. Go has had aliases since 1.9, and since 1.24 an alias may take type parameters. Two aliases are built into the language: byte for uint8 and rune for int32. any, added in Go 1.18, is an alias for interface{}.

Which to use

Define a type when you want the compiler to keep two things apart. Use an alias only to move or rename a type without breaking code that still uses the old name.

Example

Example · go
package main

import "fmt"

// A definition makes a brand new type that behaves like a float64.
type Celsius float64
type Fahrenheit float64

// An alias with = makes a second name. Temp and float64 are one type.
type Temp = float64

func main() {
	var c Celsius = 100

	// Celsius and Fahrenheit are different types, so a conversion is needed.
	// The sum is worked out on plain float64 values, then converted back.
	f := Fahrenheit(float64(c)*9/5 + 32)
	fmt.Println(c, f)

	// This would not compile, because there is no conversion:
	// var wrong Fahrenheit = c

	// An alias needs no conversion. There is no second type to cross.
	var t Temp = 36.6
	var plain float64 = t
	fmt.Println(t, plain)

	// %T prints the type. By then the alias has gone.
	fmt.Printf("%T %T %T\n", c, f, t)
}

// Output:
// 100 212
// 36.6 36.6
// main.Celsius main.Fahrenheit float64

When to use it

  • Stop a UserID being passed where an OrderID belongs, at compile time.
  • Keep temperatures, distances or currencies in units the compiler will not mix.
  • Rename or move a type and leave an alias behind so existing callers still build.
  • Name a small fixed set of log levels or statuses so a bare int cannot be used.

More examples

Two ids that cannot be swapped by accident

Both are text underneath, and both support everything a string supports, but the compiler will not let one stand in for the other. That catches the argument-order mistake that is otherwise found in production. Writing the conversion is still allowed, and that is deliberate: it marks the one place where you crossed from one meaning to another.

Example · go
package main

import "fmt"

// Two defined types over string. The compiler keeps them apart.
type UserID string
type OrderID string

func main() {
	var u UserID = "u-1"
	var o OrderID = "o-9"
	fmt.Println(u, o)

	// o = u would not compile. A conversion says you meant it.
	o = OrderID(u)
	fmt.Println(o)

	// A defined type keeps its underlying type's operations, so + and len work.
	u = u + "-eu"
	fmt.Println(u, len(u))
}

// Output:
// u-1 o-9
// u-1
// u-1-eu 6

An alias carries an old name through a rename

When a type is renamed or moved to another package, an alias keeps every existing use compiling while callers are updated one at a time, with no conversions anywhere. This is what aliases were added for in Go 1.9. The same example shows the built-in aliases: a byte and a uint8 are interchangeable because they are literally one type.

Example · go
package main

import "fmt"

// Seconds is the name this package uses now.
type Seconds float64

// Secs was the old name. The alias keeps old code compiling meanwhile.
type Secs = Seconds

func main() {
	var s Seconds = 1.5
	var old Secs = s // no conversion: one type, two names
	fmt.Println(old)

	var b byte = 'A'
	var u uint8 = b // no conversion either: byte IS uint8
	fmt.Printf("%T %T %d\n", b, u, u)
}

// Output:
// 1.5
// uint8 uint8 65

A defined type as a small fixed set of values

Giving the constants their own type means a bare int cannot be passed where a Level is wanted, so a caller cannot invent a fourth level by mistake. Level is still an int underneath, so comparison and arithmetic work on it. In real code the three values are usually written once with iota rather than numbered by hand.

Example · go
package main

import "fmt"

// A defined type plus constants of that type gives a small fixed set.
type Level int

const (
	Debug Level = 0
	Info  Level = 1
	Error Level = 2
)

func main() {
	l := Info
	fmt.Println(l, l+1 == Error)

	// Level is an int underneath, so %d and int(l) both work.
	fmt.Printf("%T %d\n", l, int(l))

	// A plain int needs converting before it can be a Level.
	n := 2
	fmt.Println(Level(n) == Error)
}

// Output:
// 1 true
// main.Level 1
// true

Discussion

  • Be the first to comment on this lesson.