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.

0 Comments

Creative Commons License