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.
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á:
OutputSammy 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:
OutputSammy says Hello!
Esse exemplo é idêntico ao anterior. Porém, desta vez usam_os 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:
OutputSammy 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.
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:
Outputocean 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 Ocea
n 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étodolog
solicita um fmt.Stringer
como seu argumento. O erro se parece com este:
Outputsrc/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:
Outputsrc/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.
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:
OutputThe 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.
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:
OutputSammy 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:
Outputcannot 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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!