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