Type Alias vs Defined Type in Go Compared Side by Side
A defined type like type Celsius float64 is new with its own methods, and an alias like type Temp = float64 is another name. Behavior, uses and a verdict.

Two declarations differ by a single character and mean opposite things. type Celsius float64 makes a new type. type Temp = float64 makes a second name for an existing one. The first is how you build a domain model, the second is a refactoring tool, and mixing them up produces either a type that does not protect you or an error about methods on a type that is not yours. This comparison runs both side by side, shows the compiler's response in each direction, covers the one place aliases are the right answer and gives the rule I follow. It builds on type conversion and structs, and sits in the Go basics path.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
What actually differs
Declare one of each and try to use them:
package main
import "fmt"
type Celsius float64
type Temp = float64
type Set[T comparable] = map[T]struct{}
func main() {
var c Celsius = 20
var t Temp = 20
var f float64 = t
fmt.Println(c, t, f)
fmt.Printf("%T %T %T\n", c, t, f)
s := Set[string]{"a": {}}
fmt.Printf("%T %d\n", s, len(s))
var b byte = 'x'
var u uint8 = b
var r rune = 'y'
var i int32 = r
fmt.Printf("%T %T %T %T\n", b, u, r, i)
}20 20 20
main.Celsius float64 float64
map[string]struct {} 1
uint8 uint8 int32 int32Read the %T line. c is a main.Celsius, a distinct type, while t prints as float64, because the alias is only another spelling and the type is float64. That means var f float64 = t compiles without a conversion: an alias value is a float64. The generic alias Set[string] prints as map[string]struct {}, and the last line confirms that byte and uint8 are one type, as are rune and int32. Those two are aliases the language predeclares, which is why error messages and %T never mention byte or rune.
The two things you cannot do with an alias
Assigning a defined type to its underlying type needs a conversion in both directions, and the compiler refuses a bare assignment:
package main
import "fmt"
type Celsius float64
type Temp = float64
func (c Celsius) String() string { return fmt.Sprintf("%.1f C", float64(c)) }
func (t Temp) Double() Temp { return t * 2 }
func main() {
var c Celsius = 20
var f float64
f = c
fmt.Println(f)
}# example.test/lab
./main.go:10:9: cannot define new methods on non-local type Temp
./main.go:15:6: cannot use c (variable of float64 type Celsius) as float64 value in assignmentThere are two errors. The assignment f = c fails because Celsius and float64 are different types, as the cannot use value as type error explains. That friction is the benefit of a defined type: a function taking a Celsius will not accept a bare float64 from another unit, so you cannot pass a price where a temperature is expected.
The other error is cannot define new methods on non-local type Temp. An alias is the underlying type, float64, and you cannot add methods to a type from the universe or from another package. A defined type, Celsius, is yours and can carry methods, such as the String that formats it. If you want behavior on a basic type, you need a defined type.
What this means in practice
| Question | Defined type type T U | Alias type T = U |
|---|---|---|
| Is it a new type? | Yes | No, the same type |
| Can it have methods? | Yes | No (unless U is local) |
Assignable to and from U | Only with a conversion | Always |
What %T prints | pkg.T | The name of U |
Does reflect see it as different? | Yes | No |
| Typical use | Domain concepts, enums, units | Refactoring, renaming, short names |
The defined type is the default. When I write type UserID int or type Celsius float64, I want the compiler's help in keeping them apart from each other and from raw integers, and I want a place for String, Valid or Parse methods. An alias gives none of that.
The one place aliases are right: moving a type
Aliases were added to Go 1.9 for large refactors. Suppose a type lives in package old and you move it to package new. Every importer of old.User would break. With an alias, the old name continues to work while callers migrate:
package newpkg
type User struct {
Name string
}
func (u User) Greeting() string { return "hello, " + u.Name }package oldpkg
import "example.test/lab/newpkg"
// User is the old name for newpkg.User.
//
// Deprecated: Use newpkg.User.
type User = newpkg.Userpackage main
import (
"fmt"
"example.test/lab/newpkg"
"example.test/lab/oldpkg"
)
func main() {
var u oldpkg.User = newpkg.User{Name: "ada"}
var v newpkg.User = u
fmt.Println(u.Greeting(), v == u)
fmt.Printf("%T\n", u)
}hello, ada true
newpkg.UserAn oldpkg.User and a newpkg.User are interchangeable, because they are the same type, so existing code keeps compiling while the Deprecated: notice nudges people toward the new name. The %T line prints newpkg.User, confirming that nothing of the old name survives at run time. Had oldpkg.User been a defined type, type User newpkg.User, it would have been a different type with none of the methods, and every caller would break. This is the use case the alias was designed for, and the one the Go team describes in the codebase refactoring design.
Defined types from structs and interfaces
The same rule applies when the underlying type is a struct, with one more consequence. A defined type keeps the fields of its underlying struct and none of its methods:
type User struct{ Name string }
func (u User) Greeting() string { return "hello, " + u.Name }
type Admin User // a new type: has Name, has no Greeting
type Member = User // the same type: has Name and GreetingAdmin{Name: "ada"}.Greeting() does not compile, because the method set of Admin is empty, and you convert with User(a) to get the methods back. That is how you strip behavior on purpose, for example to avoid an infinite loop when a type's MarshalJSON method wants to call the default encoder: define type plain User inside the method, convert and encode the plain one, which has the fields and no MarshalJSON. A defined type whose underlying type is an interface keeps the interface's method set, since those methods belong to the interface. An alias adds nothing in either case and also takes nothing away.
Generic aliases and short names
Go 1.24 allowed type aliases to have type parameters, as in the Set[T] alias above, which makes Set[string] a convenient name for map[string]struct{} while remaining that exact type. It is the same pure-naming use: a shorter or clearer spelling of an unwieldy type, with nothing new at run time. In a single package, an alias is also a way to give a long composite type a name, type Handlers = map[string]func(http.ResponseWriter, *http.Request), though a defined type there lets you hang methods on it, which is usually better.
My verdict
Use a defined type whenever the thing is a concept: an ID, a unit, a status, a currency amount, a duration. The conversions you pay for are the point. Use an alias when you are renaming or moving a type and want old and new spellings to coexist, or when a generic type is awkward to spell. If you catch yourself writing type Name = string to give a string a clearer name in signatures, that is a defined-type case that wanted the safety: write type Name string, and accept the conversions at the boundary. The cost of the defined type is a conversion where untyped values arrive, such as Name("ada") for a variable and no change for constants, since an untyped constant adapts. For the language rules, see the specification on type declarations, which covers alias declarations and type definitions together.
When the other choice wins
Defined types lose when you need to interoperate with a library that takes the underlying type everywhere and you convert at every call: a type Seconds int passed to ten functions that expect int adds noise without catching errors. Aliases lose the moment you want a method, or a difference in identity for a switch on types. A third option, embedding the underlying type in a struct, is another way to extend a type you do not own, with the costs described in embedding. Whichever you choose, put the declaration where the reader will find the reason: a comment explaining why a name is an alias saves the next person from "fixing" it.


