Uso de métodos en Go

Completado

En Go, un método es un tipo especial de función con una sencilla diferencia: se debe incluir un parámetro adicional antes del nombre de la función. Este parámetro adicional se conoce como receptor.

Los métodos son útiles cuando quiere agrupar funciones y enlazarlas a un tipo personalizado. Este enfoque en Go es similar a la creación de una clase en otros lenguajes de programación, ya que permite implementar determinadas características del modelo de programación orientada a objetos (OOP), como las de inserción, sobrecarga y encapsulación.

Para comprender la importancia de los métodos en Go, comenzará con la forma de declarar uno.

Declaración de métodos

Hasta ahora, solo ha usado las estructuras como otro tipo personalizado que puede crear en Go. En este módulo, aprenderá que, al agregar métodos, puede agregar comportamientos a las estructuras que cree.

La sintaxis para declarar un método es similar a la siguiente:

func (variable type) MethodName(parameters ...) {
    // method functionality
}

Pero antes de poder declarar un método, tiene que crear una estructura. Imagine que quiere crear un paquete de geometría y, como parte de ese paquete, decide crear una estructura de triángulo denominada triangle. Después, quiere utilizar un método para calcular el perímetro de ese triángulo. En Go puede representarlo de esta manera:

type triangle struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

La estructura tiene el aspecto normal, pero la función perimeter() tiene un parámetro adicional de tipo triangle antes del nombre de la función. Esto significa que, al usar la estructura, puede llamar a la función de esta manera:

func main() {
    t := triangle{3}
    fmt.Println("Perimeter:", t.perimeter())
}

Si intenta llamar a la función perimeter() como lo haría normalmente, no funcionará porque en su firma se indica que necesita un receptor. La única forma de llamar a ese método consiste en declarar primero una estructura, que proporciona acceso al método. Esto significa que incluso puede tener el mismo nombre para un método siempre que pertenezca a otra estructura. Por ejemplo, podría declarar una estructura square con una función perimeter(), de esta forma:

package main

import "fmt"

type triangle struct {
    size int
}

type square struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

func (s square) perimeter() int {
    return s.size * 4
}

func main() {
    t := triangle{3}
    s := square{4}
    fmt.Println("Perimeter (triangle):", t.perimeter())
    fmt.Println("Perimeter (square):", s.perimeter())
}

Al ejecutar el código anterior, observe que no hay ningún error y que obtiene la salida siguiente:

Perimeter (triangle): 9
Perimeter (square): 16

A partir de las dos llamadas a la función perimeter(), el compilador determina la que se llama en función del tipo de receptor. Esto ayuda a mantener la coherencia y los nombres cortos en las funciones entre paquetes y evita incluir el nombre del paquete como prefijo. En la unidad siguiente, cuando se describan las interfaces, se explicará por qué esto podría ser importante.

Punteros en métodos

Habrá ocasiones en las que un método necesite actualizar una variable. O bien, si el argumento del método es demasiado grande, es posible que desee evitar copiarlo. En estos casos, debe usar punteros para pasar la dirección de una variable. En un módulo anterior, al analizar los punteros, se ha afirmado que cada vez que se llama a una función en Go, Go realiza una copia de cada valor de argumento para usarlo.

El mismo comportamiento está presente cuando es necesario actualizar la variable de receptor en un método. Por ejemplo, imagine que quiere crear un método para duplicar el tamaño del triángulo. Tendrá que usar un puntero en la variable de receptor, de esta manera:

func (t *triangle) doubleSize() {
    t.size *= 2
}

Puede demostrar que el método funciona de la siguiente manera:

func main() {
    t := triangle{3}
    t.doubleSize()
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}

Al ejecutar el código anterior, debe obtener la salida siguiente:

Size: 6
Perimeter: 18

No necesita un puntero en la variable de receptor cuando el método simplemente accede a la información del receptor. No obstante, la convención de Go determina que si algún método de una estructura tiene un receptor de puntero, todos los métodos de esa estructura deben tener un receptor de puntero, aunque un método de una estructura no lo necesite.

Declaración de métodos para otros tipos

Un aspecto fundamental de los métodos consiste en definirlos para cualquier tipo, no solo para los personalizados como las estructuras. Pero no se puede definir una estructura a partir de un tipo que pertenece a otro paquete. Por tanto, no se puede crear un método en un tipo básico, como string.

Pero puede usar un truco para crear un tipo personalizado a partir de un tipo básico y usarlo como si fuera el básico. Por ejemplo, imagine que quiere crear un método para transformar una cadena de letras minúsculas a mayúsculas. Podría escribir algo como lo siguiente:

