Skip to main content


Composição


A composição é uma forma de montar um tipo usando outros tipos. Como em Go não existe herança como em linguagens baseadas em classes, em vez de criar uma relação como "um tipo herda de outro", o Go prefere composição, ou seja, um tipo pode conter outro tipo.



Por que composição existe


Imagine que temos uma struct chamada Endereco:

type Endereco struct {
Cidade string
Estado string
}

Agora imagine que um Usuario também precisa ter cidade e estado, uma opção seria repetir os campos:

type Usuario struct {
Nome string
Idade int
Cidade string
Estado string
}

Isso funciona, mas começa a ficar repetitivo, caso outros tipos também precisarem de endereço, como Empresa ou Fornecedor, os mesmos campos seriam repetidos várias vezes. Com composição, podemos separar Endereco em uma struct própria e colocar essa struct dentro de outras structs.



Composição com campo nomeado


A forma mais explícita de composição é usar uma struct como campo de outra struct.

package main

import "fmt"

// Endereco representa dados de endereço:
type Endereco struct {
Cidade string
Estado string
}

// Usuario tem um Endereco.
// Aqui Endereco é um campo nomeado da struct Usuario:
type Usuario struct {
Nome string
Idade int
Endereco Endereco // Pertence a struct 'Endereco'.
}

func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
Endereco: Endereco{
Cidade: "São Paulo",
Estado: "SP",
},
}

// Acessa o campo Nome diretamente.
fmt.Println(usuario.Nome)

// Acessa os dados de endereço através do campo Endereco.
fmt.Println(usuario.Endereco.Cidade)
fmt.Println(usuario.Endereco.Estado)
}

Resultado:

Ana
São Paulo
SP

Aqui Usuario tem um campo chamado Endereco, e esse campo é do tipo Endereco. Essa forma é bem clara, porque o acesso mostra exatamente de onde o dado vem:

usuario.Endereco.Cidade


Composição com campo embutido


Também podemos colocar uma struct dentro de outra sem dar um nome explícito para o campo. Isso é chamado de embedding, ou campo embutido. Eu particularmente prefiro esse método, mesmo que ele seja menos explícito.

package main

import "fmt"

// Endereco representa dados de endereço:
type Endereco struct {
Cidade string
Estado string
}

type Usuario struct {
Nome string
Idade int

// Endereco foi embutido em Usuario.
Endereco
}

func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
Endereco: Endereco{
Cidade: "São Paulo",
Estado: "SP",
},
}

// Forma completa.
fmt.Println(usuario.Endereco.Cidade)

// Forma promovida pelo embedding.
fmt.Println(usuario.Cidade)
}

Resultado:

São Paulo
São Paulo

Mesmo que Cidade esteja dentro de Endereco, Go permite acessar como se fosse um campo direto de Usuario. Isso acontece porque os campos de Endereco foram promovidos para Usuario. Ser "promovido" não significa que o campo foi copiado, e sim que o Go permite um acesso mais curto.



Métodos também são promovidos


Composição não promove apenas campos, os métodos também podem ser promovidos.

package main

import "fmt"

type Endereco struct {
Cidade string
Estado string
}

// resumo é um método de Endereco:
func (e Endereco) resumo() string {
return e.Cidade + " - " + e.Estado
}

type Usuario struct {
Nome string
Endereco
}

func main() {
usuario := Usuario{
Nome: "Ana",
Endereco: Endereco{
Cidade: "São Paulo",
Estado: "SP",
},
}

// Forma completa.
fmt.Println(usuario.Endereco.resumo())

// Forma promovida pelo embedding.
fmt.Println(usuario.resumo())
}

Resultado:

São Paulo - SP
São Paulo - SP

O método resumo pertence a Endereco, mas como Endereco foi embutido em Usuario, podemos chamar:

usuario.resumo()

Isso é um dos motivos pelos quais composição é útil em Go, um tipo pode reaproveitar campos e métodos de outro tipo sem usar herança.



Conflito de nomes


Se a struct externa tiver um campo com o mesmo nome de um campo embutido, o campo mais externo tem prioridade.

package main

import "fmt"

type Endereco struct {
Cidade string
}

type Usuario struct {
Nome string

// Campo direto em Usuario.
Cidade string

// Endereco também tem Cidade.
Endereco
}

func main() {
usuario := Usuario{
Nome: "Ana",
Cidade: "Rio de Janeiro",
Endereco: Endereco{
Cidade: "São Paulo",
},
}

// Acessa Cidade de Usuario.
fmt.Println(usuario.Cidade)

// Acessa Cidade de Endereco.
fmt.Println(usuario.Endereco.Cidade)
}

Resultado:

Rio de Janeiro
São Paulo

Quando escrevemos:

usuario.Cidade

O Go usa o campo direto de Usuario, para acessar a cidade de Endereco, temos que usar: usuario.Endereco.Cidade.



Ponteiros


Um ponteiro é um valor que guarda o endereço de memória de outro valor. Em vez de guardar o dado diretamente, o ponteiro guarda onde esse dado está.

idade := 20
ponteiro := &idade

A variável idade guarda o valor 20, enquanto que a variável ponteiro guarda o endereço onde idade está na memória.


Em Go, quando passamos um valor para uma função, normalmente esse valor é copiado, exemplo:

package main

import "fmt"

