Voltar ao blog

A palavra-chave type em Go: tipos definidos, aliases e métodos

Como funciona a palavra-chave type do Go: tipos definidos vs aliases, tipos subjacentes e conversões, porque é que os métodos não passam para o novo tipo, tipos função como http.HandlerFunc, tipos genéricos e type switches.

A palavra-chave type em Go: tipos definidos, aliases e métodos

A palavra-chave type dá um nome a um tipo. Existe em duas formas. type UserID int64 é uma definição de tipo, que cria um tipo completamente novo, com identidade própria e o seu próprio conjunto de métodos. type Handler = http.Handler é uma declaração de alias, que acrescenta um segundo nome a um tipo que já existe. A forma que escolhes decide se o compilador te deixa misturar os dois nomes e se podes acrescentar métodos (especificação do Go).

Resumo rápido

  • type Name Underlying define um tipo novo. type Name = Other declara um alias. Um bloco type ( ... ) agrupa várias declarações.
  • Um tipo definido é diferente de qualquer outro tipo, mesmo de um com o mesmo tipo subjacente. Não podes passar um UserID onde se espera um OrderID ou um int64 sem uma conversão explícita.
  • As constantes sem tipo e os valores sem nome, como os literais de map, continuam a poder ser atribuídos a um tipo definido sem conversão, por isso cancelOrder(42) compila.
  • Um tipo definido começa sem métodos. type Timestamp time.Time perde o Format. Incorpora o time.Time numa struct se os quiseres manter.
  • Só podes declarar métodos em tipos do teu próprio pacote. func (d time.Duration) ... falha com cannot define new methods on non-local type.
  • Um alias é o mesmo tipo com outro nome. byte, rune e any são aliases. Desde o Go 1.24, os aliases podem ter parâmetros de tipo.
  • Os tipos função como http.HandlerFunc podem ter métodos, e é assim que o Go transforma uma função simples num valor de interface.
  • Os tipos genéricos recebem parâmetros de tipo, como em Page[T any]. Desde o Go 1.27, os métodos também podem declarar os seus próprios parâmetros de tipo.

Como se declara um tipo em Go?

Escreve type, um nome e o tipo em que se baseia. Várias declarações podem partilhar um bloco type ( ... ), tal como acontece com os blocos const e var:

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)
}

Isto imprime 1001. O bloco contém quatro definições de tipo e um alias. struct e interface são o que mais aparece do lado direito, e cada uma tem a sua própria palavra-chave, explicada em Palavras-chave do Go: as 25 explicadas. As structs como Order têm o seu próprio guia em a palavra-chave struct em Go. As interfaces como OrderStore têm o seu próprio guia em a palavra-chave interface em Go.

Os nomes de tipos seguem as regras habituais de visibilidade. Order é exportado porque começa com maiúscula, e orderRow só seria visível dentro do seu pacote. A palavra-chave package em Go explica os nomes exportados com mais detalhe.

Também podes declarar um tipo dentro de uma função. Só é visível nessa função, o que dá jeito para estruturas usadas uma única vez, como o corpo de um pedido:

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)
}

Isto imprime [email protected] <nil>. Um tipo local não pode ter métodos, porque as declarações de métodos só existem ao nível do pacote.

O que é um tipo definido em Go?

É um tipo declarado como type Name Underlying, sem o =. A especificação chama-lhe definição de tipo. Todos os tipos definidos têm um tipo subjacente, que decide como os seus valores são guardados e que operadores funcionam com eles. Os tipos UserID e OrderID acima têm ambos o tipo subjacente int64, por isso podes somá-los, compará-los e imprimi-los como qualquer int64.

O que não partilham é a identidade. Um tipo definido é sempre diferente de qualquer outro tipo, por isso o compilador recusa-se a misturá-los:

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

É por causa deste erro que vale a pena definir tipos para os IDs. Uma função com a assinatura refund(userID, orderID int64) aceita os argumentos por qualquer ordem. Com UserID e OrderID, trocá-los dá um erro de compilação em vez de um reembolso enviado ao cliente errado.

