Usar métodos em Go

Concluído

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

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

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

Declarar métodos

Até agora, você usou structs apenas como outro tipo personalizado que pode criar em Go. Neste módulo, você aprenderá que, adicionando métodos, você pode adicionar comportamentos às estruturas criadas.

A sintaxe para declarar um método é algo assim:

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

No entanto, antes de poder declarar um método, você precisa criar uma estrutura. Digamos que você queira fazer um pacote de geometria e, como parte desse pacote, decida criar uma estrutura de triângulo chamada triangle. Em seguida, você deseja usar um método para calcular o perímetro desse triângulo. Você pode representá-lo em Go assim:

type triangle struct {
    size int
}

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

O struct parece um normal, mas a perimeter() função tem um parâmetro extra de tipo triangle antes do nome da função. Este recetor significa que quando você usa o struct, você pode chamar a função assim:

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

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

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á nenhum erro e você obtém a seguinte saída:

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

A partir das duas chamadas para a perimeter() função, o compilador determina qual função chamar com base no tipo de recetor. Esse comportamento ajuda a manter a consistência e nomes curtos em funções entre pacotes e evita incluir o 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 precisará atualizar uma variável. Ou, se o argumento para o método for muito grande, convém 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 ponteiros, dissemos que toda vez que você chama uma função em Go, Go faz uma cópia de cada valor de argumento para usá-la.

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

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

Você pode provar que o método funciona, assim:

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

Quando você executa o código anterior, você deve obter a seguinte saída:

Size: 6
Perimeter: 18

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

Declarar métodos para outros tipos

Um aspeto 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 stringarquivo .

No entanto, você pode usar um hack para criar um tipo personalizado a partir de um tipo básico e, em seguida, 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ê poderia escrever algo assim:

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())
}

Quando você executa o código anterior, você obtém a seguinte saída:

Learning Go!
LEARNING GO!

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

Incorporar métodos

Em um módulo anterior, você aprendeu que pode usar uma propriedade em uma struct e incorporar a mesma propriedade em outra struct. Ou seja, você pode reutilizar propriedades de uma struct para evitar repetição e manter a consistência em sua base de código. Uma ideia semelhante aplica-se aos métodos. Você pode chamar métodos da struct incorporada mesmo se o recetor for diferente.

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

type coloredTriangle struct {
    triangle
    color string
}

Você pode então inicializar o coloredTriangle struct e chamar o perimeter() método do triangle struct (e até mesmo acessar seus campos), da seguinte forma:

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

Vá em frente e inclua as alterações anteriores no seu programa para ver como a incorporação funciona. Quando você executa o programa com um main() método 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++, pode pensar que o triangle struct se parece com uma classe base e coloredTriangle é uma subclasse (como herança), mas isso não está correto. O que está acontecendo, na realidade, é que o compilador Go está promovendo o perimeter() método criando um método wrapper, que se parece com isto:

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

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

Métodos de sobrecarga

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

Em outras palavras, você pode escrever o método wrapper que discutimos se quiser alterar seu comportamento. Se o perímetro de um triângulo colorido for o dobro do perímetro de um triângulo normal, o código seria algo assim:

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

Agora, sem alterar mais nada no método que main() você escreveu antes, 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 perimeter() método do triangle struct, você pode fazê-lo acessando-o explicitamente, assim:

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 você executa esse código, você deve obter a seguinte saída:

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

Como você deve ter notado, em Go, você pode substituir um método e ainda acessar o original , se precisar.

Encapsulamento em métodos

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 private palavras-chave ou public antes do nome do método. Em Go, você precisa usar apenas um identificador em maiúsculas para tornar um método público e um identificador sem maiúsculas para tornar um método privado.

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

Para experimentar, crie um novo pacote geometry e mova a estrutura do triângulo para lá, assim:

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, assim:

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

E você deve obter a seguinte saída:

Perimeter 18

Se você tentar chamar o size campo ou o doubleSize() método da main() função, o programa entrará em pânico, assim:

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

Quando você executa o código anterior, você obtém o seguinte erro:

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