Back to Blog

The Go type Keyword: Defined Types, Aliases and Methods

How Go's type keyword works: defined types vs aliases, underlying types and conversions, why methods don't carry over, function types like http.HandlerFunc, generic types and type switches.

The Go type Keyword: Defined Types, Aliases and Methods

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 Underlying defines a new type. type Name = Other declares an alias. A type ( ... ) block groups several declarations.
  • A defined type is different from every other type, even one with the same underlying type. A UserID can't be passed where an OrderID or an int64 is 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.Time loses Format. Embed time.Time in a struct if you want to keep them.
  • You can only declare methods on types from your own package. func (d time.Duration) ... fails with cannot define new methods on non-local type.
  • An alias is the same type under another name. byte, rune and any are aliases. Aliases can take type parameters since Go 1.24.
  • Function types like http.HandlerFunc can 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.gogo
package 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.gogo
func 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.gogo
package 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.gogo
type 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.gogo
package 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.gogo
type 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.gogo
package 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.gogo
func (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.gogo
type 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.gogo
type 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 typeNo, identical to the original
Mixing with the originalNeeds an explicit conversionWorks directly
Methods of the originalNot inheritedThe same methods, since it is the same type
Can declare new methodsYes, in the same packageOnly if the original is a defined type in your package
%T printsmain.UserIDThe original name
Typical useIDs, units, enums, domain typesMoving 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.gogo
func 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.gogo
package 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.texttext
0 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.gogo
package 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.gogo
func 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.gogo
var 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.gogo
type 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

Write Go like a senior engineer

Interactive lessons in your browser. The first ones are free.

Try a free lessonOr create a free account