One place for hosting & domains

      Definir

      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

      Definir structs en Go


      Introducción

      La capacidad para crear abstracciones en torno a detalles concretos es la mejor herramienta que un lenguaje de programación puede ofrecer a un desarrollador. Las “structs” permiten que los desarrolladores de Go describan el mundo en el que un programa de Go funciona. En vez de razonar sobre cadenas que describen una Street, una City o un PostalCode, las structs nos permiten hablar sobre una Address. Sirven como un nexo natural para la documentación en nuestros esfuerzos por indicar a desarrolladores futuros (incluidos nosotros mismos) los datos que son importantes para nuestros programas de Go y la forma en que el código futuro debería usar esos datos como corresponde. Las structs pueden definirse de varias formas diferentes. En este tutorial, veremos cada una de estas técnicas.

      Definir structs

      Las structs funcionan como formularios en papel que podría usar, por ejemplo, para declarar sus impuestos. Los formularios en papel tienen campos para datos textuales, como su nombre y apellido. A parte de los campos de texto, los formularios pueden tener casillas de verificación para indicar valores booleanos como “married” (casado) o “single” (soltero), o campos de fecha para la fecha de nacimiento. De forma similar, las structs recogen diferentes datos y los organizan con diferentes nombres de campos. Cuando inicia una variable con una nueva struct, es como si fotocopiase un formulario y lo dejase listo para completarse.

      Para crear una nueva struct, primero debe proporcionar a Go un esquema que describa los campos que esta contiene. Esta definición de struct normalmente comienza con la palabra clave type seguida por el nombre de la struct. Después de esto, utilice la palabra clave struct seguida de un par de llaves {} en el que declare los campos que tendrá la struct. Una vez que defina la struct, podrá declarar las variables que usan esta definición de struct. En este ejemplo se define y se utiliza una struct:

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

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

      output

      Sammy the Shark

      Primero definimos una struct Creature en este ejemplo, la cual contiene un campo Name de tipo string. En el cuerpo de main, creamos una instancia de Creature colocando un par de corchetes después del nombre del tipo, Creature, y luego especificando los valores para los campos de esa instancia. El campo Name de la instancia c contendrá “Sammy the Shark”. En la invocación de la función fmt.PrintIn, obtenemos los valores del campo de instancia ubicando un punto después de la variable en la que se creó la instancia y, después de esto, el nombre del campo al que queremos acceder. Por ejemplo, c.Name en este caso muestra el campo Name.

      Cuando se declara una nueva instancia de una struct, generalmente se enumeran los nombres de campos con sus valores, como en el último ejemplo. También se pueden omitir los nombres de campos, si el valor de cada campo se proporciona durante la creación de instancias de una struct, como en este ejemplo:

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

      El resultado es el mismo que el del último ejemplo:

      output

      Sammy the Shark

      Agregamos un campo a Creature para realizar un seguimiento del Type de criatura como una string. Al crear una instancia de Creature en el cuerpo de main, optamos por usar la forma de creación de instancias más corta proporcionando valores para cada campo a fin de ordenar y omitir los nombres de estos. En la declaración Creature{"Sammy", "Shark"}, el campo Name toma el valor Sammy y el campo Type toma el valor Shark porque Name aparece primero en la declaración de tipo, seguido de Type.

      Esta forma de declaración más corta tiene algunas desventajas que hicieron que la comunidad Go prefiriera la forma más larga en la mayoría de las circunstancias. Debe proporcionar valores para cada campo en la struct al utilizar la declaración corta; no puede omitir los campos que no le interesan. Esto rápidamente hace que las declaraciones cortas para las structs con muchos campos se vuelvan confusas. Por este motivo, declarar structs usando la forma corta es común con structs que tienen pocos campos.

      Los nombres de campo de los ejemplos hasta el momento comenzaron con letras mayúsculas. Esto es más importante que una preferencia estilística. El uso de letras mayúsculas o minúsculas para nombres de campos determina si el código que se ejecute en otros paquetes podrá acceder a ellos.

      Exportación de campos de struct

      Los campos de una struct siguen las mismas reglas de exportación que otros identificadores del lenguaje de programación Go. Si un nombre de campo comienza con una letra mayúscula, será legible y se podrá escribir a través de código que se encuentre fuera del paquete en el que se definió la struct. Si el campo comienza con una letra minúscula, solo el código dentro del paquete de esa struct podrá realizar tareas de lectura y escritura en ese campo. En este ejemplo definen los campos que se exportan y los que no:

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

      El resultado será el siguiente:

      output

      Sammy the Shark Password is secret

      Agregamos un campo a nuestros ejemplos anteriores: secret. secret es una string no exportada, lo cual significa que cualquier otro paquete que intente crear una instancia de una Creature no podrá acceder a su campo secret ni configurarlo. En el mismo paquete, podemos acceder a estos campos, como en este ejemplo. Ya que main está también en el paquete main, puede hacer referencia a c.password y obtener el valor almacenado allí. Es común que haya campos no exportado en structs con acceso a ellos mediado por los métodos exportados.

      Structs en línea

      Además de definir un nuevo tipo para representar una struct, puede definir una struct en línea. Estas definiciones de structs sobre la marcha son útiles en situaciones en las cuales inventar nuevos nombres para los tipos de structs sería un esfuerzo inútil. Por ejemplo, en las pruebas a menudo se utilizan un struct para definir todos los parámetros que forman un caso de prueba concreto. Sería engorroso pensar en nuevos nombres como CreatureNamePrintingTestCase cuando esa struct se utiliza en un único lugar.

      Las definiciones de structs en línea aparecen en el lado derecho de una asignación de variable. Debe proporcionar una creación de instancias de ellas de inmediato proveyendo un par adicional de corchetes con valores para cada uno de los campos que defina. En el siguiente ejemplo se muestra la definición de una struct en línea:

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

      El resultado de este ejemplo será el siguiente:

      output

      Sammy the Shark

      En vez de definir un nuevo tipo que describa nuestra struct con la palabra clave type, este ejemplo define una struct en línea disponiendo la definición de struct inmediatamente después el operador de asignación corta :=. Definimos los campos de la struct como en los ejemplos anteriores, pero luego debemos proporcionar de inmediato otro par de llaves y los valores que cada campo asumirá. Esta struct ahora se usa exactamente de la misma manera que antes; podemos hacer referencia a los nombres de campos usando la notación de puntos. Verá structs en línea declaradas con mayor frecuencia durante pruebas, ya que a menudo las structs puntuales se definen para contener datos y expectativas para un caso de prueba concreto.

      Conclusión

      Las structs son colecciones de datos heterogéneos definidas por los programadores para organizar la información. La mayoría de los programas manejan enormes volúmenes de datos, y sin structs sería difícil recordar las variables string o int que debían estar juntas o las que eran diferentes. La próxima vez que se encuentre haciendo malabares con grupos de variables, pregúntese si quizá esas variables estarían mejor agrupadas con una struct. Es posible que esas variables hayan descrito un concepto de nivel más alto todo el tiempo.



      Source link