A mesma regra separa um tipo definido do seu tipo subjacente. Passar um UserID a uma função que recebe um int64 também falha:

example.texttext
./main.go:13:11: cannot use id (variable of int64 type UserID) as int64 value in argument to loadUser

Como se converte entre tipos definidos?

Escreve o tipo de destino como se fosse uma chamada de função. OrderID(user) e int64(user) compilam ambos, porque a conversão é permitida sempre que dois tipos têm o mesmo tipo subjacente. A conversão não tem custo em tempo de execução. O valor fica igual e só muda o tipo que o compilador vê.

As conversões são explícitas de propósito. Quando vês OrderID(user) numa revisão de código, sabes que alguém decidiu que um ID de utilizador deve ser tratado como um ID de encomenda, e podes perguntar porquê.

Porque é que podes passar 42 ou um literal de map sem conversão?

Nenhum deles tem ainda um tipo definido. A atribuibilidade tem algumas regras além de «os tipos são idênticos», e duas delas aparecem constantemente (especificação do Go).

A primeira tem a ver com constantes sem tipo. 42 é uma constante sem tipo e pode passar a ser qualquer tipo cujo tipo subjacente a consiga representar, por isso cancelOrder(42) compila e imprime cancelling order 42. É a mesma regra que te deixa escrever 5 * time.Second, já que o próprio time.Duration é um tipo definido com o tipo subjacente int64. A palavra-chave const em Go aprofunda as constantes sem tipo.

A segunda tem a ver com tipos sem nome. Um valor pode ser atribuído a um tipo definido quando ambos têm tipos subjacentes idênticos e pelo menos um dos dois não é um tipo com nome. map[string]string é um literal de tipo sem nome, por isso uma função que recebe Headers aceita-o diretamente:

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)
}

As duas chamadas compilam. A regra funciona da mesma forma para []byte, func(...) e literais de struct. Nunca te deixa passar diretamente de um tipo com nome para outro, como de UserID para OrderID ou de UserID para int64.

Porque é que um tipo definido não herda métodos?

Um tipo definido recebe a estrutura do tipo subjacente, mas nenhum dos seus métodos. Isto apanha as pessoas de surpresa da primeira vez que embrulham um 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)

O conjunto de métodos começa vazio para que o tipo novo se possa comportar de forma diferente do antigo. O http.Header é um map[string][]string com métodos como Get e Set que normalizam os nomes dos cabeçalhos, e o sort.StringSlice é um []string com os métodos de que sort.Interface precisa. Se os tipos definidos copiassem todos os métodos do tipo subjacente, o Timestamp ficaria com métodos que nunca decidiste dar-lhe.

Tens duas opções quando queres os métodos antigos de volta. A primeira é converter para o tipo original onde precisas deles. Um método String escrito desta forma controla como o valor é impresso:

example.gogo
type Timestamp time.Time

func (t Timestamp) String() string {
	return time.Time(t).Format(time.RFC3339)
}

A segunda é a incorporação (embedding). Uma struct que incorpora time.Time promove todos os seus métodos, e acrescentas os teus ao lado deles:

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())
}

Isto imprime 2026-09-25 e depois 2026-09-25T14:30:00Z. A incorporação mantém o Format, o Before, o Unix e todos os outros métodos de time.Time. Também faz com que o Timestamp satisfaça todas as interfaces que o time.Time satisfaz, incluindo json.Marshaler, por isso é codificado em JSON como uma string RFC 3339 e não como um objeto.

Podes acrescentar métodos a um tipo built-in ou importado?

Não. Um método tem de ser declarado no mesmo pacote que o tipo do seu recetor. Acrescentar um método a time.Duration falha:

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

O mesmo acontece com int, string e todos os outros tipos pré-declarados, e com um alias para um tipo de outro pacote. A regra impede que dois pacotes acrescentem métodos em conflito ao mesmo tipo. Quando precisas de comportamento extra, define o teu próprio tipo, como em type Cents int64, e põe os métodos nesse tipo.

O que é um alias de tipo em Go?

Um alias é um segundo nome para um tipo que já existe. Escreve-se com =:

example.gogo
type Handler = http.Handler

