Métodos
Os Métodos são funções associadas a um tipo, na prática, um método é uma função que "pertence" a uma struct ou a outro tipo criado no programa.
Antes, criamos funções assim:
func saudar(nome string) {
fmt.Println("Olá,", nome)
}
Com métodos, podemos associar o comportamento a um tipo específico.
func (u Usuario) saudar() {
fmt.Println("Olá,", u.Nome)
}
A parte diferente é esta:
(u Usuario)
Isso é chamado de receiver, ele indica que a função é um método do tipo Usuario.
Por que métodos existem
As structs guardam dados, os métodos adicionam comportamentos a esses dados. Por exemplo, uma struct Usuario pode guardar nome e idade:
type Usuario struct {
Nome string
Idade int
}
Só que talvez o usuário também precise saber se é maior de idade, mostrar uma descrição ou validar algum campo. Até poderíamos criar funções soltas:
func ehMaiorDeIdade(usuario Usuario) bool {
return usuario.Idade >= 18
}
Isso funciona, mas o comportamento fica separado do tipo. Com método, o comportamento fica ligado ao próprio Usuario:
func (u Usuario) ehMaiorDeIdade() bool {
return u.Idade >= 18
}
Assim, usamos:
usuario.ehMaiorDeIdade()
Isso deixa claro que aquele comportamento pertence ao usuário.
Criando um método
Vejamos um exemplo simples:
package main
import "fmt"
// Usuario representa os dados de um usuário.
type Usuario struct {
Nome string
Idade int
}
// apresentar é um método do tipo Usuario.
// O receiver (u Usuario) permite acessar os campos da struct.
func (u Usuario) apresentar() {
fmt.Println("Nome:", u.Nome)
fmt.Println("Idade:", u.Idade)
}
func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
}
// Chama o método apresentar a partir do valor usuario.
usuario.apresentar()
}
Resultado:
Nome: Ana
Idade: 20
Receiver
O receiver é o valor que o método recebe antes do nome. O receiver é bem parecido com o self do Python na ideia geral.
func (u Usuario) apresentar() {
}
Nesse caso, u é o nome usado dentro do método e Usuario é o tipo ao qual o método pertence. Dentro do método, usamos u para acessar os campos:
u.Nome
u.Idade
O nome do receiver costuma ser curto. Em Go, é comum usar a primeira letra do tipo.
func (u Usuario) apresentar() {
}
Para Produto, seria comum:
func (p Produto) exibir() {
}
Para Pedido, seria comum:
func (p Pedido) finalizar() {
}
Não é obrigatório, mas é um padrão mais adotado.
Método com retorno
Um método também pode retornar valor.
package main
import "fmt"
type Usuario struct {
Nome string
Idade int
}
// maiorDeIdade retorna true se o usuário tiver 18 anos ou mais.
func (u Usuario) maiorDeIdade() bool {
return u.Idade >= 18
}
func main() {
usuario := Usuario{
Nome: "Carlos",
Idade: 17,
}
fmt.Println(usuario.maiorDeIdade())
}
Resultado:
false
Esse método pertence ao tipo Usuario e retorna um bool.
A assinatura é:
func (u Usuario) maiorDeIdade() bool
A parte final:
bool
indica o tipo retornado.
Método que monta uma string
Métodos são úteis quando queremos transformar dados da struct em alguma informação pronta.
package main
import "fmt"
type Usuario struct {
Nome string
Idade int
}
// resumo retorna uma descrição do usuário.
func (u Usuario) resumo() string {
return fmt.Sprintf("%s tem %d anos", u.Nome, u.Idade)
}
func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
}
fmt.Println(usuario.resumo())
}
Resultado:
Ana tem 20 anos
Aqui o método não imprime diretamente. Ele retorna uma string.
Isso deixa o método mais flexível, porque quem chamou decide o que fazer com o texto.
Método vs função
Função solta:
func resumoUsuario(u Usuario) string {
return fmt.Sprintf("%s tem %d anos", u.Nome, u.Idade)
}
Uso:
resumoUsuario(usuario)
Método:
func (u Usuario) resumo() string {
return fmt.Sprintf("%s tem %d anos", u.Nome, u.Idade)
}
Uso:
usuario.resumo()
A diferença principal é que o método fica associado ao tipo.
Use função quando o comportamento não pertence claramente a um tipo.
Use método quando o comportamento faz sentido como uma ação ou informação daquele tipo.
Por exemplo:
usuario.resumo()
usuario.maiorDeIdade()
produto.aplicarDesconto()
pedido.calcularTotal()
Esses nomes fazem sentido como métodos, porque estão ligados aos dados da própria struct.
Método com parâmetros
Além do receiver, um método também pode receber parâmetros normais.
package main
import "fmt"
type Produto struct {
Nome string
Preco float64
}
// precoComDesconto recebe uma porcentagem e retorna o preço final.
func (p Produto) precoComDesconto(percentual float64) float64 {
desconto := p.Preco * percentual / 100
return p.Preco - desconto
}
func main() {
produto := Produto{
Nome: "Teclado",
Preco: 200.00,
}
precoFinal := produto.precoComDesconto(10)
fmt.Println(precoFinal)
}
Resultado:
180
Nesse exemplo, p Produto é o receiver.
percentual float64 é um parâmetro normal do método.
Método que altera a struct
Quando um método precisa alterar os campos da struct original, usamos receiver com ponteiro.
func (u *Usuario) fazerAniversario() {
u.Idade++
}
O *Usuario significa que o método recebe uma referência para o valor original, não uma cópia simples.
Exemplo:
package main
import "fmt"
type Usuario struct {
Nome string
Idade int
}
// fazerAniversario altera a idade do usuário original.
// Por isso usamos receiver com ponteiro: *Usuario.
func (u *Usuario) fazerAniversario() {
u.Idade++
}
func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
}
usuario.fazerAniversario()
fmt.Println(usuario.Idade)
}
Resultado:
21
Se o receiver fosse sem ponteiro:
func (u Usuario) fazerAniversario() {
u.Idade++
}
a alteração seria feita em uma cópia, e o valor original não mudaria.
Exemplo:
package main
import "fmt"
type Usuario struct {
Nome string
Idade int
}
// Aqui o receiver não é ponteiro.
// O método recebe uma cópia do usuário.
func (u Usuario) fazerAniversario() {
u.Idade++
}
func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
}
usuario.fazerAniversario()
// Continua 20, porque o método alterou uma cópia.
fmt.Println(usuario.Idade)
}
Resultado:
20
Por enquanto, a regra prática é:
Se o método só lê os dados, pode usar receiver sem ponteiro. Se o método precisa alterar a struct original, use receiver com ponteiro.
Método não precisa ser só de struct
Métodos podem ser criados para tipos próprios, não apenas structs.
Exemplo:
package main
import "fmt"
// Idade é um tipo próprio baseado em int.
type Idade int
// maiorDeIdade é um método do tipo Idade.
func (i Idade) maiorDeIdade() bool {
return i >= 18
}
func main() {
idade := Idade(20)
fmt.Println(idade.maiorDeIdade())
}
Resultado:
true
Isso mostra que método pertence a um tipo, e não obrigatoriamente a uma struct.
Mesmo assim, no começo, o uso mais comum será com structs.
Métodos e campos podem ter nomes parecidos, mas não iguais no mesmo tipo
Um tipo não deve ter um campo e um método com o mesmo nome.
Exemplo ruim:
type Usuario struct {
Nome string
}
// Evite criar método com o mesmo nome de um campo.
// func (u Usuario) Nome() string {
// return u.Nome
// }
Isso gera conflito de nome.
Uma opção melhor seria:
func (u Usuario) obterNome() string {
return u.Nome
}
Ou, na maioria dos casos, acessar o campo diretamente:
usuario.Nome
Métodos com letra maiúscula e minúscula
A regra de visibilidade também vale para métodos.
Método com letra minúscula fica acessível apenas dentro do mesmo pacote:
func (u Usuario) resumo() string {
return u.Nome
}
Método com letra maiúscula pode ser acessado por outros pacotes:
func (u Usuario) Resumo() string {
return u.Nome
}
Como ainda estamos usando package main, isso não pesa tanto agora. Quando estudarmos packages, essa diferença vai ficar mais importante.
Exemplo mais completo
package main
import "fmt"
type Produto struct {
Nome string
Preco float64
}
// exibir mostra os dados do produto.
func (p Produto) exibir() {
fmt.Println("Produto:", p.Nome)
fmt.Println("Preço:", p.Preco)
}
// precoComDesconto calcula um novo preço sem alterar o produto original.
func (p Produto) precoComDesconto(percentual float64) float64 {
desconto := p.Preco * percentual / 100
return p.Preco - desconto
}
// alterarPreco muda o preço do produto original.
// Como altera a struct, usa receiver com ponteiro.
func (p *Produto) alterarPreco(novoPreco float64) {
p.Preco = novoPreco
}
func main() {
produto := Produto{
Nome: "Mouse",
Preco: 100.00,
}
produto.exibir()
precoFinal := produto.precoComDesconto(10)
fmt.Println("Preço com desconto:", precoFinal)
produto.alterarPreco(80.00)
fmt.Println("Novo preço:", produto.Preco)
}
Resultado:
Produto: Mouse
Preço: 100
Preço com desconto: 90
Novo preço: 80