The type keyword gives a type a name. It comes in two forms. type UserID int64 is a type definition, which creates a brand new type with its own identity and its own set of methods. type Handler = http.Handler is an alias declaration, which adds a second name for a type that already exists. Which form you pick decides whether the compiler lets you mix the two names and whether you can add methods (Go spec).
TL;DR
type Name Underlyingdefines a new type.type Name = Otherdeclares an alias. Atype ( ... )block groups several declarations.- A defined type is different from every other type, even one with the same underlying type. A
UserIDcan't be passed where anOrderIDor anint64is expected without an explicit conversion. - Untyped constants and unnamed values like map literals still assign to a defined type without a conversion, so
cancelOrder(42)compiles. - A defined type starts with no methods.
type Timestamp time.TimelosesFormat. Embedtime.Timein a struct if you want to keep them. - You can only declare methods on types from your own package.
func (d time.Duration) ...fails withcannot define new methods on non-local type. - An alias is the same type under another name.
byte,runeandanyare aliases. Aliases can take type parameters since Go 1.24. - Function types like
http.HandlerFunccan have methods, which is how Go turns a plain function into an interface value. - Generic types take type parameters, as in
Page[T any]. Since Go 1.27, methods can declare their own type parameters too.
How do you declare a type in Go?
Write type, a name and the type it is based on. Several declarations can share one type ( ... ) block, the same way const and var blocks work:
example.gogopackage main import ( "fmt" "net/http" "time" ) type ( UserID int64 OrderID int64 Order struct { ID OrderID UserID UserID CreatedAt time.Time } OrderStore interface { Get(id OrderID) (Order, error) } Handler = http.Handler ) func main() { o := Order{ID: 1001, UserID: 42} fmt.Println(o.ID) }
This prints 1001. The block holds four type definitions and one alias. struct and interface are the most common right-hand sides, and each has its own keyword, covered in Go keywords: all 25 explained. Structs like Order have their own guide in the Go struct keyword. Interfaces like OrderStore have their own guide in the Go interface keyword.
Type names follow the usual visibility rules. Order is exported because it starts with a capital letter, and orderRow would only be visible inside its package. The Go package keyword covers exported names in more detail.
You can also declare a type inside a function. It is only visible in that function, which suits one-off shapes such as a request body:
example.gogofunc main() { type loginRequest struct { Email string `json:"email"` Password string `json:"password"` } var req loginRequest err := json.Unmarshal([]byte(`{"email":"[email protected]","password":"hunter2"}`), &req) fmt.Println(req.Email, err) }
This prints [email protected] <nil>. A local type can't have methods, because method declarations only exist at package level.
What is a defined type in Go?
It's a type declared as type Name Underlying, without the =. The spec calls this a type definition. Every defined type has an underlying type, which decides how its values are stored and which operators work on them. UserID and OrderID above both have the underlying type int64, so you can add, compare and print them like any int64.
What they don't share is identity. A defined type is always different from every other type, so the compiler refuses to mix them:
example.gogopackage main import "fmt" type UserID int64 type OrderID int64 func cancelOrder(id OrderID) { fmt.Println("cancelling order", id) } func main() { var user UserID = 42 cancelOrder(user) }
example.texttext./main.go:14:14: cannot use user (variable of int64 type UserID) as OrderID value in argument to cancelOrder
That error is why ID types are worth defining. A function with the signature refund(userID, orderID int64) accepts its arguments in either order. With UserID and OrderID, swapping them is a compile error instead of a refund sent to the wrong customer.
The same rule separates a defined type from its underlying type. Passing a UserID to a function that takes an int64 fails too:
example.texttext./main.go:13:11: cannot use id (variable of int64 type UserID) as int64 value in argument to loadUser
How do you convert between defined types?
Write the target type like a function call. OrderID(user) and int64(user) both compile, because a conversion is allowed whenever two types have the same underlying type. The conversion has no runtime cost. The value stays the same and only the type the compiler sees changes.
Conversions are explicit on purpose. When you see OrderID(user) in a code review, you know someone decided that a user ID should be treated as an order ID, and you can ask why.
Why can you pass 42 or a map literal without a conversion?
Neither of them has a defined type yet. Assignability has a few rules beyond "the types are identical", and two of them come up constantly (Go spec).
The first is about untyped constants. 42 is an untyped constant, and it can become any type whose underlying type can represent it, so cancelOrder(42) compiles and prints cancelling order 42. It is the same rule that lets you write 5 * time.Second, since time.Duration is itself a defined type with the underlying type int64. The Go const keyword covers untyped constants in depth.
The second is about unnamed types. A value can be assigned to a defined type when both have identical underlying types and at least one of the two isn't a named type. map[string]string is a type literal with no name, so a function that takes Headers accepts it directly:
example.gogotype Headers map[string]string func send(h Headers) { fmt.Println(len(h), h["Content-Type"]) } func main() { send(map[string]string{"Content-Type": "application/json"}) raw := map[string]string{"Accept": "text/plain"} send(raw) }
Both calls compile. The rule works the same way for []byte, func(...) and struct literals. It never lets you go straight from one named type to another, like UserID to OrderID or UserID to int64.
Why doesn't a defined type inherit methods?
A defined type gets the underlying type's structure but none of its methods. That surprises people the first time they wrap time.Time:
example.gogopackage main import ( "fmt" "time" ) type Timestamp time.Time func main() { created := Timestamp(time.Now()) fmt.Println(created.Format(time.RFC3339)) }
example.texttext./main.go:12:22: created.Format undefined (type Timestamp has no field or method Format)
The method set starts empty so the new type can behave differently from the old one. http.Header is a map[string][]string with methods like Get and Set that canonicalize header names, and sort.StringSlice is a []string with the methods sort.Interface needs. If defined types copied every method from their underlying type, Timestamp would pick up methods you never chose to give it.
You have two options when you want the old methods back. The first is to convert to the original type where you need them. A String method written this way controls how the value prints:
example.gogotype Timestamp time.Time func (t Timestamp) String() string { return time.Time(t).Format(time.RFC3339) }
The second is embedding. A struct that embeds time.Time promotes all of its methods, and you add your own next to them:
example.gogopackage main import ( "fmt" "time" ) type Timestamp struct { time.Time } func (t Timestamp) ISO() string { return t.UTC().Format(time.RFC3339) } func main() { created := Timestamp{time.Date(2026, 9, 25, 14, 30, 0, 0, time.UTC)} fmt.Println(created.Format("2006-01-02")) fmt.Println(created.ISO()) }
This prints 2026-09-25 and then 2026-09-25T14:30:00Z. Embedding keeps Format, Before, Unix and every other time.Time method. It also means Timestamp satisfies every interface time.Time satisfies, including json.Marshaler, so it encodes to JSON as an RFC 3339 string instead of an object.
Can you add methods to a built-in or imported type?
No. A method must be declared in the same package as its receiver's type. Adding a method to time.Duration fails:
example.gogofunc (d time.Duration) Minutes10() float64 { return d.Minutes() / 10 }
example.texttext./main.go:5:9: cannot define new methods on non-local type time.Duration
The same happens with int, string and every other predeclared type, and with an alias to a type from another package. The rule keeps two packages from attaching conflicting methods to the same type. When you need extra behavior, define your own type, as in type Cents int64, and put the methods on that.
What is a type alias in Go?
An alias is a second name for a type that already exists. It is written with =:
example.gogotype Handler = http.Handler
Handler and http.Handler are identical types. You can pass one where the other is expected, the compiler never asks for a conversion and %T prints the original name. An alias also doesn't create a new method set. Declaring a method on an alias is the same as declaring it on the original type, so the "non-local type" rule applies to it as well.
Go uses aliases in its own predeclared names. byte is an alias for uint8, rune for int32 and any for interface{}. That is why a []byte goes into a function that takes a []uint8 without a conversion.
Aliases were added in Go 1.9 mainly so a type could move between packages without breaking callers. When a large codebase moves Order from billing to orders, the old package keeps type Order = orders.Order for a while. Code that still imports billing compiles, and the callers move over one at a time (Go blog).
Can a type alias be generic?
Yes, since Go 1.24. An alias can declare type parameters, and it must be instantiated where it is used (Go 1.24 release notes):
example.gogotype Set[K comparable] = map[K]struct{} func main() { allowed := Set[string]{"GET": {}, "HEAD": {}} var plain map[string]struct{} = allowed _, ok := plain["GET"] fmt.Println(ok, len(allowed)) fmt.Printf("%T\n", allowed) }
This prints true 2 and then map[string]struct {}. Set[string] is still just a name for map[string]struct{}, so the assignment to plain needs no conversion.
Defined type vs type alias: what's the difference?
Defined type (type UserID int64) | Alias (type Handler = http.Handler) | |
|---|---|---|
| New type? | Yes, different from every other type | No, identical to the original |
| Mixing with the original | Needs an explicit conversion | Works directly |
| Methods of the original | Not inherited | The same methods, since it is the same type |
| Can declare new methods | Yes, in the same package | Only if the original is a defined type in your package |
%T prints | main.UserID | The original name |
| Typical use | IDs, units, enums, domain types | Moving types between packages, shorter names |
Most of the time you want a defined type. Reach for an alias when two names have to mean exactly the same type, usually during a refactor.
How do function types work in Go?
A function signature is a type like any other, so you can name it with type. Once it has a name, it can have methods. The standard library uses this to turn a plain function into an interface value. http.Handler is an interface with one method, ServeHTTP, and a function isn't a Handler on its own:
example.gogofunc health(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "ok") } var h http.Handler = health
example.texttext./main.go:13:23: cannot use health (value of type func(w http.ResponseWriter, r *http.Request)) as http.Handler value in variable declaration: func(w http.ResponseWriter, r *http.Request) does not implement http.Handler (missing method ServeHTTP)
net/http declares type HandlerFunc func(ResponseWriter, *Request) and gives it a ServeHTTP method that calls the function itself. Converting with http.HandlerFunc(health) produces a value that satisfies the interface. http.HandleFunc and mux.HandleFunc do that conversion for you (net/http docs). The Go func keyword covers function values, closures and method values in depth.
The same pattern works for your own policies. A retry policy is a function from the attempt number to a delay, and a method on the function type can wrap it:
example.gogopackage main import ( "fmt" "time" ) type RetryPolicy func(attempt int) time.Duration func (p RetryPolicy) Capped(limit time.Duration) RetryPolicy { return func(attempt int) time.Duration { return min(p(attempt), limit) } } func Exponential(base time.Duration) RetryPolicy { return func(attempt int) time.Duration { return base << attempt } } func main() { policy := Exponential(100 * time.Millisecond).Capped(time.Second) for attempt := range 5 { fmt.Println(attempt, policy(attempt)) } }
example.texttext0 100ms 1 200ms 2 400ms 3 800ms 4 1s
The name tells readers what the function is for, and calling Capped looks like calling a method on any other value. An HTTP client can take a RetryPolicy field, and tests can pass a policy that always returns 0.
How do generic types work in Go?
A type declaration can have type parameters in square brackets after the name. A paginated API response is a common case, since the paging fields are the same for every kind of item:
example.gogopackage main import ( "fmt" "strconv" ) type Page[T any] struct { Items []T NextCursor string } func (p Page[T]) HasMore() bool { return p.NextCursor != "" } func (p Page[T]) Map[U any](fn func(T) U) Page[U] { out := Page[U]{NextCursor: p.NextCursor} for _, item := range p.Items { out.Items = append(out.Items, fn(item)) } return out } type Order struct { ID int64 Total int64 } func main() { orders := Page[Order]{ Items: []Order{{ID: 1, Total: 4999}, {ID: 2, Total: 1250}}, NextCursor: "b3JkZXI6Mg", } ids := orders.Map(func(o Order) string { return strconv.FormatInt(o.ID, 10) }) fmt.Println(ids.Items, ids.HasMore()) }
This prints [1 2] true. Page[Order] and Page[string] are two different types built from one declaration. Methods repeat the type parameter in the receiver, as in func (p Page[T]).
Map declares its own type parameter U. That is a generic method, and it is new in Go 1.27 (Go 1.27 release notes). With go 1.26 or earlier in go.mod, the same code fails with:
example.texttext./main.go:17:22: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)
Interface methods still can't have type parameters. Map[U any](fn func(string) U) []U inside an interface fails with interface method must have no type parameters, so a generic method never counts toward satisfying an interface.
Where else does type appear in Go?
What does .(type) do in a type switch?
x.(type) is only valid in a switch, and it branches on the dynamic type of an interface value. A common use is errors:
example.gogofunc describe(err error) string { switch e := err.(type) { case nil: return "ok" case *fs.PathError: return "file error on " + e.Path case net.Error: return fmt.Sprintf("network error, timeout=%v", e.Timeout()) default: return "other: " + e.Error() } }
In each case e has that case's type, so e.Path and e.Timeout() compile without a separate type assertion. Calling describe with the error from os.Open("/etc/levelupgo.yaml") prints file error on /etc/levelupgo.yaml when the file doesn't exist.
A type switch only looks at the outermost error. After fmt.Errorf("load config: %w", err), the same call falls through to default. For wrapped errors, use errors.As, which walks the chain:
example.gogovar pathErr *fs.PathError if errors.As(wrapped, &pathErr) { fmt.Println("errors.As found", pathErr.Path) }
How is type used in generic constraints?
A constraint is an interface, usually declared with type, and it can list types as well as methods. The ~ in front of a type means "any type whose underlying type is this one", which is what lets defined types through:
example.gogotype Cents int64 type Amount interface { ~int64 | ~float64 } func Sum[T Amount](values []T) T { var total T for _, v := range values { total += v } return total }
Sum([]Cents{4999, 1250}) returns 6249 as a Cents. Without the ~, Cents is rejected, and the compiler points at the fix:
example.texttext./main.go:20:17: Cents does not satisfy Amount (possibly missing ~ for int64 in Amount)
Go enums are built on defined types too. A type like type OrderStatus int plus a const block with iota gives you a typed list of values. The Go const keyword walks through it, and the Go var keyword covers how variables of these types start at their zero value.
Where LevelUpGo fits
LevelUpGo teaches Go through exercises that run real Go code in the browser. Composite Types builds structs, constructor functions and methods step by step. Interfaces & Polymorphism covers implicit interfaces, type assertions and type switches. Go Generics Masterclass goes through generic types and custom constraints. The Training Ground has short standalone exercises for practicing outside a course. For the other 24 reserved words, see Go keywords: all 25 explained.
FAQ
Is type a keyword in Go?
Yes. type is one of Go's 25 reserved keywords, so you can't use it as a variable or function name. It starts a type declaration, either a type definition like type UserID int64 or an alias like type Handler = http.Handler. It also appears in type switches as x.(type).
What is the difference between a type definition and a type alias in Go?
A type definition, type UserID int64, creates a new type that is different from int64 and starts with no methods. An alias, type ID = int64, is another name for int64 itself. Values of a defined type need a conversion to mix with the original type, while an alias needs none because the two names denote the same type.
Can you add methods to a built-in type in Go?
No. Methods can only be declared on types defined in the same package, so func (n int) IsEven() bool fails with cannot define new methods on non-local type int. Define your own type, such as type Quantity int, and declare the method on it.
Why does Go say "cannot use id (variable of int64 type UserID) as int64 value"?
Because UserID is a defined type, and Go never converts a typed value from one named type to another on its own, even when the underlying type is the same. Only untyped constants like 42 adapt to the type they need. Convert explicitly with int64(id). If you find yourself converting all the time, the function probably should take a UserID instead.
What does .(type) do in Go?
It is the type switch form of a type assertion. switch e := err.(type) checks the dynamic type of an interface value against each case, and inside a case e has that case's type. It only works in a switch. To test for a single type, use v, ok := x.(T), and for wrapped errors use errors.As.
Sources
- The Go Programming Language Specification, Type declarations: https://go.dev/ref/spec#Type_declarations
- The Go Programming Language Specification, Alias declarations: https://go.dev/ref/spec#Alias_declarations
- The Go Programming Language Specification, Assignability: https://go.dev/ref/spec#Assignability
- The Go Programming Language Specification, Method declarations: https://go.dev/ref/spec#Method_declarations
- The Go Programming Language Specification, Type switches: https://go.dev/ref/spec#Type_switches
- Go 1.24 Release Notes, generic type aliases: https://go.dev/doc/go1.24
- Go 1.27 Release Notes: https://go.dev/doc/go1.27
- The Go Blog, What's in an (Alias) Name?: https://go.dev/blog/alias-names
- net/http, HandlerFunc: https://pkg.go.dev/net/http#HandlerFunc