Handler e http.Handler são tipos idênticos. Podes passar um onde se espera o outro, o compilador nunca pede uma conversão e o %T imprime o nome original. Um alias também não cria um novo conjunto de métodos. Declarar um método num alias é o mesmo que declará-lo no tipo original, por isso a regra do «non-local type» também se aplica.

O Go usa aliases nos seus próprios nomes pré-declarados. byte é um alias de uint8, rune de int32 e any de interface{}. É por isso que um []byte entra numa função que recebe um []uint8 sem conversão.

Os aliases foram acrescentados no Go 1.9 sobretudo para que um tipo pudesse mudar de pacote sem partir o código de quem o usa. Quando uma codebase grande move Order de billing para orders, o pacote antigo mantém type Order = orders.Order durante algum tempo. O código que ainda importa billing continua a compilar, e quem o usa vai mudando um de cada vez (blog do Go).

Um alias de tipo pode ser genérico?

Sim, desde o Go 1.24. Um alias pode declarar parâmetros de tipo, e tem de ser instanciado onde é usado (notas de lançamento do Go 1.24):

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)
}

Isto imprime true 2 e depois map[string]struct {}. Set[string] continua a ser só um nome para map[string]struct{}, por isso a atribuição a plain não precisa de conversão.

Tipo definido vs alias de tipo: qual é a diferença?

Tipo definido (type UserID int64)Alias (type Handler = http.Handler)
Tipo novo?Sim, diferente de qualquer outro tipoNão, idêntico ao original
Misturar com o originalPrecisa de uma conversão explícitaFunciona diretamente
Métodos do originalNão são herdadosOs mesmos métodos, já que é o mesmo tipo
Pode declarar métodos novosSim, no mesmo pacoteSó se o original for um tipo definido no teu pacote
O %T imprimemain.UserIDO nome original
Uso típicoIDs, unidades, enums, tipos de domínioMover tipos entre pacotes, nomes mais curtos

Na maior parte dos casos, queres um tipo definido. Recorre a um alias quando dois nomes têm de significar exatamente o mesmo tipo, normalmente durante um refactoring.

Como funcionam os tipos função em Go?

Uma assinatura de função é um tipo como qualquer outro, por isso podes dar-lhe um nome com type. Depois de ter nome, pode ter métodos. A biblioteca padrão usa isto para transformar uma função simples num valor de interface. O http.Handler é uma interface com um único método, ServeHTTP, e uma função sozinha não é um Handler:

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)

O net/http declara type HandlerFunc func(ResponseWriter, *Request) e dá-lhe um método ServeHTTP que chama a própria função. Converter com http.HandlerFunc(health) produz um valor que satisfaz a interface. O http.HandleFunc e o mux.HandleFunc fazem essa conversão por ti (documentação do net/http). A palavra-chave func em Go aprofunda os valores de função, as closures e os method values.

O mesmo padrão funciona para as tuas próprias políticas. Uma política de retry é uma função que recebe o número da tentativa e devolve um atraso, e um método no tipo função pode envolvê-la:

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

O nome diz a quem lê para que serve a função, e chamar Capped parece uma chamada de método sobre qualquer outro valor. Um cliente HTTP pode ter um campo RetryPolicy, e os testes podem passar uma política que devolve sempre 0.

Como funcionam os tipos genéricos em Go?

Uma declaração de tipo pode ter parâmetros de tipo entre parênteses retos a seguir ao nome. Uma resposta paginada de uma API é um caso comum, já que os campos de paginação são os mesmos para qualquer tipo de 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())
}

Isto imprime [1 2] true. Page[Order] e Page[string] são dois tipos diferentes construídos a partir de uma só declaração. Os métodos repetem o parâmetro de tipo no recetor, como em func (p Page[T]).

O Map declara o seu próprio parâmetro de tipo U. Isso é um método genérico, e é uma novidade do Go 1.27 (notas de lançamento do Go 1.27). Com go 1.26 ou anterior no go.mod, o mesmo código falha com:

example.texttext
./main.go:17:22: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)

