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:
- Ejemplo con interfaz.
- Mismo código sin interfaz.
- Ejecución del código.
- Explicación.
- Objetos de tipo interfaz.
- Array de tipo interfaz como entrada de una función.
- Inclusión de interfaces desde otra interfaz.
- Restricción de métodos al cumplir con una interfaz.
- Go assertion.
- Puntero como receiver en un método de una interfaz
- Interfaces vacÃas
- Type switch.
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.