Menu

Embedding de structs em Golang: campos e métodos promovidos

Embedding coloca um tipo dentro de outro sem nome de campo, e os campos e métodos dele são promovidos para o tipo externo. Veja como a promoção funciona, embedding de interfaces, conflitos de nome e por que embedding não é herança.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Embedding em um exemplo

Um campo com tipo mas sem nome é um campo embutido. Os campos e métodos do tipo embutido passam a ser acessíveis diretamente no tipo externo.

Saída:

Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}

O nome do campo embutido é o nome do seu tipo, User. É assim que você se refere a ele em literais (User: User{...}) e quando precisa do próprio valor interno (a.User). Um campo promovido não pode ser definido pelo nome curto em um literal: Admin{Name: "Ana"} falha com unknown field Name in struct literal of type Admin.

Métodos promovidos satisfazem interfaces

A promoção acrescenta os métodos do tipo embutido ao conjunto de métodos do tipo externo. Então o tipo externo satisfaz todas as interfaces que o tipo interno satisfaz.

As regras de conjunto de métodos seguem o tipo embutido. Embutir T promove os métodos com receiver de valor de T tanto para o valor externo quanto para o ponteiro externo, mas os métodos com receiver ponteiro de *T só para o ponteiro externo. Embutir *T promove os dois conjuntos para os dois. Na dúvida, embuta o ponteiro, ou use o tipo externo por meio de um ponteiro.

Embutir um ponteiro tem um custo: o ponteiro pode ser nil. Service{} sem Logger compila, e svc.Log(...) então causa panic.

Embedding não é herança

O embedding parece subclasse, mas três coisas se comportam diferente de Java ou Python.

O tipo externo não é o tipo interno. Um Admin não é um User. Uma função que recebe um User não aceita um Admin; passe a.User.

Não existe despacho virtual. Um método promovido executa sobre o valor embutido e não sabe nada do tipo externo. Se ele chamar outro método, chama a versão do próprio tipo, mesmo quando o tipo externo define um com o mesmo nome.

Saída:

woof
The animal says ...

Em uma linguagem baseada em classes, Speak imprimiria "woof". Em Go, d.Speak() é um atalho para d.Animal.Speak(), e o receiver dessa chamada é o Animal. Quando você quer um comportamento que varie por tipo, use uma interface: passe um Sounder para o código que precisa dele.

O tipo interno não sabe que está embutido. Não existe super. Para estender um método promovido, defina um método com o mesmo nome no tipo externo e chame o interno explicitamente:

func (a Admin) Greet() string {
	return a.User.Greet() + " (admin)"
}

Conflitos de nome e sombreamento

Um campo ou método do tipo externo sombreia um promovido com o mesmo nome, como Dog.Sound fez acima. O nome mais raso vence.

Quando dois tipos embutidos na mesma profundidade promovem o mesmo nome, esse nome fica ambíguo. O programa continua compilando desde que nada use o nome ambíguo:

Resolva o conflito usando o caminho completo, ou definindo ID no tipo externo.

Embedding de interfaces

Interfaces podem embutir outras interfaces. A biblioteca padrão monta interfaces maiores desse jeito:

type ReadWriter interface {
	Reader
	Writer
}

Uma struct também pode embutir uma interface. A struct então satisfaz essa interface por meio do valor que você guardar no campo, e você sobrescreve só os métodos que importam. É o padrão decorator com quase nenhum código:

O c.Reader.Read(p) explícito é obrigatório. Escrever c.Read(p) dentro de Read chamaria a si mesmo para sempre.

Embutir uma interface em um fake de teste é um atalho comum: embuta a interface grande, implemente o único método de que o teste precisa e deixe o resto. Qualquer método não implementado causa panic de ponteiro nil se for chamado, o que muitas vezes é exatamente o que você quer em um teste.

Um uso real comum: embutir sync.Mutex

type Stats struct {
	sync.Mutex
	hits map[string]int
}

func (s *Stats) Hit(page string) {
	s.Lock()
	defer s.Unlock()
	s.hits[page]++
}

Isso fica legível, mas também exporta Lock e Unlock como parte da API de Stats, então qualquer código que a use pode travar a sua struct. Para tipos usados fora do pacote, um campo nomeado (mu sync.Mutex) mantém o lock privado. Esse dilema vale para todo embedding: tudo o que o tipo embutido exporta passa a fazer parte da superfície pública do seu tipo.

Erros comuns

  • Esperar despacho virtual. Métodos promovidos nunca chamam as versões sobrescritas do tipo externo.
  • Definir campos promovidos em um literal. Use o nome do tipo embutido: Admin{User: User{Name: "Ana"}}.
  • Ponteiros embutidos nil. Um *T ou uma interface embutidos precisam ser definidos antes de os métodos serem chamados.
  • API exposta sem querer. O embedding exporta no seu tipo todos os métodos exportados do tipo interno.

Perguntas frequentes

O que é embedding de structs em Go?

Declarar um campo só com o tipo, sem nome: type Admin struct { User; Level int }. Os campos e métodos do User embutido são promovidos, então a.Name e a.Greet() funcionam diretamente em um Admin. O valor embutido continua sendo um campo normal, acessível como a.User.

Go tem herança?

Não. Go não tem classes nem herança de subtipos. O embedding dá reuso de código por composição: o tipo externo ganha os métodos do tipo interno, mas um Admin não é um User. Você não pode passar um Admin onde se espera um User, e métodos promovidos não conseguem chamar de volta os métodos do tipo externo. Em Go, o polimorfismo vem das interfaces.

Como inicializar uma struct embutida em Go?

Em um literal composto, identifique o campo embutido pelo nome do tipo: Admin{User: User{Name: "Ana"}, Level: 2}. Não dá para definir campos promovidos diretamente no literal: Admin{Name: "Ana"} falha com unknown field Name in struct literal of type Admin.

Dá para embutir uma interface em uma struct em Go?

Sim. A struct passa a satisfazer a interface por meio do valor embutido, e você pode sobrescrever métodos individuais. É comum em decorators e em fakes de teste. Se o campo de interface embutido for nil, chamar um método que você não sobrescreveu causa panic de desreferência de ponteiro nil.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR