Introdução

As funções permitem que você organize a lógica em procedimentos repetíveis que possam usar diferentes argumentos sempre que forem executados. Durante o processo de definição das funções, com frequência você verá que várias funções podem operar na mesma parte dos dados a cada vez. O Go reconhece esse padrão e permite que você defina funções especiais, chamadas de methods (métodos), cujo objetivo é operar em instâncias de um tipo específico, chamado de receiver (receptor). A adição dos métodos aos tipos permite que você comunique não apenas sobre do que se tratam os dados, mas também como esses dados devem ser usados.

Definindo um método

A sintaxe para definir um método é similar à sintaxe para definir uma função. A única diferença é a adição de um parâmetro extra após a palavra-chave func, para especificar o receptor do método. O receptor é uma declaração do tipo que você deseja para definir o método. O exemplo a seguir define um método em um tipo struct:

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() {
    fmt.Printf("%s says %s", c.Name, c.Greeting)
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    Creature.Greet(sammy)
}

Se executar este código, o resultado será:

Output
Sammy says Hello!

Criamos um struct chamado Creature, com os campos da string para Name e Greeting. Este Creature tem um método único definido, o Greet. Dentro da declaração do receptor, atribuímos a instância do Creature à variável c para que pudéssemos referir-nos aos campos do Creature, à medida que agrupamos a mensagem de saudação no fmt.Printf.

Em outras linguagens,normalmente, referimo-nos ao receptor das invocações de métodos por meio de uma palavra-chave (p. ex., this ou self). O Go considera o receptor como uma variável como qualquer outra. Assim, você pode dar a ele o nome que quiser. O estilo preferido pela comunidade para esse parâmetro é a versão do tipo de receptor com o primeiro caractere escrito em letra minúscula. Neste exemplo, usamos o c porque o tipo de receptor era o Creature.

Dentro do corpo do main, criamos uma instância de Creature e especificamos os valores para seus campos Name e Greetings. Aqui, invocamos o método Greet, juntando o nome do tipo e o nome do método com um ., fornecendo a instância de Creature como o primeiro argumento.

A linguagem Go oferece outras maneiras mais convenientes de chamar os métodos em instâncias de um struct, como mostramos neste exemplo:

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() {
    fmt.Printf("%s says %s", c.Name, c.Greeting)
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    sammy.Greet()
}

Se executar isso, o resultado será o mesmo que o do exemplo anterior:

Output
Sammy says Hello!

Esse exemplo é idêntico ao anterior. Porém, desta vez usamos dot notation para invocar o método Greet usando o Creature armazenado na variável sammy como o receptor. Esta é uma notação abreviada para a invocação da função no primeiro exemplo. A preferência que a biblioteca padrão e a comunidade Go têm por esse estilo é tanta que raramente veremos o estilo de invocação de função mostrado anteriormente.

O próximo exemplo mostra um motivo pelo qual a notação de ponto é a mais prevalente:

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() Creature {
    fmt.Printf("%s says %s!\n", c.Name, c.Greeting)
    return c
}

func (c Creature) SayGoodbye(name string) {
    fmt.Println("Farewell", name, "!")
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    sammy.Greet().SayGoodbye("gophers")

    Creature.SayGoodbye(Creature.Greet(sammy), "gophers")
}

Se executar esse código, o resultado ficará parecido com este:

Output
Sammy says Hello!! Farewell gophers ! Sammy says Hello!! Farewell gophers !

Nós modificamos os exemplos anteriores para introduzir um outro método chamado SayGoodbye e mudamos o Greet para retornar um Creature para que possamos invocar outros métodos naquela instância. No corpo do main, chamamos os métodos Greet e SayGoodbye na variável sammy, primeiro usando a notação de ponto e, depois, utilizando o estilo de invocação de função.

Ambos os estilos produzem os mesmos resultados, mas o exemplo que usa a notação de ponto é muito mais legível. A cadeia de pontos também nos diz a sequência na qual os métodos serão invocados, onde o estilo funcional inverte essa sequência. A adição de um parâmetro à chamada SayGoodbye torna confusa a ordem das chamadas de método. A clareza da notação de ponto é o motivo deste ser o estilo preferido para invocar os métodos no Go: seja na biblioteca padrão ou entre pacotes de terceiros, você o encontrará por todo o ecossistema Go.

Definir métodos sobre tipos, ao invés de definir funções que operam em algum valor, têm outra importância especial para a linguagem de programação Go. Métodos são o conceito fundamental por trás das interfaces.

Interfaces

Ao definir um método em qualquer tipo no Go, tal método será adicionado ao method set do tipo. O conjunto de métodos é a coleção de funções associadas àquele tipo como métodos, sendo usada pelo compilador do Go para determinar se algum tipo pode ser atribuído a uma variável com um tipo de interface. Um tipo de interface consiste na especificação dos métodos que o compilador utiliza para garantir que um tipo forneça as implementações para esses métodos. Qualquer tipo que possua métodos com nome, parâmetros e valores de retorno iguais àqueles encontrados na definição de uma interface são reconhecidos por implementar tal interface e podem ser atribuídos às variáveis com o mesmo tipo daquela interface. A definição da interface fmt.Stringer da bilbioteca padrão é a seguinte:

type Stringer interface {
  String() string
}

Para que um tipo implemente a interface fmt.Stringer, ele precisa fornecer um método String() que retorna uma string. Implementar essa interface permitirá que o seu tipo seja impresso exatamente como quiser (por vezes chamado de “pretty-printed”(com estilo de formatação)) quando passar as instâncias do seu tipo para as funções definidas no pacote fmt. O exemplo a seguir define um tipo que implementa esta interface:

package main

import (
    "fmt"
    "strings"
)

type Ocean struct {
    Creatures []string
}

func (o Ocean) String() string {
    return strings.Join(o.Creatures, ", ")
}

func log(header string, s fmt.Stringer) {
    fmt.Println(header, ":", s)
}

func main() {
    o := Ocean{
        Creatures: []string{
            "sea urchin",
            "lobster",
            "shark",
        },
    }
    log("ocean contains", o)
}

Quando executar o código, você verá este resultado:

Output
ocean contains : sea urchin, lobster, shark

Esse exemplo define um novo tipo de struct chamado Ocean. O Ocean é conhecido por_ implementar_ a interface fmt.Stringer, uma vez que o Ocean define um método chamado String, o qual não precisa de parâmetros e retorna uma string. No main, definimos um novo Ocean e o passamos para uma função log, a qual toma uma string para imprimir primeiro, seguido de qualquer coisa que implemente o fmt.Stringer. Neste ponto, o compilador Go nos permite passar um o porque o Ocean implementa todos os métodos solicitados pelo fmt.Stringer. Dentro do log, usamos o fmt.Println, que chama o método String do Ocean quando ele encontra um fmt.Stringer como um dos seus parâmetros.

Se o Ocean não fornecesse um método String(), o Go produziria um erro de compilação, pois o métodologsolicita um fmt.Stringer como seu argumento. O erro se parece com este:

Output
src/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log: Ocean does not implement fmt.Stringer (missing String method)

O Go também irá certificar-se que o método String() fornecido corresponda exatamente ao que foi solicitado pela interface do fmt.Stringer. Se não o fizer, ele produzirá um erro que se parece com este:

Output
src/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log: Ocean does not implement fmt.Stringer (wrong type for String method) have String() want String() string

Até agora, nos exemplos, definimos os métodos no receptor de valor. Ou seja, se usarmos a invocação funcional de métodos, o primeiro parâmetro - referindo-se ao tipo em que o método foi definido - será um valor desse tipo, em vez de um ponteiro. Consequentemente, quaisquer modificações que fizermos na instância fornecida para o método será descartada quando o método completar a execução, pois o valor recebido é uma cópia dos dados. Também é possível definir métodos no receptor ponteiro para um tipo.

Ponteiros receptores

A sintaxe para definir métodos no ponteiro receptor é quase idêntica à usada para definir os métodos no receptor de valor. A diferença é a prefixação do nome do tipo na declaração do receptor com um asterisco (*). O exemplo a seguir define um método no ponteiro receptor para um tipo:

package main

import "fmt"

type Boat struct {
    Name string

    occupants []string
}

func (b *Boat) AddOccupant(name string) *Boat {
    b.occupants = append(b.occupants, name)
    return b
}

func (b Boat) Manifest() {
    fmt.Println("The", b.Name, "has the following occupants:")
    for _, n := range b.occupants {
        fmt.Println("\t", n)
    }
}

func main() {
    b := &Boat{
        Name: "S.S. DigitalOcean",
    }

    b.AddOccupant("Sammy the Shark")
    b.AddOccupant("Larry the Lobster")

    b.Manifest()
}

Você verá o seguinte resultado quando executar este exemplo:

Output
The S.S. DigitalOcean has the following occupants: Sammy the Shark Larry the Lobster

Esse exemplo definiu um tipo Boat (barco) com um Name (Nome) e os occupants (ocupantes). Queremos forçar o código em outros pacotes para adicionar apenas os ocupantes com o método AddOccupant. Assim, tornamos o campo campo occupants não exportado, deixando a primeira letra do nome do campo em letra minúscula. Também queremos garantir que chamar o AddOccupant fará a instância Boat ser modificada, motivo pelo qual definimos o AddOccupant no ponteiro receptor. Os ponteiros atuam como referência para uma uma instância específica de um tipo e não como uma cópia daquele tipo. Saber que o AddOccupant será chamado usando um ponteiro para o Boat garante que quaisquer modificações irão persistir.

Dentro do main, definimos uma nova variável, b, a qual reterá um ponteiro para um Boat (*Boat). Invocamos o método AddOccupant duas vezes nessa instância para adicionar dois passageiros. O método Manisfest é definido no valor Boat porque, em sua definição, o receptor foi especificado como (b Boat). No main, ainda conseguimos chamar o Manifest porque o Go consegue desreferenciar automaticamente o ponteiro para obter o valor de Boat. O b.Manifest() é equivalente ao (*b). Manifest().

O fato de um método ser definido em um ponteiro receptor ou em um receptor de valor tem implicações importantes ao se tentar atribuir valores para variáveis que sejam tipos de interface.

Ponteiros receptores e interfaces

Quando você atribuir um valor a uma variável com um tipo de interface, o compilador Go examinará o conjunto de métodos do tipo que está sendo atribuído para garantir que ele tenha os métodos que a interface espera. Os conjuntos de métodos para o ponteiro receptor e para o receptor de valor são diferentes pois os métodos - que recebem um ponteiro - podem modificar o seu receptor onde os que recebem um valor não podem.

O exemplo a seguir demonstra a definição de dois métodos: uma no ponteiro receptor de um tipo e em seu receptor de valor. No entanto, apenas o ponteiro receptor poderá satisfazer a interface - também definida neste exemplo:

package main

import "fmt"

type Submersible interface {
    Dive()
}

type Shark struct {
    Name string

    isUnderwater bool
}

func (s Shark) String() string {
    if s.isUnderwater {
        return fmt.Sprintf("%s is underwater", s.Name)
    }
    return fmt.Sprintf("%s is on the surface", s.Name)
}

func (s *Shark) Dive() {
    s.isUnderwater = true
}

func submerge(s Submersible) {
    s.Dive()
}

func main() {
    s := &Shark{
        Name: "Sammy",
    }

    fmt.Println(s)

    submerge(s)

    fmt.Println(s)
}

Quando executar o código, você verá este resultado:

Output
Sammy is on the surface Sammy is underwater

Esse exemplo definiu uma interface chamada Submersible que espera tipos que possuam o método Dive(). Na sequência, definimos um tipo Shark com um campo Name e um método isUnderwater para monitorar o estado do Shark. Definimos um método Dive() no ponteiro receptor para o Shark, o qual modificou o isUnderwater para true. Também definimos o método String() do receptor do valor, de modo que ele pudesse imprimir claramente o estado do Shark, usando o fmt.Println, através da interface fmt.Stringer - aceita pelo fmt.Println, conforme examinamos anteriormente. Também usamos uma função submerge que recebe um parâmetro Submersible.

Usar a interface Submersible em vez de uma *Shark permite que a função submerge dependa apenas do comportamento fornecido por um tipo. Isso torna a função submerge mais reutilizável, uma vez que você não teria que escrever novas funções submerge para um Submarine, uma Whale, ou qualquer outro habitante aquático no futuro - sobre os quais ainda nem pensamos. Contanto que eles definam um método Dive(), eles podem ser usados com a função submerge.

Dentro do main definimos uma variável s que é um ponteiro para um Shark e que imprimiu um s imediatamente com o fmt.Println. Isso demonstra a primeira parte do resultado, Sammy is on the surface. Passamos o s para submerge e, então, chamamos o fmt.Println novamente com o s como seu argumento, a fim de verificar a segunda parte do resultado impresso, Sammy is underwater.

Se nós mudássemos o s para ser um Shark, em vez de um *Shark, o compilador Go produziria o erro:

Output
cannot use s (type Shark) as type Submersible in argument to submerge: Shark does not implement Submersible (Dive method has pointer receiver)

O lado bom é que o compilador Go nos diz que o Shark de fato tem um método Dive, o qual foi definido apenas no ponteiro receptor. Quando você ver essa mensagem em seu código, a solução é passar um ponteiro para o tipo de interface, usando o operador & antes da variável onde o tipo de valor estiver atribuído.

Conclusão

No final das contas, declarar os métodos no Go não é diferente de se definir as funções que recebem diferentes tipos de variáveis. Aplicam-se ao caso as mesmas regras encontradas em Trabalhando com ponteiros. O Go fornece algumas conveniências para essa definição de função extremamente comum e as coleta em conjuntos de métodos que podem ser fundamentados por tipos de interface. O uso de métodos de maneira eficaz permitirá que você trabalhe com interfaces em seu código, a fim de melhorar a capacidade de teste e deixando uma organização melhor para os futuros leitores do seu código.

Se quiser aprender mais sobre a linguagem de programação Go de maneira geral, confira nossa série de artigos sobre Como codificar em Go.

0 Comments

Creative Commons License