Esta pagina se ve mejor con JavaScript habilitado

Go interfaces

 ·  🎃 kr0m

En Go, una interfaz es un conjunto de métodos que un tipo debe implementar.

A diferencia de algunos lenguajes que requieren que una clase explícitamente declare que implementa una interfaz. En Go, la implementación de una interfaz es implícita, si un tipo implementa los métodos requeridos por una interfaz, se considera que implementa esa interfaz automáticamente.

Las interfaces permiten la implementación del polimorfismo, lo que significa que una misma función de interfaz puede ser llamada usando diferentes tipos, proporcionando un comportamiento distinto según el tipo.

Además proporcionan una forma de abstracción, ocultando los detalles de implementación y centrándose en la funcionalidad esencial que debe proporcionar un objeto.

Este artículo es bastante extenso así que lo he organizado de la siguiente manera:


Si eres nuevo en el mundo de Go te recomiendo los siguientes artículos anteriores:


Ejemplo con interfaz:

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type geometry interface {
    area() float64
    perimeter() float64
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    fmt.Println("-----")
    measure(c)
}

Mismo código sin interfaz:

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

func (r rectangle) measure_r() {
    fmt.Println(r)
    fmt.Println(r.area())
    fmt.Println(r.perimeter())
}

func (c circle) measure_c() {
    fmt.Println(c)
    fmt.Println(c.area())
    fmt.Println(c.perimeter())
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    r.measure_r()
    fmt.Println("-----")
    c.measure_c()
}

Ejecución del código:

Con interfaz:

