Usar métodos em Go

Concluído

Um método em Go é um tipo especial de função com uma diferença simples: você precisa incluir um parâmetro extra antes do nome da função. Esse parâmetro extra é conhecido como o receptor.

Os métodos são úteis quando você deseja agrupar funções e associá-las a um tipo personalizado. Essa abordagem em Go é semelhante à criação de uma classe em outras linguagens de programação, pois permite que você implemente determinados recursos do modelo de OOP (programação orientada a objeto), como inserção, sobrecarga e encapsulamento.

Para entender por que os métodos são importantes em Go, vamos começar com a forma como você os declara.

Declarar métodos

Até agora, você usou structs somente como mais um tipo personalizado qualquer que pode ser criado no Go. Neste módulo, você aprenderá que, ao adicionar métodos, poderá adicionar comportamentos aos structs que criar.

A sintaxe para declarar um método é como no seguinte exemplo:

func (variable type) MethodName(parameters ...) {
    // method functionality
}

No entanto, para declarar um método, é necessário criar um struct. Digamos que você deseje criar um pacote de geometria e, como parte desse pacote, você decida criar um struct de triângulo chamado triangle. Em seguida, você deseja usar um método para calcular o perímetro desse triângulo. Você pode representá-lo no Go da seguinte maneira:

type triangle struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

O struct é semelhante a um normal, mas a função perimeter() tem um parâmetro extra do tipo triangle antes do nome da função. Esse receptor significa que, quando você usar o struct, poderá chamar a função desta maneira:

func main() {
    t := triangle{3}
    fmt.Println("Perimeter:", t.perimeter())
}

Se você tentar chamar a função perimeter() como faria normalmente, ela não funcionará porque a assinatura da função diz que ela precisa de um receptor. A única maneira de chamar esse método é declarando um struct primeiro, o que fornece a você acesso ao método. Você pode até ter o mesmo nome para um método, desde que ele pertença a um struct diferente. Por exemplo, você pode declarar um struct square com uma função perimeter(), desta forma:

package main

import "fmt"

type triangle struct {
    size int
}

type square struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

func (s square) perimeter() int {
    return s.size * 4
}

func main() {
    t := triangle{3}
    s := square{4}
    fmt.Println("Perimeter (triangle):", t.perimeter())
    fmt.Println("Perimeter (square):", s.perimeter())
}

Quando você executa o código anterior, observe que não há erro e você obtém a seguinte saída:

Perimeter (triangle): 9
Perimeter (square): 16

Das duas chamadas para a função perimeter(), o compilador determina qual função chamar com base no tipo de receptor. Esse comportamento ajuda a manter a consistência e os nomes curtos nas funções entre os pacotes e evita a inclusão do nome do pacote como um prefixo. Falaremos sobre por que esse comportamento pode ser importante quando abordarmos as interfaces na próxima unidade.

Ponteiros em métodos

Haverá momentos em que um método precisa atualizar uma variável. Ou, então, se o argumento para o método é muito grande, talvez você queira evitar copiá-lo. Nesses casos, você precisa usar ponteiros para passar o endereço de uma variável. Em um módulo anterior, quando discutimos os ponteiros, dissemos que sempre que você chama uma função em Go, o Go faz uma cópia de cada valor de argumento para usá-lo.

O mesmo comportamento está presente quando você precisa atualizar a variável receptora em um método. Por exemplo, digamos que você queira criar um método para dobrar o tamanho do triângulo. Você precisa usar um ponteiro na variável receptora, desta forma:

func (t *triangle) doubleSize() {
    t.size *= 2
}

Você pode provar que o método funciona, da seguinte maneira:

func main() {
    t := triangle{3}
    t.doubleSize()
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}

Ao executar o código anterior, você obterá a seguinte saída:

Size: 6
Perimeter: 18

Você não precisa de um ponteiro na variável receptora quando o método está apenas acessando as informações do receptor. No entanto, a convenção do Go determina que, se qualquer método de um struct tiver um receptor de ponteiro, todos os métodos desse struct precisarão ter um receptor de ponteiro. Mesmo que um método do struct não precise dele.

Declarar métodos para outros tipos

Um aspecto crucial dos métodos é defini-los para qualquer tipo, não apenas para tipos personalizados, como structs. No entanto, você não pode definir um struct de um tipo que pertence a outro pacote. Portanto, você não pode criar um método em um tipo básico, como um string.

No entanto, você pode usar um ataque para criar um tipo personalizado com base em um tipo básico e usá-lo como se fosse o tipo básico. Por exemplo, digamos que você queira criar um método para transformar uma cadeia de caracteres de letras minúsculas em maiúsculas. Você poderá escrever algo como este código:

