DEV Community

Go! ...objects, structs...

El lenguaje de programación Go, como vimos en la anterior entrega, se parece bastante a C, aunque con bastantes mejoras, en el sentido de evitar comportamientos indefinidos, por ejemplo. Más claro y más definido, podríamos decir.

De acuerdo, pero... al margen de programación procedural, ¿hay algún otro tipo de programación soportada por Go? ¿Concretamente, Go soporta programación orientada a objetos? Sí. Bueno, no. Más o menos. Es "algo" parecido.

Go no soporta las típicas construcciones a las que estamos acostumbrados: no existe la palabra clave class, no hay herencia como tal... De hecho, "solo" soporta estructuras, struct. Ah, pero las estructuras pueden tener métodos. Y mezclándolo con los módulos, podremos tener incluso algo más o menos parecido a constructores.

Bueno, suficientes rodeos, metámonos en el meollo.

package main
import "fmt"

type Animal struct {
    name string
    color string
    age int
}

func NewAnimal(n, c string, a int) *Animal {
    return &Animal{name: n, color: c, age: a}
}

func (a *Animal) Name() string {
    return a.name
}

func (a *Animal) Color() string {
    return a.color
}

func (a *Animal) Age() int {
    return a.age
}

func main() {
    cat := NewAnimal("Panchito", "white", 18)
    fmt.Printf("%s is %d years old.", cat.Name(), cat.Age())
}
Enter fullscreen mode Exit fullscreen mode

La salida es la esperada: Panchito is 18 years old.

La estructura Animal tiene todos sus miembros en minúsculas; esto quiere decir que todos sus miembros son privados. Al crear métodos para devolver sus valores, pero no para cambiarlos, estamos creando en esencia atributos de solo lectura.

Podemos devolver sin temor un puntero a una estructura, como hacemos en NewAnimal(). Lo que estamos diciendo es que ese objeto se va a pasar por referencia, al fin y al cabo. Es importante insistir en que no estamos devolviendo un puntero al stack de la función.

En Go, no se distingue si estamos utilizando un puntero al objeto o un objeto mismo. Para acceder al mismo utilizaremos igualmente el punto ('.') como selector de acceso a los miembros, como por ejemplo, cat.Name().

La forma de crear métodos no es meterlos dentro de la estructura, sino crearlos a continuación de esta, con el receptor de la llamada colocado antes del nombre de la función. Nunca se utiliza this o self, sino alguna abreviatura del nombre de la estructura.

No existe como tal un equivalente al método toString(), pero sí que existe la interfaz Stringer, que espera que la estructura presente un método llamado String(). Como aparece a continuación:

func (a *Animal) String() string {
    return fmt.Sprintf("%s is %s and %d years old.", a.Name(), a.Color(), a.Age())
}
Enter fullscreen mode Exit fullscreen mode

Ahora podemos cambiar la función main(), ya que print() ya espera que el objeto que se le pase tenga el método String().

func main() {
    cat := NewAnimal("Panchito", "white", 18)
    fmt.Print(cat)
}
Enter fullscreen mode Exit fullscreen mode

La salida es la que esperábamos: Panchito is white and 18 years old.

La OOP tiene como características principales encapsulación, herencia y polimorfismo. La encapsulación, ya hemos visto que la presenta, al poder unir métodos y atributos en una estructura, y además con posibilidades de visibilidad público o privado (no existe nada parecido a protected).

Así que lo siguiente es comprobar si presenta herencia. No existe ninguna sintaxis específica para esto (como extends en Java), pero sí que podemos embeber una estructura A dentro de otra B... lo que hace que los métodos de A se promocionen como métodos también de B.

type Cat struct {
    Animal
    noise string
}

func NewCat(n, c string, a int, ns string) *Cat {
    return &Cat{Animal: *NewAnimal(n, c, a), noise: ns}
}

func (c *Cat) Noise() string {
    return c.noise
}

func main() {
    cat := NewCat("Panchito", "white", 18, "Purrr...")
    fmt.Print(cat)
    fmt.Printf("\n%s", cat.Noise())
}
Enter fullscreen mode Exit fullscreen mode

Con la adición a nuestro programa de arriba, tenemos ahora un tipo más específico de animales, los gatos. Los gatos presentan todas las caracerísticas de los animales, más un ruido (el ronroneo). Como los métodos de Animal promocionan a Cat, la salida de Print() en el programa sigue siendo la misma: Panchito is white and 18 years old. Purrr..., solo que ahora añadimos una línea de texto más para la que llamamos a Cat.Noise().

Así que... sí que tenemos herencia. Pero... ¿tenemos polimorfismo? ¿Podemos llamar a un método foo() con la confianza de que se llamará a foo() del propio objeto? Resulta que sí. Si tenemos un método de mismo nombre en Animal que en Cat, el segundo oculta al primero.

func (c* Cat) String() string {
    return fmt.Sprintf("%s is %s and %d years old. Says: %s.", c.Name(), c.Color(), c.Age(), c.Noise())
}

func main() {
    cat := NewCat("Panchito", "white", 18, "Purrr...")
    fmt.Print(cat)
}
Enter fullscreen mode Exit fullscreen mode

La salida es la esperada: Panchito is white and 18 years old. Says: Purrr....

Hay salvedades, por supuesto, con respecto a lo que estamos acostumbrados a lenguajes de programación como Java, o C#. No podemos tener un puntero a Animal apuntando a un objeto Cat.

func main() {
    var cat *Animal = NewCat("Panchito", "white", 18, "Purrr...")
    fmt.Print(cat)
}
Enter fullscreen mode Exit fullscreen mode

Este programa no compila. La salida del compilador de Go es:

./prog.go:48:9: cannot use NewCat("Panchito", "white", 18, "Purrr...") (type *Cat) as type *Animal in assignment
Enter fullscreen mode Exit fullscreen mode

Veremos cómo podemos solucionar esto en una futura entrega.

¿Y qué pasa contigo? ¿Te gusta Go? ¿Es para ti el lenguaje del futuro?

Top comments (0)