{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793

Sin interfaz:

{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793

Explicación:

Cualquier objeto que tenga las funciones area() y perimeter() se considera un interfaz de tipo geometry.

type geometry interface {
    area() float64
    perimeter() float64
}

En este caso las estructuras rectangle y circle tienen dichas funciones debido a los métodos de go

De este modo el código resulta mas genérico ya que solo hay que llamar a la función measure(), sin tener que preocuparse de si es un rectángulo: r.measure_r() o un circulo: c.measure_c().

Desde main siempre se llama a la misma función, tan solo debemos preocuparnos por implementar las funciones requeridas por la interfaz(area/perimeter) para cada forma.

Además la función measure() solo podrá utilizarse cuando se le pase un objeto que cumpla la interfaz, ya que así lo requiere como parámetro de entrada:

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

Objetos de tipo interfaz:

Otra forma de implementar el mismo código con interfaces pero sin utilizar una función es generar objetos de tipo interfaz, de este modo la asignación solo será posible si el objeto origen cumple con la interfaz. Veamos el ejemplo anterior.

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type geometry interface {
    area() float64
    perimeter() float64
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    var g geometry = r
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
    fmt.Println("-----")
    g = c
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}
{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793

Cuidado con los objetos nulos, ya que Go permite definirlos pero si ejecutamos alguno de sus métodos el software explotará con un panic.

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type geometry interface {
    area() float64
    perimeter() float64
}

func main() {
    // Empty geometry object
    var g geometry
    fmt.Println(g.area())
}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x47a210]

goroutine 1 [running]:
main.main()
	/home/kr0m/test/test.go:36 +0x10
exit status 2

Array de tipo interfaz como entrada de una función:

Ahora supongamos que queremos pasar un slice de geometrías para sumar las areas y perimetros.

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type geometry interface {
    area() float64
    perimeter() float64
}

func sumMeasures(geometryArray []geometry) {
    totalArea := 0.0
    totalPerimeter := 0.0

    for _, geometryObject := range geometryArray {
        totalArea += geometryObject.area()
        totalPerimeter += geometryObject.perimeter()
    }
    fmt.Println("totalArea: ", totalArea)
    fmt.Println("totalPerimeter: ", totalPerimeter)
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    // geometryArray is an slice of geometry interface type
    geometryArray := []geometry{r, c}
    sumMeasures(geometryArray)
}

Podemos ver que la función ahora requiere un slice de geometry:

func sumMeasures(geometryArray []geometry) {

Por lo tanto antes de llamar a la función, creamos dicho slice:

    geometryArray := []geometry{r, c}

Al ejecutar el código obtendremos la siguiente salida:

totalArea:  90.53981633974483
totalPerimeter:  45.41592653589793

Inclusión de interfaces desde otra interfaz:

Las interfaces pueden incluir otras interfaces por ejemplo podríamos haber definido una interfaz area otra perimeter y finalmente geometry que incluya a las dos anteriores:

type area interface {
    area() float64
}
type perimeter interface {
    perimeter() float64
}
type geometry interface {
    area
    perimeter
}

El código final quedaría del siguiente modo:

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type area interface {
    area() float64
}

type perimeter interface {
    perimeter() float64
}

type geometry interface {
    area
    perimeter
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    fmt.Println("-----")
    measure(c)
}

Ejecutamos el código.

{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793

Restricción de métodos al cumplir con una interfaz:

Siguiendo con el ejemplo anterior, implementemos un nuevo método que calcule el diámetro del círculo.

func (c circle) diameter() float64 {
    return c.radius * 2
}

El código final quedaría del siguiente modo:

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}

type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}
// EXTRA METHOD
func (c circle) diameter() float64 {
    return c.radius * 2
}

type geometry interface {
    area() float64
    perimeter() float64
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    fmt.Println("-----")
    measure(c)
}

Ahora el círculo implementa tres métodos, area(), perimeter() y diameter(). Pero la función measure sigue requiriendo la interfaz geometry la cual solo requiere las funciones area() y perimeter() lo que provocará que el método diameter() sea excluído dentro de la función measure.

Si modificamos la función measure() para que utilice el método diameter(), fallará con el siguiente error:

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
    fmt.Println(g.diameter())
}
./04.go:43:19: g.diameter undefined (type geometry has no field or method diameter)

Go assertion:

Continuando con el ejemplo anterior, si necesitamos acceder a dicho método se puede utilizar lo que Go llama assertions, esto consiste en crear un objeto nuevo e invocar el objeto_original.(tipo), el assert retornará un objeto con todos los métodos asociados al tipo y un ok que indicará si el objeto es de dicho tipo.

Con un ejemplo lo veremos mucho más fácilmente.

package main

import (
    "fmt"
    "math"
)

type rectangle struct {
    width, height float64
}

type circle struct {
    radius float64
}

func (r rectangle) area() float64 {
    return r.width * r.height
}
func (r rectangle) perimeter() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}
// EXTRA METHOD
func (c circle) diameter() float64 {
    return c.radius * 2
}

type geometry interface {
    area() float64
    perimeter() float64
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perimeter())
    // NEW CODE
    if circle, ok := g.(circle); ok {
        fmt.Println(circle.diameter())
    }
}

