One place for hosting & domains

      Definindo

      Definindo métodos em Go


      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.



      Source link

      Definindo structs no Go


      Introdução

      Compilar abstrações em volta de detalhes concretos é a maior ferramenta que uma linguagem de programação pode dar a um desenvolvedor. O struct permite que os desenvolvedores do Go descrevam o mundo no qual um programa em Go opera. Em vez de pensar em strings descrevendo uma Street (Rua), City (Cidade) ou um PostalCode (CEP), os structs nos permitem falar sobre um Address (Endereço). Eles servem como uma ligação natural com a documentação em nossos esforços para dizer aos futuros desenvolvedores (incluindo nós mesmos) quais dados são importantes para nossos programas em Go e como o código futuro deve usar esses dados de maneira correta. Os structs podem ser definido e usados de diferentes maneiras. Neste tutorial, vamos dar uma olhada em cada uma dessas técnicas.

      Definindo Structs

      Os Structs funcionam como formulários de papel que você pode usar, por exemplo, para registrar seus impostos. Os formulários de papel podem ter campos para informações de texto como seu nome e sobrenome. Além dos campos de texto, os formulários podem ter caixas de seleção para indicar valores booleanos, como “casado” ou “solteiro”, ou campos de datas para a data de nascimento. De igual modo, os structs coletam diferentes dados e os organizam sob diferentes nomes de campo. Quando você inicializa uma variável com um novo struct, é como se você tivesse feito uma cópia de um formulário e o deixou pronto para preencher.

      Para criar um novo struct, você deve primeiro dar ao Go um plano gráfico que descreva os campos do struct. Essa definição do struct geralmente começa com a palavra-chave type, seguida pelo nome do struct. Após isso, utilize a palavra-chave struct, seguida de um par de chaves {}, onde você declara os campos que o struct irá conter. Assim que definir o struct, você poderá, então, declarar as variáveis que usam essa definição de struct. Este exemplo define um struct e o usa:

      package main
      
      import "fmt"
      
      type Creature struct {
          Name string
      }
      
      func main() {
          c := Creature{
              Name: "Sammy the Shark",
          }
          fmt.Println(c.Name)
      }
      

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

      output

      Sammy the Shark

      Neste exemplo, primeiro definimos um struct Creature, contendo um campo Name do tipo string. Dentro do corpo do main, criamos uma instância de Creature, colocando um par de chaves após o nome do tipo, Creature e, em seguida, especificamos os valores para os campos daquela instância. A instância em c terá seu campo Name configurado para “Sammy the Shark”(Sammy, o tubarão). Dentro da invocação da função fmt.Println, recuperamos os valores do campo da instância, colocando um ponto após a variável em que a instância foi criada, seguida pelo nome do campo que gostaríamos de acessar. Por exemplo, o c.Name, neste caso, retorna o campo Name.

      Ao declarar uma nova instância de um struct, você geralmente enumera os nomes de campo com seus valores, como no último exemplo. De forma alternativa, se cada valor de campo for fornecido durante a instanciação de um struct, você pode omitir os nomes de campo, como neste exemplo:

      package main
      
      import "fmt"
      
      type Creature struct {
          Name string
          Type string
      }
      
      func main() {
          c := Creature{"Sammy", "Shark"}
          fmt.Println(c.Name, "the", c.Type)
      }
      

      O resultado é o mesmo que o do último exemplo:

      output

      Sammy the Shark

      Adicionamos um campo extra para a Creature rastrear o Type de criatura como uma string. Ao instanciar a Creature dentro do corpo do main, optamos pelo uso do formulário de instanciação mais curto, fornecendo valores para cada campo em ordem e omitindo seus nomes de campo. Na declaração Creature{"Sammy", "Shark"}, o campo Name recebe o valor Sammy e o campo Type recebe o valor Shark, pois o Name aparece primeiro na declaração de tipo, seguido pelo Type.

      Este formulário de declaração mais curto tem algumas desvantagens que levaram a comunidade Go a preferir o formulário mais longo, na maioria das circunstâncias. Você deve fornecer os valores de cada campo no struct ao usar a declaração curta — você não pode ignorar os campos com os quais não se importa. Isso faz com que as declarações curtas para structs com muitos campos se tornem confusas rapidamente. Por esse motivo, declarar os structs usando o formulário curto é normalmente usado com structs que têm poucos campos.

      Até agora, os nomes de campos nos exemplos começaram todos com letras maiúsculas. Isso é mais significativo do que uma preferência estilística. O uso de letras maiúsculas ou minúsculas para os nomes dos campos afeta a acessibilidade dos seus nomes de campos pelos códigos em execução em outros pacotes.

      Exportação do campo de struct

      Os campos de um struct seguem as mesmas regras de exportação que outros identificadores dentro da linguagem de programação Go. Se um nome de campo iniciar com uma letra maiúscula, ele será legível e gravável pelo código fora do pacote onde o struct foi definido. Se o campo iniciar com uma letra minúscula, apenas o código dentro do pacote do struct poderá ler e gravar naquele campo. Este exemplo define os campos que são exportados e os que não são:

      package main
      
      import "fmt"
      
      type Creature struct {
          Name string
          Type string
      
          password string
      }
      
      func main() {
          c := Creature{
              Name: "Sammy",
              Type: "Shark",
      
              password: "secret",
          }
          fmt.Println(c.Name, "the", c.Type)
          fmt.Println("Password is", c.password)
      }
      

      Isso resultará em:

      output

      Sammy the Shark Password is secret

      Acrescentamos um campo adicional aos nossos exemplos anteriores, secret.secret, que é um campo de string não exportado, o que significa que qualquer outro pacote que tentar instanciar uma Creature não poderá acessar ou definir seu campo secret. Dentro do mesmo pacote, podemos acessar esses campos, como esse exemplo fez. Como o main também está no pacote main, ele pode referir o c.password e recuperar o valor armazenado lá. É comum ter campos não exportados em structs com acesso a eles e mediados por métodos exportados.

      Structs em linha

      Além de definir um novo tipo para representar um struct, você também pode definir um struct em linha. Essas definições de struct dinâmicas são úteis em situações onde a criação de novos nomes para tipos de struct seriam um esforço perdido. Por exemplo, com frequência os testes usam um struct para definir todos os parâmetros que compõem um caso de teste em particular. Seria complicado inventar novos nomes como CreatureNamePrintingTestCase quando esse struct é usado em apenas um lugar.

      As definições de struct em linha aparecem à direita de uma atribuição de variável. Você deve fornecer uma instanciação deles imediatamente após, fornecendo um par de chaves adicional com os valores de cada um dos campos que você definir. O exemplo seguinte mostra uma definição de um struct em linha:

      package main
      
      import "fmt"
      
      func main() {
          c := struct {
              Name string
              Type string
          }{
              Name: "Sammy",
              Type: "Shark",
          }
          fmt.Println(c.Name, "the", c.Type)
      }
      

      O resultado deste exemplo será:

      output

      Sammy the Shark

      Em vez de definir um novo tipo descrevendo nosso struct com a palavra-chave type, esse exemplo define um struct em linha colocando a definição do struct imediatamente após o operador de atribuição curto, :=. Definimos os campos do struct como nos exemplos anteriores, mas devemos fornecer imediatamente outro par de chaves e os valores que cada campo irá assumir. Agora, o uso desse struct ficou exatamente como era antes — podemo-nos referir aos nomes de campo usando a notação de ponto. O lugar mais comum em que você verá os structs declarados em linha será durante testes, já que os structs únicos são definidos para conter dados e expectativas de um caso de teste em particular.

      Conclusão

      Os structs são coleções de dados heterogêneos, definidos pelo programadores para organizar informações. A maioria dos programas lida com volumes de dados imensos e, sem structs, seria difícil lembrar quais variáveis de string ou int pertenciam juntas ou quais eram diferentes. Da próxima vez que você se encontrar manipulando grupos de variáveis, pergunte a si mesmo se essas variáveis ficariam melhor agrupadas usando um struct. Essas variáveis podem estar descrevendo um conceito de nível superior desde o começo.



      Source link