package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return strings.ToUpper(string(s))
}

func main() {
    s := upperstring("Learning Go!")
    fmt.Println(s)
    fmt.Println(s.Upper())
}

Al ejecutar el código anterior, se obtiene la salida siguiente:

Learning Go!
LEARNING GO!

Observe cómo puede usar el nuevo objeto s como si fuera una cadena la primera vez que imprime su valor. Después, al llamar al método Upper, s imprime todas las letras mayúsculas del tipo cadena.

Inserción de métodos

En un módulo anterior, ha visto que puede usar una propiedad en una estructura e insertar la misma propiedad en otra. Es decir, puede volver a usar las propiedades de una estructura para evitar la repetición y mantener la coherencia en el código base. Un concepto similar se aplica a los métodos. Puede llamar a los métodos de la estructura insertada incluso si el receptor es diferente.

Por ejemplo, imagine que quiere crear una estructura de triángulo con lógica para incluir un color. Además, quiere seguir usando la estructura de triángulo que ha declarado antes. Por tanto, la estructura de triángulo con color tendría el aspecto siguiente:

type coloredTriangle struct {
    triangle
    color string
}

Después, puede inicializar la estructura coloredTriangle y llamar al método perimeter() desde la estructura triangle (e incluso acceder a sus campos), de esta forma:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Continúe e incluya los cambios anteriores en el programa para ver cómo funciona la inserción. Al ejecutar el programa con un método main() como el anterior, debería obtener la salida siguiente:

Size: 3
Perimeter 9

Si está familiarizado con un lenguaje de programación orientada a objetos como Java o C++, es posible que piense que la estructura triangle es como una clase base y que coloredTriangle es una subclase (como la herencia), pero no es correcto. En realidad, lo que sucede es que el compilador de Go promociona el método perimeter() creando un método contenedor, que tiene un aspecto similar al siguiente:

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

Observe que el receptor es coloredTriangle, que llama al método perimeter() desde el campo de triángulo. La buena noticia es que no es necesario crear el método anterior. Podría hacerlo, pero Go lo hace de forma automática en segundo plano. El ejemplo anterior solo se ha incluido con fines de aprendizaje.

Sobrecarga de métodos

Ahora volverá al ejemplo triangle que se ha descrito antes. ¿Qué ocurre si quiere cambiar la implementación del método perimeter() en la estructura coloredTriangle? No puede tener dos funciones con el mismo nombre. Pero como los métodos necesitan un parámetro adicional (el receptor), se le permite tener un método con el mismo nombre siempre y cuando sea específico del receptor que quiere usar. Hacer uso de esta distinción es cómo sobrecarga los métodos.

Es decir, podría escribir el método contenedor que se acaba de describir si quiere cambiar su comportamiento. Si el perímetro de un triángulo coloreado es el doble del de un triángulo normal, el código sería similar al siguiente:

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

Ahora, sin cambiar nada más en el método main() que ha escrito antes, tendría el siguiente aspecto:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Al ejecutarlo, obtendrá otra salida:

Size: 3
Perimeter 18

Pero si todavía necesita llamar al método perimeter() desde la estructura triangle, puede hacerlo si accede a él de forma explícita, como se indica a continuación:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter (colored)", t.perimeter())
    fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}

Al ejecutar este código, debería obtener la salida siguiente:

Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

Como habrá observado, en Go, puede invalidar un método y todavía acceder al original si lo necesita.

Encapsulación en los métodos

La encapsulación significa que un método es inaccesible para el autor de la llamada (el cliente) de un objeto. Normalmente, en otros lenguajes de programación, se colocan las palabras clave private o public delante del nombre del método. En Go, solo tiene que usar un identificador en mayúsculas para convertir un método en público y un identificador sin mayúsculas para convertirlo en privado.

La encapsulación en Go solo surte efecto entre paquetes. Es decir, solo puede ocultar los detalles de implementación de otro paquete, no del propio paquete.

Para probarlo, cree un paquete geometry y mueva la estructura de triángulo, como se indica a continuación:

package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}

Podría usar el paquete anterior, de esta forma:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
}

Y debería obtener la salida siguiente:

Perimeter 18

Si intenta llamar al campo size o al método doubleSize() desde la función main(), el programa entrará en pánico, de esta forma:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Size", t.size)
    fmt.Println("Perimeter", t.Perimeter())
}

Al ejecutar el código anterior, se obtiene el error siguiente:

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)