One place for hosting & domains

      Definir

      Como definir e chamar funções em Go


      Introdução

      Uma função é uma seção do código que, uma vez definida, pode ser reutilizada. As funções são usadas para deixar seu código mais fácil de entender: o código é dividido em tarefas menores e compreensíveis, que podem ser usadas mais de uma vez ao longo do programa.

      O Go vem com uma biblioteca padrão poderosa que possui várias funções predefinidas. As funções do pacote fmt, com as quais provavelmente você já está familiarizado, são:

      • fmt.Println() – que imprimirá objetos para a saída padrão (provavelmente o seu terminal).
      • fmt.Printf() – que permite que você formate seu resultado impresso.

      Os nomes das funções incluem parênteses e podem incluir parâmetros.

      Neste tutorial, vamos explicaremos sobre como definir suas próprias funções para usar em seus projetos de código.

      Definindo uma função

      Vamos começar a transformar o programa clássico “Hello, World!” em uma função.

      Criaremos um novo arquivo de texto em nosso editor de textos preferido e chamaremos o programa hello.go. Então, vamos definir a função.

      Uma função é definida usando a palavra-chave func. Depois dessa palavra-chave vem um nome de sua escolha e um conjunto de parênteses que possui quaisquer parâmetros que a função receberá (elas podem ficar vazias). As linhas de código da função ficam entre chaves {}.

      Neste caso, vamos definir uma função chamada hello():

      hello.go

      func hello() {}
      

      Isso define a instrução inicial para a criação de uma função.

      A partir daqui, adicionaremos uma segunda linha para fornecer as instruções para o que a função faz. Neste caso, vamos imprimir Hello, World! para o console:

      hello.go

      func hello() {
          fmt.Println("Hello, World!")
      }
      

      Nossa função agora está totalmente definida; porém, se executarmos o programa neste ponto, nada acontecerá, pois não chamamos a função.

      Assim, dentro do nosso bloco de funções main(), vamos chamar a função com hello():

      hello.go

      package main
      
      import "fmt"
      
      func main() {
          hello()
      }
      
      func hello() {
          fmt.Println("Hello, World!")
      }
      

      Agora, vamos executar o programa:

      Você receberá o seguinte resultado:

      Output

      Hello, World!

      Note que também introduzimos uma função chamada main(). A função main() é uma função especial que diz ao compilador que é aqui que o programa deve iniciar. Para qualquer programa que você queira que seja executável (um programa que pode ser executado a partir da linha de comando), você vai precisar de uma função main(). A função main() deve aparecer apenas uma vez, estar no pacote main() e não deve receber nem retornar argumentos. Isso permite a execução do programa em qualquer programa em Go. De acordo com o exemplo a seguir:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          fmt.Println("this is the main section of the program")
      }
      

      As funções podem ficar mais complicadas do que a função hello() que definimos. Podemos usar loops for, instruções condicionais e mais, dentro do nosso bloco de função.

      Por exemplo, a função a seguir usa uma instrução condicional para verificar se a entrada da variável name contém uma vogal; depois, ela usa um loop for para iterar nas letras da string name.

      names.go

      package main
      
      import (
          "fmt"
          "strings"
      )
      
      func main() {
          names()
      }
      
      func names() {
          fmt.Println("Enter your name:")
      
          var name string
          fmt.Scanln(&name)
          // Check whether name has a vowel
          for _, v := range strings.ToLower(name) {
              if v == 'a' || v == 'e' || v == 'i' || v == 'o' || v == 'u' {
                  fmt.Println("Your name contains a vowel.")
                  return
              }
          }
          fmt.Println("Your name does not contain a vowel.")
      }
      

      A função name() que definimos aqui define uma variável name com entrada e, em seguida, define uma instrução condicional dentro de um loop for. Isso mostra como o código pode ser organizado dentro de uma definição de função. No entanto, dependendo do que pretendemos com nosso programa e como queremos configurar nosso código, podemos querer definir a instrução condicional e o loop for como duas funções separadas.

      Definir funções dentro de um programa torna o nosso código modular e reutilizável para que possamos chamar as mesmas funções sem reescrevê-las.

      Até agora, examinamos funções com parênteses vazios, que não recebem argumentos, mas podemos definir parâmetros nas definições da função dentro de seus parênteses.

      Um parâmetro é uma entidade nomeada na definição de uma função, que especifica um argumento que a função pode aceitar. Na linguagem Go, você deve especificar o tipo de dados de cada parâmetro.

      Vamos criar um programa que repete uma palavra pelo número de vezes especificado. O programa aceitará um parâmetro de string, chamado word e um parâmetro de int chamado reps em relação ao número de vezes a se repetir a palavra.

      repeat.go

      package main
      
      import "fmt"
      
      func main() {
          repeat("Sammy", 5)
      }
      
      func repeat(word string, reps int) {
          for i := 0; i < reps; i++ {
              fmt.Print(word)
          }
      }
      

      Enviamos o valor Sammy para o parâmetro word e 5 para o parâmetro reps. Esses valores correspondem a cada parâmetro na ordem que eles foram dados. A função repeat tem um loop for que irá iterar pelo número de vezes especificado pelo parâmetro reps. Para cada iteração, o valor do parâmetro word será impresso.

      Aqui está o resultado do programa:

      Output

      SammySammySammySammySammy

      Se você tiver um conjunto de parâmetros - todos com o mesmo valor, você pode omitir, especificando o tipo a cada vez. Vamos criar um pequeno programa que recebe os parâmetros x, y e z que estão todos com valores int. Criaremos uma função que adiciona os parâmetros juntos, em configurações diferentes. As somas deles serão impressos pela função. Então, vamos chamar a função e enviar números para a função.

      add_numbers.go

      package main
      
      import "fmt"
      
      func main() {
          addNumbers(1, 2, 3)
      }
      
      func addNumbers(x, y, z int) {
          a := x + y
          b := x + z
          c := y + z
          fmt.Println(a, b, c)
      }
      

      Ao criarmos a assinatura da função para addNumbers, não precisamos especificar o tipo a cada vez, apenas no final.

      Enviamos o número 1 para o parâmetro x, o 2 para o parâmetro y e o 3 para o parâmetro z. Esses valores correspondem a cada parâmetro na ordem em que eles são dados.

      O programa está fazendo a seguinte operação matemática, com base nos valores que enviamos para os parâmetros:

      a = 1 + 2
      b = 1 + 3
      c = 2 + 3
      

      A função também imprime a, b e c e, com base nessa operação matemática, esperamos que a seja igual a 3, b igual a 4 e c igual a 5. Vamos executar o programa:

      Output

      3 4 5

      Quando enviamos 1, 2 e 3 como parâmetros para a função addNumbers(), recebemos o resultado esperado.

      Os parâmetros são argumentos, normalmente definidos como variáveis nas definições da função. Ao executar o método, você pode atribuir valores aos parâmetros, enviando os argumentos para a função.

      Retornando um valor

      Você pode enviar um valor de parâmetro para uma função e uma função também pode produzir um valor.

      Uma função pode produzir um valor com a instrução return, o qual sairá de uma função e enviará, opcionalmente, uma expressão de volta para o chamador. O tipo de dados retornados também deve ser especificado.

      Até agora, usamos a instrução fmt.Println() em vez da instrução return em nossas funções. Vamos criar um programa que, em vez de imprimir, retornará uma variável.

      Em um novo arquivo de texto chamado double.go, vamos criar um programa que duplica o parâmetro x e retorna a variável y. Nós emitimos uma chamada para imprimir a variável result, que é formada por meio da execução da função double() com um 3 enviado para ela:

      double.go

      package main
      
      import "fmt"
      
      func main() {
          result := double(3)
          fmt.Println(result)
      }
      
      func double(x int) int {
          y := x * 2
          return y
      }
      
      

      Podemos executar o programa e ver o resultado:

      Output

      6

      O número inteiro 6 é retornado como o resultado, que se trata do que esperávamos da multiplicação de 3 por 2.

      Se uma função especifica um retorno, você deve fornecer um retorno como parte do código. Se não fizer isso, receberá um erro de compilação.

      Podemos demonstrar isso, comentando a linha (do código) com a instrução de retorno:

      double.go

      package main
      
      import "fmt"
      
      func main() {
          result := double(3)
          fmt.Println(result)
      }
      
      func double(x int) int {
          y := x * 2
          // return y
      }
      
      

      Agora, vamos executar o programa novamente:

      Output

      ./double.go:13:1: missing return at end of function

      Sem usar a instrução return aqui, o programa não pode compilar.

      As funções saem imediatamente quando atingem a instrução return, mesmo se elas não estiverem no final da função:

      return_loop.go

      package main
      
      import "fmt"
      
      func main() {
          loopFive()
      }
      
      func loopFive() {
          for i := 0; i < 25; i++ {
              fmt.Print(i)
              if i == 5 {
                  // Stop function at i == 5
                  return
              }
          }
          fmt.Println("This line will not execute.")
      }
      

      Aqui, iteramos um loop for e dizemos ao loop para executar 25 iterações. No entanto, dentro do loop for, temos uma instrução condicional if que verifica se o valor de i é igual a 5. Se for, emitimos uma instrução return. Como estamos na função loopFive, qualquer return em qualquer ponto na função sairá da função. Consequentemente, nunca chegaremos à última linha dessa função para imprimir a instrução This line will not execute. (Esta linha não será executada.).

      Usar a instrução return dentro do loop for encerra a função, de modo que a linha que está fora do loop não será executada. Se, em vez disso, tivéssemos usado uma instrução break, apenas o loop teria saído naquele momento e a última linha da fmt.Println() seria executada.

      A instrução return sai de uma função e pode retornar um valor se especificado na assinatura da função.

      Retornando valores múltiplos

      Mais de um valor retornado pode ser especificado para uma função. Vamos examinar o programa repeat.go e fazer com que ele retorne dois valores. O primeiro será o valor repetido e o segundo será um erro se o parâmetro reps não for um valor maior que 0:

      repeat.go

      package main
      
      import "fmt"
      
      func main() {
          val, err := repeat("Sammy", -1)
          if err != nil {
              fmt.Println(err)
              return
          }
          fmt.Println(val)
      }
      
      func repeat(word string, reps int) (string, error) {
          if reps <= 0 {
              return "", fmt.Errorf("invalid value of %d provided for reps. value must be greater than 0.", reps)
          }
          var value string
          for i := 0; i < reps; i++ {
              value = value + word
          }
          return value, nil
      }
      

      A primeira coisa que a função repeat faz é verificar se o argumento reps é um valor válido. Qualquer valor que não seja maior do que 0 causará um erro. Como enviamos o valor de -1, essa ramificação do código será executada. Note que, quando retornamos da função, precisamos fornecer os valores retornados string e error. Como os argumentos fornecidos resultaram em um erro, vamos enviar uma string em branco de volta para o primeiro valor retornado e o erro para o segundo valor retornado.

      Na função main() podemos receber ambos valores retornados, declarando duas novas variáveis, value e err. Como pode haver um erro no retorno, vamos verificar se recebemos um erro antes de continuar com nosso programa. Neste exemplo, recebemos um erro. Nós imprimimos o erro e o return da função main() para sair do programa.

      Se não houvesse um erro, imprimiríamos o valor retornado da função.

      Nota: é considerada uma melhor prática retornar apenas dois ou três valores. Além disso, você deve sempre retornar todos os erros como o último valor de retorno de uma função.

      Executar o programa resultará no seguinte resultado:

      Output

      invalid value of -1 provided for reps. value must be greater than 0.

      Nesta seção, analisamos como podemos usar a instrução return para retornar valores múltiplos de uma função.

      Conclusão

      As funções são blocos de código de instruções que realizam ações dentro de um programa, ajudando a tornar o nosso código reutilizável e modular.

      Para aprender mais sobre como tornar seu código mais modular, leia o nosso guia sobre Como escrever pacotes em Go.



      Source link

      Usando ldflags para definir informações de versão em aplivativos Go


      Introdução

      Ao implantar aplicativos em um ambiente de produção, a compilação de binários com informações de versão e outros metadados irá melhorar seu processo de monitoramento, registro e depuração através da adição de informações de identificação para ajudar a rastrear suas compilações ao longo do tempo. Essas informações de versão com frequência podem incluir dados altamente dinâmicos, como o tempo de compilação, a máquina ou o usuário que compila o binário, o ID de confirmação do Sistema de Controle de Versão (VCS) em relação ao qual foi compilado, entre outras coisas. Como esses valores estão em constante mudança, codificar esses dados diretamente no código fonte e modificá-los antes de cada nova compilação é um processo tedioso e propenso a erros: os arquivos fonte podem mover-se e as variáveis/constantes podem trocar arquivos ao longo do desenvolvimento, interrompendo o processo de compilação.

      Uma maneira de resolver isso em Go é usando -ldflags com o comando go build para inserir informações dinâmicas no binário no momento da compilação, sem a necessidade de modificar códigos fonte. Neste identificador, o ld significa linker [vinculador], o programa que vincula os diferentes pedaços do código fonte compilado em um binário final. ldflags, então, significa *linker flags *[identificadores de vinculador]. Ele recebe esse nome porque passa um identificador para o conjunto subjacente do vinculador da cadeia de ferramentas em Go, cmd/link, que permite que você altere os valores de pacotes importados no momento da compilação a partir da linha de comando.

      Neste tutorial, você usará -ldflags para alterar o valor das variáveis no momento da compilação e introduzir suas próprias informações dinâmicas em um binário, usando um aplicativo exemplo que imprime informações de versão para a tela.

      Pré-requisitos

      Para seguir o exemplo neste artigo, será necessário:

      Compilando seu aplicativo exemplo

      Antes de poder usar ldflags para introduzir dados dinâmicos, será necessário primeiro um aplicativo no qual inserir as informações. Neste passo, você criará esse aplicativo, o qual, por enquanto,imprimirá apenas informações sobre o controle de versão estática. Vamos criar esse aplicativo agora.

      No seu diretório src, crie um diretório com o nome do seu aplicativo. Este tutorial usará o nome de aplicativo app:

      Mude seu diretório de trabalho para essa pasta:

      Em seguida, usando o editor de texto de sua escolha, crie o ponto de entrada do seu programa, main.go:

      Agora, faça seu aplicativo imprimir informações de versão, adicionando o seguinte conteúdo:

      app/main.go

      package main
      
      import (
          "fmt"
      )
      
      var Version = "development"
      
      func main() {
          fmt.Println("Version:t", Version)
      }
      

      Dentro da função main(), você declarou a variável Version, em seguida imprimiu a string Version:, seguida de um caractere de guia (tab), t e, na sequência declarou a variável.

      Neste ponto, a variável Version foi definida como development, a qual será a versão padrão desse app. Mais tarde, você trocará esse valor por um número oficial de versão, organizado segundo o formato semântico para controle de versão.

      Salve e saia do arquivo. Assim que terminar, compile e execute o aplicativo para confirmar que ele imprime a versão correta:

      Você verá o seguinte resultado:

      Output

      Agora, você tem um aplicativo que imprime informações da versão padrão, mas ainda não tem como enviar as informações da versão atual no momento da compilação. No próximo passo, você usará -ldflags e go build para resolver esse problema.

      Assim como mencionado anteriormente, ldflags significa identificadores de vinculador e é usado para enviar identificadores para o vinculador subjacente na cadeia de ferramentas Go. Isso funciona de acordo com a seguinte sintaxe:

      • go build -ldflags="-flag"

      Nesse exemplo, transmitimos o flag para o comando go tool link subjacente que executa como parte do go build. Esse comando usa aspas duplas ao redor do conteúdo transmitido para os ldflags para evitar quebrar caracteres nele, ou caracteres que a linha de comando possa interpretar como algo diferente do que queremos. A partir daqui, você poderia enviar muitos e diferentes identificadores de link. Para os fins deste tutorial, usaremos o identificador -X para gravar informações na variável no momento de vincular, seguido do caminho do pacote até a variável e seu novo valor:

      • go build -ldflags="-X 'package_path.variable_name=new_value'"

      Dentro das aspas, há agora a opção -X e um par chave-valor que representa a variável a ser alterada e seu novo valor. O caractere . separa o caminho do pacote e o nome da variável e aspas únicas são usadas para evitar a quebra de caracteres no par chave-valor.

      Para substituir a variável Version no seu aplicativo exemplo, use a sintaxe no último bloco de comando para enviar um novo valor e compilar o novo binário:

      • go build -ldflags="-X 'main.Version=v1.0.0'"

      Neste comando, main é o caminho de pacote da variável Version, uma vez que essa variável está no arquivo main.go. Version é a variável para a qual está gravando, e o v1.0.0 é o novo valor.

      Para usar o ldflags, o valor que você quiser alterar deve existir e ser uma variável do nível de pacote do tipo string. Essa variável pode ser exportada ou não exportada. O valor não pode ser const ou ter seu valor definido pelo resultado de uma chamada de função. Felizmente, Version se encaixa em todos esses requisitos: ela já foi declarada como variável no arquivo main.go, assim como o valor atual (development) e o valor desejado (v1.0.0) são ambos strings.

      Assim que seu novo binário app for compilado, execute o aplicativo:

      Você receberá o seguinte resultado:

      Output

      Usando -ldflags, você mudou com sucesso a variável Version de development para v1.0.0.

      Agora, você modificou uma variável string dentro de um aplicativo simples na hora da compilação. Usando ldflags, você pode inserir detalhes de versão, informações de licenciamento e outras coisas em um binário pronto para a distribuição, usando apenas a linha de comando.

      Neste exemplo, a variável que você mudou estava no programa main, o que reduz a dificuldade em determinar o nome do caminho. No entanto, as vezes, o caminho para essas variáveis é mais complicado de se encontrar. No próximo passo, você irá gravar valores para as variáveis nos subpacotes para demonstrar a melhor maneira de se determinar caminhos de pacotes mais complexos.

      Concentrando-se em variáveis para subpacotes

      Na última seção, você manipulou a variável Version, a qual estava no pacote de nível superior do aplicativo. Mas esse não é sempre o caso. Com frequência, é mais prático colocar essas variáveis em outro pacote, já que o main não é um pacote importável. Para simular isso em seu aplicativo exemplo, você criará um novo subpacote, app/build que armazenará informações sobre a hora em que o binário foi compilado e o nome do usuário que emitiu o comando de compilação.

      Para adicionar um novo subpacote, adicione primeiro um novo diretório ao seu projeto chamado build:

      Depois, crie um novo arquivo chamado build.go para reter as novas variáveis:

      No seu editor de texto, adicione novas variáveis para Time e User:

      app/build/build.go

      package build
      
      var Time string
      
      var User string
      

      A variável Time reterá uma representação de string da hora em que o binário foi compilado. A variável User reterá o nome do usuário que compilou o binário. Como essas duas variáveis sempre terão valores, você não precisa inicializar essas variáveis com valores padrão como você fez para Version.

      Salve e saia do arquivo.

      Em seguida, abra o main.go para adicionar essas variáveis ao seu aplicativo:

      Dentro de main.go, adicione as seguintes linhas destacadas:

      main.go

      package main
      
      import (
          "app/build"
          "fmt"
      )
      
      var Version = "development"
      
      func main() {
          fmt.Println("Version:t", Version)
          fmt.Println("build.Time:t", build.Time)
          fmt.Println("build.User:t", build.User)
      }
      

      Nessas linhas, você importou primeiro o pacote app/build e, então, imprimiu build.Time e build.User da mesma forma que imprimiu Version.

      Salve o arquivo e depois saia do seu editor de texto.

      Em seguida, para atingir essas variáveis com o ldflags, você poderia usar o caminho de importação app/build seguido de . User ou . Time, uma vez que você já sabe o caminho de importação. No entanto, para simular uma situação mais complexa na qual o caminho para a variável não é evidente, em vez disso, vamos usar o comando nm na cadeia de ferramentas Go.

      O comando go tool nm dará como resultado os símbolos envolvidos em um dado executável, arquivo de objeto ou arquivo. Neste caso, um símbolo se refere a um objeto no código, como uma variável ou função definida ou importada. Ao gerar uma tabela de símbolos com nm e usar o grep para procurar por uma variável, você pode rapidamente encontrar informações sobre seu caminho.

      Nota: o comando nm não ajudará você a encontrar o caminho da sua variável se o nome do pacote tiver qualquer caractere não ASCII ou um caractere " ou %, já que essa é uma limitação da ferramenta em si.

      Para usar esse comando, compile primeiro o binário para app:

      Agora que o app foi compilado, aponte a ferramenta nm para ele e examine o resultado:

      • go tool nm ./app | grep app

      Quando executada, a ferramenta nm gerará muitos dados como resultado. Por isso, o comando anterior usou o símbolo | para canalizar o resultado até o comando grep, o qual, na sequência, pesquisou termos que tivessem app de nível elevado no título.

      Você irá receber um resultado parecido com este:

      Output

      55d2c0 D app/build.Time 55d2d0 D app/build.User 4069a0 T runtime.appendIntStr 462580 T strconv.appendEscapedRune . . .

      Neste caso, as duas primeiras linhas do conjunto de resultados contêm os caminhos para as duas variáveis que você está procurando: app/build.Time e app/build.User.

      Agora que você conhece os caminhos, compile o aplicativo novamente, desta vez alterando Version, User e Time no momento de compilar. Para tanto, passe vários identificadores -X para os -ldflags:

      • go build -v -ldflags="-X 'main.Version=v1.0.0' -X 'app/build.User=$(id -u -n)' -X 'app/build.Time=$(date)'"

      Aqui, você enviou o comando Bash id -u -n para listar o usuário atual e o comando date para listar a data atual.

      Assim que o executável estiver compilado, execute o programa:

      Esse comando, quando executado em um sistema Unix, gerará um resultado similar ao seguinte:

      Output

      Version: v1.0.0 build.Time: Fri Oct 4 19:49:19 UTC 2019 build.User: sammy

      Agora, você tem um binário que contém informações do controle de versão e compilação que podem fornecer assistência vital na produção ao resolver problemas.

      Conclusão

      Este tutorial mostrou como, quando aplicado corretamento, o ldflags pode ser uma ferramenta poderosa para injetar informações valiosas em binários no momento da compilação. Dessa forma, você pode controlar identificadores de recursos, informações de ambiente, informações de controle de versão e outras coisas, sem introduzir alterações no seu código fonte. Ao adicionar ldflags ao seu fluxo de trabalho de compilação atual, você pode maximizar os benefícios do formato de distribuição binária independente do Go.

      Se quiser aprender mais sobre a linguagem de programação Go, confira toda a nossa série sobre Como codificar em Go. Se estiver procurando mais soluções para controle de versão, teste nosso guia de referência Como usar o Git.



      Source link

      Definir métodos en Go


      Introducción

      Las funciones le permiten organizar la lógica en procedimientos repetibles que pueden usar diferentes argumentos cada vez que se ejecutan. Durante la definición de las funciones, a menudo observará que varias funciones pueden funcionar sobre los mismos datos en cada ocasión. Go reconoce este patrón y le permite definir funciones especiales, llamadas métodos, cuya finalidad es operar sobre instancias de algún tipo específico, conocidas como receptores. Añadir métodos a los tipos le permite comunicar no solo lo que representan los datos, sino también cómo deberían usarse.

      Definir un método

      La sintaxis para definir un método es similar a la que se usa para definir una función. La única diferencia es la adición de un parámetro después de la palabra clave func para especificar el receptor del método. El receptor es una declaración del tipo en el que desea definir el método. El siguiente ejemplo define un método sobre un tipo estructura:

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

      Si ejecuta este código, el resultado será el siguiente:

      Output

      Sammy says Hello!

      Creamos un “struct” llamado Creature con campos string para un Name y un Greeting. Este Creature tiene un único método definido, Greet. En la declaración del receptor, asignamos la instancia de Creature a la variable c para poder hacer referencia a los campos de Creature a medida que preparamos el mensaje de saludo en fmt.Printf.

      En otros lenguajes, normalmente se hace referencia al receptor de las invocaciones del método mediante una palabra clave (por ejemplo, this o self). Go considera que el receptor es una variable como cualquier otra, de modo que puede darle el nombre que usted prefiera. El estilo preferido por la comunidad para este parámetro es una versión en minúsculas del primer carácter del tipo receptor. En este ejemplo, usamos c porque el tipo receptor era Creature.

      En el cuerpo de main, creamos una instancia de Creature y especificamos los valores para sus campos Name y Greetings. Invocamos el método Greet aquí uniendo el nombre del tipo y el nombre del método con . y proporcionando la instancia de Creature como primer argumento.

      Go ofrece otra forma más conveniente de invocar métodos en instancias de un struct, como se muestra en este ejemplo:

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

      Si ejecuta esto, el resultado será el mismo que en el ejemplo anterior:

      Output

      Sammy says Hello!

      Este ejemplo es idéntico al anterior, pero esta vez usamos notación de puntos para invocar el método Greet usando el Creature guardado en la variable sammy como el receptor. Esta es una notación abreviada para la invocación de función del primer ejemplo. En la biblioteca estándar y la comunidad de Go se prefiere este estilo a tal extremo que en raras ocasiones verá el estilo de invocación de función previamente mostrado.

      En el siguiente ejemplo, se muestra un motivo por el cual la notación de puntos es más frecuente:

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

      Si ejecuta este código, el resultado tiene este aspecto:

      Output

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

      Modificamos los ejemplos anteriores para introducir otro método llamado SayGoodbye y también cambiamos Greet para que muestre Creature, de modo que podamos invocar métodos adicionales en esa instancia. En el cuerpo de main, invocamos los métodos Greet y SayGoodbye en la variable sammy primero usando la notación de puntos y luego usando el estilo de invocación funcional.

      Los resultados de ambos estilos son los mismos, pero el ejemplo en el que se utiliza la notación de punto es mucho más legible. La cadena de puntos también nos indica la secuencia en la cual se invocarán los métodos, mientras que el estilo funcional invierte esta secuencia. La adición de un parámetro a la invocación SayGoodbye oculta más el orden de las invocaciones del método. La claridad de la notación de puntos es el motivo por el cual es el estilo preferido para invocar métodos en Go, tanto en la biblioteca estándar como entre los paquetes externos que encontrará en el ecosistema de Go.

      La definición de métodos en tipos, en contraposición la definición de funciones que operan en algún valor, tiene otra relevancia especial en el lenguaje de programación Go. Los métodos son el concepto principal que subyace a las interfaces.

      Interfaces

      Cuando define un método en cualquier tipo en Go, ese método se añade al conjunto de métodos del tipo. El conjunto de métodos es el grupo de funciones asociadas con ese tipo como métodos y el compilador de Go los utiliza para definir si algún tipo puede asignarse a una variable con un tipo de interfaz. Un tipo de interfaz es una especificación de métodos usados por el compilador para garantizar que un tipo proporcione implementaciones para esos métodos. Se dice que cualquier tipo que tenga métodos con el mismo nombre, los mismos parámetros y los mismos valores de retorno que los que se encuentran en la definición de una interfaz_ implementan _esa interfaz y pueden asignarse a variables con ese tipo de interfaz. La siguiente es la definición de la interfaz fmt.Stringer de la biblioteca estándar:

      type Stringer interface {
        String() string
      }
      

      Para que un tipo implemente la interfaz fmt.Stringer, debe proporcionar un método String() que muestre una string. Implementar esta interfaz permitirá imprimir su tipo exactamente como lo desee (a veces esto se denomina “pretty-printed”) cuando pasa las instancias de su tipo a las funciones definidas en el paquete fmt. En el siguiente ejemplo, se define un tipo que implementa esta interfaz:

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

      Cuando ejecute el código, verá este resultado:

      Output

      ocean contains : sea urchin, lobster, shark

      En este ejemplo se define un nuevo tipo de struct llamado Ocean. Se dice que Ocean implementa la interfaz fmt-Stringer porque Ocean define un método llamado String, que no toma ningún parámetro y muestra una string. En main, definimos un nuevo Ocean y lo pasamos a una función log, que toma una string para imprimir primero, seguida de cualquier elemento que implemente fmt.Stringer. El compilador de Go nos permite pasar o aquí porque Ocean implementa todos los métodos solicitados por fmt.Stringer. En log usamos fmt.PrintIn, que invoca el método String de Ocean cuando encuentra un fmt.Stringer como uno de sus parámetros.

      Si Ocean no proporcionara un método String(), Go produciría un error de compilación, porque el método log solicita un fmt.Stringer como su argumento. El error tiene este aspecto:

      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)

      Go también garantizará que el método String() proporcionado coincida exactamente con el solicitado por la interfaz fmt.Stringer. Si no es así, producirá un error similar a 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

      En los ejemplos analizados hasta el momento, definimos métodos en el receptor del valor. Es decir, si usamos la invocación funcional de métodos, el primer parámetro, que se refiere al tipo en el cual el método se definió, será un valor de ese tipo en vez de un puntero. Por lo tanto, cualquier modificación que realicemos a la instancia proporcionada al método se descartará cuando el método complete la ejecución, ya que el valor recibido es una copia de los datos. También es posible definir métodos sobre el receptor de punteros de un tipo.

      Receptores de punteros

      La sintaxis para definir métodos en el receptor de punteros es casi idéntica a los métodos de definición en el receptor de valores. La diferencia radica en crear un prefijo en el nombre del tipo de la declaración del receptor con un asterisco (*). En el siguiente ejemplo, se define un método sobre el receptor de punteros para un 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()
      }
      

      Verá el siguiente resultado cuando ejecute este ejemplo:

      Output

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

      En este ejemplo, se definió un tipo Boat con un Name y occupants. Queremos introducir de forma forzosa código en otros paquetes para añadir únicamente ocupantes con el método AddOccupant; por lo tanto, hicimos que el campo occupants no se exporte poniendo en minúsculas la primera letra del nombre del campo. También queremos controlar que la invocación de AddOccupant haga que la instancia de Boat se modifique, por eso definimos AddOccupant en el receptor de punteros. Los punteros actúan más como una referencia a una instancia específica de un tipo que como una copia de ese tipo. Saber que AddOccupant se invocará usando un puntero a Boat garantiza que cualquier modificación persista.

      En main, definimos una nueva variable, b, que tendrá un puntero a Boat (*Boat). Invocamos el método AddOccupant dos veces en esta instancia para añadir dos pasajeros. El método Manifest se define en el valor Boat, porque en su definición, el receptor se especifica como (b Boat). En main, aún podemos invocar Manifest porque Go puede eliminar la referencia del puntero de forma automática para obtener el valor Boat. b.Manifest() aquí es equivalente a (*b). Manifest().

      Si un método se define en un receptor de punteros o en un receptor de valor tiene implicaciones importantes cuando se intenta asignar valores a variables que son tipos de interfaz.

      Receptores de punteros e interfaces

      Cuando se asigne un valor a una variable con un tipo de interfaz, el compilador de Go examinará el conjunto de métodos del tipo que se asigna para garantizar que tenga los métodos previstos por la interfaz. Los conjuntos de métodos para el receptor de punteros y el receptor de valores son diferentes porque los métodos que reciben un puntero pueden modificar sus receptores, mientras que aquellos que reciben un valor no pueden hacerlo.

      En el siguiente ejemplo, se demuestra la definición de dos métodos: uno en el receptor de punteros de un tipo y otro en su receptor de valores. Sin embargo, solo el receptor de punteros podrá satisfacer los requisitos de la interfaz también definida en este ejemplo:

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

      Cuando ejecute el código, verá este resultado:

      Output

      Sammy is on the surface Sammy is underwater

      En este ejemplo, se definió una interfaz llamada Submersible que prevé tipos que tengan un método Dive(). A continuación definimos un tipo Shark con un campo Name y un método isUnderwater para realizar un seguimiento del estado de Shark. Definimos un método Dive() en el receptor de punteros de Shark que cambió el valor de isUnderwater a true. También definimos el método String() del receptor de valores para que pudiera imprimir correctamente el estado de Shark con fmt.PrintIn usando la interfaz fmt.Stringer aceptada por fmt.PrintIn que observamos antes. También usamos una función submerge que toma un parámetro Submersible.

      Usar la interfaz Submersible en vez de *Shark permite que la función submerge dependa solo del comportamiento proporcionado por un tipo. Esto hace que la función submerge sea más reutilizable porque no tendrá que escribir nuevas funciones submerge para Submarine, Whale o cualquier otro elemento de tipo acuático que aún no se nos haya ocurrido. Siempre que definamos un método Dive(), puede usarse con la función submerge.

      En main, definimos una variable s que es un puntero de un Shark y se imprime inmediatamente s con fmt.PrintIn. Esto muestra la primera parte del resultado, Sammy is on the surface. Pasamos s a submerge y luego invocamos fmt.PrintIn de nuevo con s como argumento para ver la segunda parte del resultado impresa: Sammy is underwater.

      Si cambiamos s para que sea Shark en vez de *Shark, el compilador de Go generará un error:

      Output

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

      El compilador de Go indica que Shark no tiene un método Dive, solo se define en el receptor de punteros. Cuando ve este mensaje en su propio código, la solución es pasar un puntero al tipo de interfaz usando el operador & antes de la variable en la que se asignó el tipo de valor.

      Conclusión

      Declarar métodos en Go no difiere de definir funciones que reciben diferentes tipos de variables. Se aplican las mismas reglas que para trabajar con punteros. Go proporciona algunas utilidades para esta definición de funciones extremadamente común y las recoge en conjuntos de métodos que pueden probarse a través de tipos de interfaz. Usar los métodos de forma efectiva le permitirá trabajar con interfaces en su código para mejorar su capacidad de prueba y proporciona una mejor organización para los lectores futuros de su código.

      Si desea obtener más información acerca del lenguaje de programación Go en general, consulte nuestra serie Cómo escribir código en Go.



      Source link