Os métodos de interfaces continuam a não poder ter parâmetros de tipo. Map[U any](fn func(string) U) []U dentro de uma interface falha com interface method must have no type parameters, por isso um método genérico nunca conta para satisfazer uma interface.

Onde mais aparece o type em Go?

O que faz o .(type) num type switch?

O x.(type) só é válido num switch e faz a ramificação pelo tipo dinâmico de um valor de interface. Um uso comum é com erros:

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()
	}
}

Em cada caso, o e tem o tipo desse caso, por isso e.Path e e.Timeout() compilam sem uma type assertion à parte. Chamar describe com o erro de os.Open("/etc/levelupgo.yaml") imprime file error on /etc/levelupgo.yaml quando o ficheiro não existe.

Um type switch só olha para o erro mais exterior. Depois de fmt.Errorf("load config: %w", err), a mesma chamada cai no default. Para erros embrulhados, usa errors.As, que percorre a cadeia:

example.gogo
var pathErr *fs.PathError
if errors.As(wrapped, &pathErr) {
	fmt.Println("errors.As found", pathErr.Path)
}

Como se usa o type nas restrições genéricas?

Uma restrição (constraint) é uma interface, normalmente declarada com type, e pode listar tipos além de métodos. O ~ antes de um tipo significa «qualquer tipo cujo tipo subjacente seja este», e é isso que deixa passar os tipos definidos:

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}) devolve 6249 como um Cents. Sem o ~, o Cents é recusado, e o compilador aponta a solução:

example.texttext
./main.go:20:17: Cents does not satisfy Amount (possibly missing ~ for int64 in Amount)

Os enums em Go também são construídos sobre tipos definidos. Um tipo como type OrderStatus int com um bloco const e iota dá-te uma lista tipada de valores. A palavra-chave const em Go mostra como, e a palavra-chave var em Go explica como as variáveis destes tipos começam no seu valor zero.

Onde entra o LevelUpGo

O LevelUpGo ensina Go através de exercícios que executam código Go real no navegador. O Composite Types constrói structs, funções construtoras e métodos passo a passo. O Interfaces & Polymorphism aborda interfaces implícitas, type assertions e type switches. O Go Generics Masterclass percorre os tipos genéricos e as restrições personalizadas. O Training Ground tem exercícios curtos e independentes para praticares fora de um curso. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.

Perguntas frequentes

O type é uma palavra-chave em Go?

Sim. O type é uma das 25 palavras-chave reservadas do Go, por isso não o podes usar como nome de variável ou de função. Inicia uma declaração de tipo, seja uma definição de tipo como type UserID int64 ou um alias como type Handler = http.Handler. Também aparece nos type switches, na forma x.(type).

Qual é a diferença entre uma definição de tipo e um alias de tipo em Go?

Uma definição de tipo, type UserID int64, cria um tipo novo que é diferente de int64 e começa sem métodos. Um alias, type ID = int64, é outro nome para o próprio int64. Os valores de um tipo definido precisam de uma conversão para se misturarem com o tipo original, enquanto um alias não precisa de nenhuma, porque os dois nomes designam o mesmo tipo.

Podes acrescentar métodos a um tipo built-in em Go?

Não. Só se podem declarar métodos em tipos definidos no mesmo pacote, por isso func (n int) IsEven() bool falha com cannot define new methods on non-local type int. Define o teu próprio tipo, como type Quantity int, e declara o método nele.

Porque é que o Go diz "cannot use id (variable of int64 type UserID) as int64 value"?

Porque UserID é um tipo definido, e o Go nunca converte sozinho um valor tipado de um tipo com nome para outro, mesmo quando o tipo subjacente é o mesmo. Só as constantes sem tipo, como 42, se adaptam ao tipo de que precisam. Converte explicitamente com int64(id). Se dás por ti a converter a toda a hora, provavelmente a função devia receber um UserID.

O que faz o .(type) em Go?

É a forma type switch de uma type assertion. switch e := err.(type) compara o tipo dinâmico de um valor de interface com cada case, e dentro de um caso o e tem o tipo desse caso. Só funciona num switch. Para testar um único tipo, usa v, ok := x.(T), e para erros embrulhados usa errors.As.

Fontes

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita