La palabra clave type le da un nombre a un tipo. Tiene dos formas. type UserID int64 es una definición de tipo, que crea un tipo completamente nuevo con su propia identidad y su propio conjunto de métodos. type Handler = http.Handler es una declaración de alias, que añade un segundo nombre a un tipo que ya existe. La forma que elijas decide si el compilador te deja mezclar los dos nombres y si puedes añadir métodos (especificación de Go).
Resumen rápido
type Name Underlyingdefine un tipo nuevo.type Name = Otherdeclara un alias. Un bloquetype ( ... )agrupa varias declaraciones.- Un tipo definido es distinto de cualquier otro tipo, incluso de uno con el mismo tipo subyacente. Un
UserIDno se puede pasar donde se espera unOrderIDo unint64sin una conversión explícita. - Las constantes sin tipo y los valores sin nombre, como los literales de map, se siguen asignando a un tipo definido sin conversión, así que
cancelOrder(42)compila. - Un tipo definido empieza sin métodos.
type Timestamp time.TimepierdeFormat. Incrustatime.Timeen un struct si quieres conservarlos. - Solo puedes declarar métodos sobre tipos de tu propio paquete.
func (d time.Duration) ...falla concannot define new methods on non-local type. - Un alias es el mismo tipo con otro nombre.
byte,runeyanyson alias. Desde Go 1.24, los alias pueden tener parámetros de tipo. - Los tipos función como
http.HandlerFuncpueden tener métodos, y así es como Go convierte una función normal en un valor de interfaz. - Los tipos genéricos reciben parámetros de tipo, como en
Page[T any]. Desde Go 1.27, los métodos también pueden declarar sus propios parámetros de tipo.
¿Cómo se declara un tipo en Go?
Escribe type, un nombre y el tipo en el que se basa. Varias declaraciones pueden compartir un mismo bloque type ( ... ), igual que los bloques const y var:
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) }
Esto imprime 1001. El bloque contiene cuatro definiciones de tipo y un alias. struct e interface son lo más habitual a la derecha, y cada una tiene su propia palabra clave, que se explica en Palabras clave de Go: las 25 explicadas. Los structs como Order tienen su propia guía en la palabra clave struct en Go. Las interfaces como OrderStore tienen su propia guía en la palabra clave interface en Go.
Los nombres de tipo siguen las reglas de visibilidad de siempre. Order se exporta porque empieza por mayúscula, y orderRow solo sería visible dentro de su paquete. La palabra clave package en Go explica los nombres exportados con más detalle.
También puedes declarar un tipo dentro de una función. Solo es visible en esa función, lo que encaja con estructuras de un solo uso, como el cuerpo de una petición:
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) }
Esto imprime [email protected] <nil>. Un tipo local no puede tener métodos, porque las declaraciones de métodos solo existen a nivel de paquete.
¿Qué es un tipo definido en Go?
Es un tipo declarado como type Name Underlying, sin el =. La especificación lo llama definición de tipo. Todo tipo definido tiene un tipo subyacente, que decide cómo se almacenan sus valores y qué operadores funcionan con ellos. UserID y OrderID de arriba tienen los dos el tipo subyacente int64, así que puedes sumarlos, compararlos e imprimirlos como cualquier int64.
Lo que no comparten es la identidad. Un tipo definido siempre es distinto de cualquier otro tipo, así que el compilador se niega a mezclarlos:
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
Ese error es el motivo por el que vale la pena definir tipos para los IDs. Una función con la firma refund(userID, orderID int64) acepta sus argumentos en cualquier orden. Con UserID y OrderID, intercambiarlos es un error de compilación y no un reembolso enviado al cliente equivocado.
La misma regla separa un tipo definido de su tipo subyacente. Pasar un UserID a una función que recibe un int64 también falla:
example.texttext./main.go:13:11: cannot use id (variable of int64 type UserID) as int64 value in argument to loadUser
¿Cómo se convierte entre tipos definidos?
Escribe el tipo de destino como si fuera una llamada a función. OrderID(user) e int64(user) compilan los dos, porque una conversión está permitida siempre que dos tipos tengan el mismo tipo subyacente. La conversión no tiene coste en tiempo de ejecución. El valor sigue siendo el mismo y solo cambia el tipo que ve el compilador.
Las conversiones son explícitas a propósito. Cuando ves OrderID(user) en una revisión de código, sabes que alguien decidió que un ID de usuario debía tratarse como un ID de pedido, y puedes preguntar por qué.
¿Por qué puedes pasar 42 o un literal de map sin conversión?
Porque ninguno de los dos tiene todavía un tipo definido. La asignabilidad tiene algunas reglas más allá de «los tipos son idénticos», y dos de ellas aparecen constantemente (especificación de Go).
La primera trata de las constantes sin tipo. 42 es una constante sin tipo, y puede convertirse en cualquier tipo cuyo tipo subyacente pueda representarla, así que cancelOrder(42) compila e imprime cancelling order 42. Es la misma regla que te permite escribir 5 * time.Second, ya que time.Duration es a su vez un tipo definido con el tipo subyacente int64. La palabra clave const en Go explica a fondo las constantes sin tipo.
La segunda trata de los tipos sin nombre. Un valor se puede asignar a un tipo definido cuando los dos tienen tipos subyacentes idénticos y al menos uno de ellos no es un tipo con nombre. map[string]string es un literal de tipo sin nombre, así que una función que recibe Headers lo acepta directamente:
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) }
Las dos llamadas compilan. La regla funciona igual con []byte, func(...) y los literales de struct. Nunca te deja pasar directamente de un tipo con nombre a otro, como de UserID a OrderID o de UserID a int64.
¿Por qué un tipo definido no hereda los métodos?
Un tipo definido recibe la estructura del tipo subyacente, pero ninguno de sus métodos. Eso sorprende a mucha gente la primera vez que envuelve 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)
El conjunto de métodos empieza vacío para que el tipo nuevo pueda comportarse de forma distinta al antiguo. http.Header es un map[string][]string con métodos como Get y Set que normalizan los nombres de las cabeceras, y sort.StringSlice es un []string con los métodos que necesita sort.Interface. Si los tipos definidos copiaran todos los métodos de su tipo subyacente, Timestamp acabaría con métodos que nunca decidiste darle.
Tienes dos opciones cuando quieres recuperar los métodos antiguos. La primera es convertir al tipo original donde los necesites. Un método String escrito así controla cómo se imprime el valor:
example.gogotype Timestamp time.Time func (t Timestamp) String() string { return time.Time(t).Format(time.RFC3339) }
La segunda es la incrustación. Un struct que incrusta time.Time promueve todos sus métodos, y tú añades los tuyos junto a ellos:
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()) }
Esto imprime 2026-09-25 y después 2026-09-25T14:30:00Z. La incrustación conserva Format, Before, Unix y todos los demás métodos de time.Time. También significa que Timestamp satisface todas las interfaces que satisface time.Time, incluida json.Marshaler, así que se codifica en JSON como una cadena RFC 3339 y no como un objeto.
¿Se pueden añadir métodos a un tipo integrado o importado?
No. Un método tiene que declararse en el mismo paquete que el tipo de su receptor. Añadir un método a time.Duration falla:
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
Lo mismo ocurre con int, string y cualquier otro tipo predeclarado, y con un alias de un tipo de otro paquete. La regla evita que dos paquetes añadan métodos contradictorios al mismo tipo. Cuando necesites comportamiento adicional, define tu propio tipo, como type Cents int64, y pon los métodos en él.
¿Qué es un alias de tipo en Go?
Un alias es un segundo nombre para un tipo que ya existe. Se escribe con =:
example.gogotype Handler = http.Handler
Handler y http.Handler son tipos idénticos. Puedes pasar uno donde se espera el otro, el compilador nunca pide una conversión y %T imprime el nombre original. Un alias tampoco crea un conjunto de métodos nuevo. Declarar un método sobre un alias es lo mismo que declararlo sobre el tipo original, así que la regla de los «non-local type» también se le aplica.
Go usa alias en sus propios nombres predeclarados. byte es un alias de uint8, rune de int32 y any de interface{}. Por eso un []byte entra en una función que recibe un []uint8 sin conversión.
Los alias se añadieron en Go 1.9 sobre todo para que un tipo pudiera moverse entre paquetes sin romper el código que lo usa. Cuando un código base grande mueve Order de billing a orders, el paquete antiguo mantiene type Order = orders.Order durante un tiempo. El código que todavía importa billing compila, y los llamadores se migran de uno en uno (blog de Go).
¿Puede ser genérico un alias de tipo?
Sí, desde Go 1.24. Un alias puede declarar parámetros de tipo, y hay que instanciarlo donde se usa (notas de la versión Go 1.24):
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) }
Esto imprime true 2 y después map[string]struct {}. Set[string] sigue siendo solo un nombre para map[string]struct{}, así que la asignación a plain no necesita conversión.
Tipo definido frente a alias de tipo: ¿qué diferencia hay?
Tipo definido (type UserID int64) | Alias (type Handler = http.Handler) | |
|---|---|---|
| ¿Tipo nuevo? | Sí, distinto de cualquier otro tipo | No, idéntico al original |
| Mezclarlo con el original | Necesita una conversión explícita | Funciona directamente |
| Métodos del original | No se heredan | Los mismos métodos, porque es el mismo tipo |
| Puede declarar métodos nuevos | Sí, en el mismo paquete | Solo si el original es un tipo definido en tu paquete |
%T imprime | main.UserID | El nombre original |
| Uso típico | IDs, unidades, enums, tipos de dominio | Mover tipos entre paquetes, nombres más cortos |
La mayoría de las veces lo que quieres es un tipo definido. Recurre a un alias cuando dos nombres tengan que significar exactamente el mismo tipo, normalmente durante una refactorización.
¿Cómo funcionan los tipos función en Go?
La firma de una función es un tipo como cualquier otro, así que puedes ponerle nombre con type. Una vez que tiene nombre, puede tener métodos. La biblioteca estándar lo usa para convertir una función normal en un valor de interfaz. http.Handler es una interfaz con un solo método, ServeHTTP, y una función por sí sola no es un Handler:
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 declara type HandlerFunc func(ResponseWriter, *Request) y le da un método ServeHTTP que llama a la propia función. Convertir con http.HandlerFunc(health) produce un valor que satisface la interfaz. http.HandleFunc y mux.HandleFunc hacen esa conversión por ti (documentación de net/http). La palabra clave func en Go explica en detalle los valores de función, los closures y los method values.
El mismo patrón sirve para tus propias políticas. Una política de reintentos es una función que va del número de intento a una espera, y un método sobre el tipo función puede envolverla:
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
El nombre le dice a quien lee para qué sirve la función, y llamar a Capped se parece a llamar a un método sobre cualquier otro valor. Un cliente HTTP puede tener un campo RetryPolicy, y los tests pueden pasar una política que siempre devuelva 0.
¿Cómo funcionan los tipos genéricos en Go?
Una declaración de tipo puede tener parámetros de tipo entre corchetes después del nombre. Una respuesta paginada de una API es un caso habitual, porque los campos de paginación son los mismos para cualquier tipo de elemento:
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()) }
Esto imprime [1 2] true. Page[Order] y Page[string] son dos tipos distintos construidos a partir de una sola declaración. Los métodos repiten el parámetro de tipo en el receptor, como en func (p Page[T]).
Map declara su propio parámetro de tipo U. Eso es un método genérico, y es una novedad de Go 1.27 (notas de la versión Go 1.27). Con go 1.26 o anterior en go.mod, el mismo código falla con:
example.texttext./main.go:17:22: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)
Los métodos de interfaz siguen sin poder tener parámetros de tipo. Map[U any](fn func(string) U) []U dentro de una interfaz falla con interface method must have no type parameters, así que un método genérico nunca cuenta para satisfacer una interfaz.
¿Dónde más aparece type en Go?
¿Qué hace .(type) en un switch de tipos?
x.(type) solo es válido dentro de un switch, y ramifica según el tipo dinámico de un valor de interfaz. Un uso habitual son los errores:
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() } }
En cada caso, e tiene el tipo de ese caso, así que e.Path y e.Timeout() compilan sin una aserción de tipo aparte. Llamar a describe con el error de os.Open("/etc/levelupgo.yaml") imprime file error on /etc/levelupgo.yaml cuando el archivo no existe.
Un switch de tipos solo mira el error más externo. Después de fmt.Errorf("load config: %w", err), la misma llamada cae en default. Para errores envueltos, usa errors.As, que recorre la cadena:
example.gogovar pathErr *fs.PathError if errors.As(wrapped, &pathErr) { fmt.Println("errors.As found", pathErr.Path) }
¿Cómo se usa type en las restricciones genéricas?
Una restricción es una interfaz, normalmente declarada con type, y puede listar tipos además de métodos. El ~ delante de un tipo significa «cualquier tipo cuyo tipo subyacente sea este», y es lo que deja pasar a los tipos definidos:
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}) devuelve 6249 como un Cents. Sin el ~, Cents se rechaza, y el compilador te señala la solución:
example.texttext./main.go:20:17: Cents does not satisfy Amount (possibly missing ~ for int64 in Amount)
Los enums de Go también se construyen sobre tipos definidos. Un tipo como type OrderStatus int junto con un bloque const con iota te da una lista de valores con tipo. La palabra clave const en Go lo explica paso a paso, y la palabra clave var en Go explica cómo las variables de estos tipos empiezan con su valor cero.
Dónde encaja LevelUpGo
LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Composite Types construye structs, funciones constructoras y métodos paso a paso. Interfaces & Polymorphism trata las interfaces implícitas, las aserciones de tipo y los switches de tipos. Go Generics Masterclass recorre los tipos genéricos y las restricciones propias. El Training Ground tiene ejercicios cortos e independientes para practicar fuera de un curso. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.
Preguntas frecuentes
¿Es type una palabra clave en Go?
Sí. type es una de las 25 palabras clave reservadas de Go, así que no puedes usarla como nombre de variable ni de función. Inicia una declaración de tipo, ya sea una definición de tipo como type UserID int64 o un alias como type Handler = http.Handler. También aparece en los switches de tipos como x.(type).
¿Qué diferencia hay entre una definición de tipo y un alias de tipo en Go?
Una definición de tipo, type UserID int64, crea un tipo nuevo que es distinto de int64 y empieza sin métodos. Un alias, type ID = int64, es otro nombre para el propio int64. Los valores de un tipo definido necesitan una conversión para mezclarse con el tipo original, mientras que un alias no necesita ninguna, porque los dos nombres denotan el mismo tipo.
¿Se pueden añadir métodos a un tipo integrado en Go?
No. Los métodos solo se pueden declarar sobre tipos definidos en el mismo paquete, así que func (n int) IsEven() bool falla con cannot define new methods on non-local type int. Define tu propio tipo, como type Quantity int, y declara el método sobre él.
¿Por qué Go dice "cannot use id (variable of int64 type UserID) as int64 value"?
Porque UserID es un tipo definido, y Go nunca convierte por su cuenta un valor con tipo de un tipo con nombre a otro, aunque el tipo subyacente sea el mismo. Solo las constantes sin tipo como 42 se adaptan al tipo que necesitan. Convierte de forma explícita con int64(id). Si te encuentras convirtiendo todo el rato, probablemente la función debería recibir un UserID.
¿Qué hace .(type) en Go?
Es la forma de switch de tipos de una aserción de tipo. switch e := err.(type) compara el tipo dinámico de un valor de interfaz con cada case, y dentro de un caso e tiene el tipo de ese caso. Solo funciona dentro de un switch. Para comprobar un solo tipo, usa v, ok := x.(T), y para errores envueltos usa errors.As.
Fuentes
- 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
- Notas de la versión Go 1.24, alias de tipo genéricos: https://go.dev/doc/go1.24
- Notas de la versión Go 1.27: 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