func alterarIdade(idade int) {
idade = 30
}

func main() {
idade := 20

alterarIdade(idade)

fmt.Println(idade)
}

Resultado:

20

A função alterarIdade recebeu uma cópia do valor 20. Dentro da função, essa cópia foi alterada para 30, mas a variável original continuou sendo 20. Com ponteiros, podemos trabalhar com o valor original, e não com uma cópia.



Operador &


O operador & pega o endereço de uma variável.

package main

import "fmt"

func main() {
idade := 20

// &idade pega o endereço de memória da variável idade.
ponteiro := &idade

fmt.Println(idade)
fmt.Println(ponteiro)
}

Resultado aproximado:

20
0xc0000120a0

Esse valor estranho, como 0xc0000120a0, é um endereço de memória. Ele pode mudar a cada execução.



Tipo de um ponteiro


Se idade é do tipo int, então &idade é do tipo *int.

package main

import "fmt"

func main() {
idade := 20
ponteiro := &idade

fmt.Printf("tipo de idade: %T\n", idade)
fmt.Printf("tipo de ponteiro: %T\n", ponteiro)
}

Resultado:

tipo de idade: int
tipo de ponteiro: *int


Operador *


O operador * acessa o valor guardado no endereço apontado pelo ponteiro.

package main

import "fmt"

func main() {
idade := 20
ponteiro := &idade

// ponteiro guarda o endereço.
fmt.Println(ponteiro)

// *ponteiro acessa o valor que está naquele endereço.
fmt.Println(*ponteiro)
}

Resultado aproximado:

0xc0000120a0
20

Esse ato de acessar o valor apontado pelo ponteiro é chamado de dereferenciar.



Alterando valor usando ponteiro


Como *ponteiro acessa o valor original, também podemos alterar esse valor.

package main

import "fmt"

func main() {
idade := 20
ponteiro := &idade

// Altera o valor que está no endereço apontado.
*ponteiro = 30

fmt.Println(idade)
}

Resultado:

30

Aqui não alteramos uma cópia e sim o valor original de idade.



Ponteiros com structs


Ponteiros são muito comuns com structs, principalmente quando uma função ou método precisa alterar campos da struct original. Exemplo sem ponteiro:

package main

import "fmt"

type Usuario struct {
Nome string
Idade int
}

func fazerAniversario(u Usuario) {
u.Idade++
}

func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
}

fazerAniversario(usuario)

fmt.Println(usuario.Idade)
}

Resultado:

20

A função recebeu uma cópia da struct, então a idade da cópia mudou, mas a struct original não mudou. Agora com ponteiro:

package main

import "fmt"

type Usuario struct {
Nome string
Idade int
}

func fazerAniversario(u *Usuario) {
// Go permite acessar o campo diretamente com u.Idade.
// Não precisamos escrever (*u).Idade nesse caso.
u.Idade++
}

func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
}

// Passando o ponteiro para a função:
fazerAniversario(&usuario)

fmt.Println(usuario.Idade)
}

Resultado:

21

Aqui fazerAniversario recebeu um ponteiro para Usuario, então conseguiu alterar a struct original.



Ponteiros em métodos


Nos métodos, ponteiros aparecem no receiver, vejamos um receiver sem ponteiro:

func (u Usuario) apresentar() {
fmt.Println(u.Nome)
}

Esse método recebe uma cópia do Usuario. Agora vamos ver um receiver com ponteiro:

func (u *Usuario) fazerAniversario() {
u.Idade++
}

Esse método recebe acesso ao Usuario original, um exemplo completo seria:

package main

import "fmt"

type Usuario struct {
Nome string
Idade int
}

func (u Usuario) apresentar() {
fmt.Println("Nome:", u.Nome)
fmt.Println("Idade:", u.Idade)
}

func (u *Usuario) fazerAniversario() {
u.Idade++
}

func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
}

usuario.apresentar()

usuario.fazerAniversario()

usuario.apresentar()
}

Resultado:

Nome: Ana
Idade: 20
Nome: Ana
Idade: 21

O método apresentar só lê os dados, então pode usar receiver sem ponteiro, já o método fazerAniversario altera a struct, então usa receiver com ponteiro.



nil em ponteiros


Um ponteiro pode ser nil, ou seja, o ponteiro não aponta para nenhum valor.

package main

import "fmt"

func main() {
var ponteiro *int

fmt.Println(ponteiro)
}

Resultado:

<nil>

O cuidado é que não podemos dereferenciar um ponteiro nil.

package main

func main() {
var ponteiro *int

// Erro em tempo de execução.
// ponteiro não aponta para nenhum valor.
// fmt.Println(*ponteiro)
}

Antes de usar *ponteiro, é comum verificar se ele não é nil.

package main

import "fmt"

func main() {
var ponteiro *int

if ponteiro == nil {
fmt.Println("ponteiro não aponta para nenhum valor")
return
}

fmt.Println(*ponteiro)
}


Quando usar ponteiro


Use ponteiro quando a função ou método precisa alterar o valor original.

func (u *Usuario) alterarNome(nome string) {
u.Nome = nome
}

Use ponteiro quando a struct é grande e não faz sentido copiar tudo a cada chamada.

func processarPedido(p *Pedido) {
}

Use ponteiro quando nil tem significado no seu programa, por exemplo, quando algo pode existir ou não existir.

var usuario *Usuario