func main() {
    r := rectangle{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    fmt.Println("-----")
    measure(c)
}

Ahora al ejecutar el código cuando llegue a la función measure se comprobará si la forma geométrica es de tipo círculo y de ser así se imprimirá también el diámetro.

{3 4}
12
14
-----
{5}
78.53981633974483
31.41592653589793
10

Puntero como receiver en un método de una interfaz:

Cuando un método requerido por un interfaz tiene como receiver un puntero, tendremos que crear una variable del tipo que cumpla con el método de la interfaz(s) y luego asignar a la variable de tipo interfaz(t) la dirección de memoria de la primera variable(s).

package main

import "fmt"

type toggleable interface {
    toggle()
}

type switchControl string
func (s *switchControl) toggle() {
    if *s == "on" {
        *s = "off"
    } else {
        *s = "on"
    }
    fmt.Println(*s)
}

func main() {
    var t toggleable
    s := switchControl("off")
    t = &s
    t.toggle()
    t.toggle()
}

Ejecutamos el código obteniendo:

on
off

Interfaces vacías:

Hay ocasiones en las que una función puede requerir una interfaz vacía, esto implica que aceptará cualquier tipo de entrada, si además esta entrada es variadic esta estará permitiendo cualquier número de entradas de cualquier tipo, un caso real de este tipò de funciones es fmt.Println.

Recuerda que al obligar a cumplir con la interfaz a los parámetros de entrada de la función, solo los métodos indicados en la interfaz estarán disponibles a no ser que utilizamos los asserts para recuperarla, como es una interfaz vacía, no habrá ningún método disponible.

package main

import "fmt"

// Function that requires an empty interface
func acceptAnything(thing interface{}) {
    fmt.Println(thing)
}

func main() {
    acceptAnything(3.1415)
    acceptAnything("Whatsahh")
}

Si ejecutamos el código obtendremos:

3.1415
Whatsahh

En cambio si intentamos llamar al método del objeto pasado a la función acceptAnything(), dará error:

package main

import "fmt"

// Function that requires an empty interface
func acceptAnything(thing interface{}) {
    thing.printMethod()
}

type testType string
func (t testType) printMethod() {
    fmt.Println(t)
}

func main() {
    testVar := testType("AA")
    acceptAnything(testVar)
}
./08.go:7:11: thing.printMethod undefined (type interface{} has no field or method printMethod)

Si utilizamos el assertion podremos ejecutar el método sin problemas.

package main

import "fmt"

// Function that requires an empty interface
func acceptAnything(thing interface{}) {
    if testVar, ok := thing.(testType); ok {
        testVar.printMethod()
    } else {
        fmt.Println(thing)
    }
}

type testType string
func (t testType) printMethod() {
    fmt.Println(t)
}

func main() {
    testVar := testType("AA")
    acceptAnything(testVar)
    acceptAnything("Whatsahh")
}

Si ejecutamos el código obtendremos.

AA
Whatsahh

Type switch:

Es posible utilizar el tipo switch para comparar tipos, tan solo debemos hacer uso del assertion . En el siguiente ejemplo creamos una función con una interfaz vacía para que acepte cualquier tipo de dato de entrada y mediante el assertion en el switch averiguamos el tipo de variable.

package main

import (
    "fmt"
)

func findType(i interface{}) {
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}
func main() {
    findType("Naveen")
    findType(77)
    findType(89.98)
}

Al ejecutar el código obtenemos:

I am a string and my value is Naveen
I am an int and my value is 77
Unknown type

Los tipos con los que podemos comparar no se limitan a tipos primitivos, también podemos hacerlo contra interfaces.

package main

import (
    "fmt"
)

type Describer interface {
    Describe()
}

type Person struct {
    name string
    age  int
}

func (p Person) Describe() {
    fmt.Printf("%s is %d years old.\n", p.name, p.age)
}

func findType(i interface{}) {
    switch v := i.(type) {
    // case comparison against primitive type: string
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
        fmt.Printf("I am a string and my value is %s\n", v)
    // case comparison against primitive type: int
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
        fmt.Printf("I am an int and my value is %d\n", v)
    // case comparison against interface type: Describer
    case Describer:
        fmt.Printf("I am a Describer and my value is %v\n", i.(Describer))
        fmt.Printf("I am a Describer and my value is %v\n", v)
        v.Describe()
    default:
        fmt.Printf("Unknown type\n")
    }
}

func main() {
    findType("Naveen")
    findType(77)
    findType(89.98)
    person := Person{
        name: "Naveen R",
        age:  25,
    }
    findType(person)
}

Al ejecutar el código obtenemos:

I am a string and my value is Naveen
I am a string and my value is Naveen
I am an int and my value is 77
I am an int and my value is 77
Unknown type
I am a Describer and my value is {Naveen R 25}
I am a Describer and my value is {Naveen R 25}
Naveen R is 25 years old.

Hemos modificado ligeramente el switch para que en cada case el valor de la variable recibida esté disponible en la variable v directamente.

Si te ha gustado el artículo puedes invitarme a un RedBull aquí