Defined Types vs. Type Aliases
type Celsius float64 and type Fahrenheit = float64 differ by one equals sign. One makes a new type, the other makes a nickname, and the compiler treats them nothing alike.
One equals sign apart
Without the equals sign, you get a new type that happens to be laid out like a float64.
With it, you get a second name for float64 itself. Nothing new exists.
// A defined type. Celsius is a brand new type.
type Celsius float64
// An alias. Fahrenheit is another name for float64.
type Fahrenheit = float64The compiler treats them differently
An alias is the type it names, so it assigns straight across.
A defined type is its own type. The compiler refuses even though the memory is identical.
var c Celsius = 100
var f Fahrenheit = 212
var plain float64
plain = f // fine: Fahrenheit is float64
plain = c // compile errorThe error names the underlying type, which is the giveaway: Go knows Celsius is a float64 underneath, and still will not do it for you.
./main.go:8:10: cannot use c (variable of float64 type Celsius)
as float64 value in assignmentAn explicit conversion is the fix. It costs nothing at runtime. It is there so the swap is visible in the code.
plain = float64(c) // explicit conversion, no data changesOnly a defined type can have methods
A defined type is yours, so you can attach methods to it.
func (c Celsius) String() string {
return fmt.Sprintf("%.1f°C", float64(c))
}
fmt.Println(c) // 100.0°CAn alias is not yours. Fahrenheit is float64, and float64 belongs to the language, so there is nothing local to attach a method to.
This is the practical reason most new types are defined types rather than aliases.
// Trying the same on the alias:
func (f Fahrenheit) String() string { return "" }
./main.go:5:9: cannot define new methods on non-local type FahrenheitThey print differently
%T asks the runtime what a value is. It reports Celsius by name, and reports the alias as plain float64, because that is what it is.
fmt.Printf("%T\n", c) // main.Celsius
fmt.Printf("%T\n", f) // float64A type switch can only see one of them
A type switch matches on real types, so a defined type gets its own case.
switch v := x.(type) {
case Celsius:
// only Celsius lands here
case float64:
// Fahrenheit lands here, because it is float64
}An alias cannot have its own case. Writing both is the same case twice, and the compiler says so.
// Listing both is a duplicate:
case float64:
case Fahrenheit:
./main.go:9:7: duplicate case Fahrenheit in type switch
./main.go:7:7: previous caseWhen to reach for each
Reach for a defined type when the distinction matters. Two IDs that are both int64 are easy to swap by accident, and a defined type turns that into a compile error.
type UserID int64
type OrderID int64
func lookup(id UserID) {}
var o OrderID = 7
lookup(o) // compile error, which is the pointReach for an alias when two names must mean the same type, which in practice means moving a type between packages without breaking callers.
This is why byte and rune exist. They are aliases for uint8 and int32, there to say which meaning is intended.
// Moving Config from oldpkg to newpkg without breaking callers.
package oldpkg
type Config = newpkg.Config
// oldpkg.Config and newpkg.Config are the same type, so existing
// code keeps compiling while callers migrate one at a time.Side by side
| Defined type | Alias | |
|---|---|---|
| Syntax | type T U | type T = U |
| Is it a new type | Yes | No, same type as U |
Assign to a U | Needs a conversion | Direct |
| Can define methods | Yes | Only if U is local to the package |
What %T prints | The new name | The original name |
| Own type switch case | Yes | No, duplicates U's case |
| Reach for it when | The distinction should be enforced | Two names must mean one type |