package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return strings.ToUpper(string(s))
}

func main() {
    s := upperstring("Learning Go!")
    fmt.Println(s)
    fmt.Println(s.Upper())
}

Ao executar o código anterior, você obterá a seguinte saída:

Learning Go!
LEARNING GO!

Observe como você pode usar o novo objeto s como se fosse uma cadeia de caracteres ao imprimir o valor dele pela primeira vez. Em seguida, quando você chama o método Upper, s imprime todas as letras maiúsculas do tipo cadeia de caracteres.

Inserir métodos

Em um módulo anterior, você aprendeu que pode usar uma propriedade em um struct e inserir a mesma propriedade em outro struct. Ou seja, você pode reutilizar as propriedades de um struct para evitar a repetição e manter a consistência em sua base de código. Uma ideia semelhante se aplica aos métodos. Você pode chamar métodos do struct inserido mesmo se o receptor é diferente.

Por exemplo, digamos que você deseja criar um struct de triângulo com lógica para incluir uma cor. Além disso, você deseja continuar usando o struct de triângulo declarada anteriormente. Assim, o struct do triângulo colorido ficaria assim:

type coloredTriangle struct {
    triangle
    color string
}

Em seguida, você poderia inicializar o struct coloredTriangle e chamar o método perimeter() do struct triangle (e até mesmo acessar os campos dele), desta maneira:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Inclua as alterações anteriores em seu programa para ver como a inserção funciona. Ao executar o programa com um método main() como o anterior, você deve obter a seguinte saída:

Size: 3
Perimeter 9

Se você estiver familiarizado com uma linguagem OOP, como Java ou C++, talvez pense que o struct triangle se parece com uma classe base e que coloredTriangle é uma subclasse (como a herança), mas isso não está correto. O que está acontecendo, na realidade, é que o compilador do Go está promovendo o método perimeter() pela criação de um método wrapper, semelhante a este:

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

Observe que o receptor é coloredTriangle, que chama o método perimeter() do campo do triângulo. A boa notícia é que você não precisa criar o método anterior. Você poderia, mas o Go faz isso para você nos bastidores. Incluímos o exemplo anterior apenas para fins de aprendizagem.

Métodos de sobrecarga

Vamos voltar ao exemplo de triangle que discutimos anteriormente. O que acontece se você quiser alterar a implementação do método perimeter() no struct coloredTriangle? Você não pode ter duas funções com o mesmo nome. No entanto, como os métodos precisam de um parâmetro extra (o receptor), você tem permissão para ter um método com o mesmo nome, desde que ele seja específico ao receptor que você deseja usar. Fazer uso dessa distinção é como você sobrecarrega os métodos.

Em outras palavras, você poderá escrever o método wrapper que abordamos se quiser alterar o comportamento dele. Se o perímetro de um triângulo colorido for o dobro do perímetro de um triângulo normal, o código será semelhante a este:

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

Agora, sem alterar nada mais no método main() que você escreveu anteriormente, ele ficaria assim:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Ao executá-lo, você obtém uma saída diferente:

Size: 3
Perimeter 18

No entanto, se você ainda precisar chamar o método perimeter() do struct triangle, poderá fazê-lo acessando-o explicitamente, desta forma:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter (colored)", t.perimeter())
    fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}

Quando executar este código, você deverá obter a seguinte saída:

Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

Como você deve ter notado, em vez disso, é possível substituir um método e, se necessário, ainda acessar o original.

Encapsulamento em métodos

O encapsulamento significa que um método é inacessível para o chamador (cliente) de um objeto. Normalmente, em outras linguagens de programação, você coloca as palavras-chave private ou public antes do nome do método. No Go, você precisa usar apenas um identificador em maiúsculas para tornar um método público e um identificador em minúsculas para tornar um método privado.

O encapsulamento no Go entra em vigor somente entre pacotes. Em outras palavras, você só pode ocultar os detalhes de implementação de outro pacote, não o pacote propriamente dito.

Para experimentar, crie um pacote geometry e mova o struct do triângulo para ele, desta forma:

package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}

Você pode usar o pacote anterior, desta forma:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
}

Você deverá obter a seguinte saída:

Perimeter 18

Se você tentar chamar o campo size ou o método doubleSize() da função main(), o programa entrará em pane, desta forma:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Size", t.size)
    fmt.Println("Perimeter", t.Perimeter())
}

Ao executar o código anterior, você receberá o seguinte erro:

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)