Cómo usar interfaces en Go

Introducción

Escribir código flexible, reutilizable y modular es fundamental para desarrollar programas versátiles. Trabajar de esta manera asegura que el código sea más fácil de mantener evitando la necesidad de realizar la misma modificación en varios lugares. Cómo lograr esto varía de lenguaje a lenguaje. Por ejemplo, la herencia es una aproximación común que se utiliza en lenguajes como Java, C++, C# y más.

Los desarrolladores también pueden alcanzar los mismos objetivos de diseño a través de la composición. La composición es una manera de combinar objetos o tipos de datos en más complejos. Esta es la aproximación que utiliza Go para fomentar la reutilización de código, la modularidad y la flexibilidad. Las interfaces en Go proporcionan un método para organizar composiciones complejas, y aprender a utilizarlas le permitirá crear código común y reutilizable.

En este artículo, aprenderá cómo componer tipos personalizados que tienen comportamientos comunes, lo que le permitirá reutilizar su código. También aprenderá cómo implementar interfaces para sus propios tipos personalizados que satisfacen interfaces definidas en otro paquete.

Definición de un Comportamiento

Una de las implementaciones centrales de la composición es el uso de interfaces. Una interfaz define un comportamiento de un tipo. Una de las interfaces más comúnmente utilizadas en la biblioteca estándar de Go es la interfaz fmt.Stringer:

type Stringer interface {
    String() string
}

La primera línea de código define un tipo llamado Stringer. Luego establece que es una interfaz. Al igual que definir un struct, Go utiliza corchetes ({}) para rodear la definición de la interfaz. En comparación con la definición de structs, solo definimos el comportamiento de la interfaz; es decir, “qué puede hacer este tipo”.

En el caso de la interfaz Stringer, el único comportamiento es el método String(). El método no acepta ningún argumento y devuelve una cadena de texto.

A continuación, echemos un vistazo a algún código que tiene el comportamiento de fmt.Stringer:

main.go
package main

import "fmt"

type Article struct {
	Title string
	Author string
}

func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
	a := Article{
		Title: "Understanding Interfaces in Go",
		Author: "Sammy Shark",
	}
	fmt.Println(a.String())
}

Lo primero que hacemos es crear un nuevo tipo llamado Article. Este tipo tiene campos Title y Author y ambos son de tipo string datatype:

main.go
...
type Article struct {
	Title string
	Author string
}
...

A continuación, definimos un method llamado String en el tipo Article. El método String devolverá una cadena de texto que representa el tipo Article:

main.go
...
func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...

Luego, en nuestro main funcion, creamos una instancia de la clase Article y la asignamos a la variable llamada a. Se le proporcionan valores para el campo "Título" con "Understanding Interfaces in Go" y para el campo "Autor" con "Sammy Shark":

main.go
...
a := Article{
	Title: "Understanding Interfaces in Go",
	Author: "Sammy Shark",
}
...

Luego, imprimimos el resultado de la invocación al método String pasando como parámetro el resultado de la invocación al método a.String():

main.go
...
fmt.Println(a.String())

Después de ejecutar el programa verás el siguiente resultado:

Output
The "Understanding Interfaces in Go" article was written by Sammy Shark.

Hasta ahora, no hemos usado ninguna interfaz, pero sí hemos creado un tipo que tenía un comportamiento deseado. Ese comportamiento coincide con la interfaz fmt.Stringer. Ahora vamos a ver cómo podemos utilizar esa funcionalidad para hacer nuestro código más reutilizable.

Definición de una Interfaz

Ahora que hemos definido nuestro tipo con el comportamiento deseado, podemos mirar cómo usar ese comportamiento.

Antes de hacerlo, sin embargo, veamos qué necesitaríamos hacer si queriamos llamar al método String desde la clase Article en una función:

main.go
package main

import "fmt"

type Article struct {
	Title string
	Author string
}

func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
	a := Article{
		Title: "Understanding Interfaces in Go",
		Author: "Sammy Shark",
	}
	Print(a)
}

func Print(a Article) {
	fmt.Println(a.String())
}

En este código agregamos una nueva función llamada Print que tiene un argumento de tipo Article. Nota que lo único que hace la función Print es llamar al método String. Por esta razón, podríamos definir en su lugar una interfaz para pasarla a la función:

main.go
package main

import "fmt"

type Article struct {
	Title string
	Author string
}

func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Stringer interface {
	String() string
}

func main() {
	a := Article{
		Title: "Understanding Interfaces in Go",
		Author: "Sammy Shark",
	}
	Print(a)
}

func Print(s Stringer) {
	fmt.Println(s.String())
}

Aquí creamos una interfaz llamada Stringer:

main.go
...
type Stringer interface {
	String() string
}
...

La interfaz Stringer tiene solo un método llamado String() que devuelve un string. Un método es una funcion especial definida dentro de un tipo específico en Go. A diferencia de una función, un método puede ser llamado sólo desde una instancia del tipo en el que fue definido.

Luego actualizamos la firma del método Print para tomar una Stringer, y no un tipo concreto de Article. Debido a que el compilador sabe que una interfaz Stringer define el método String, sólo aceptará tipos que también tenga el método String.

Ahora podemos usar el método Print con cualquier cosa que satisfaga la interfaz Stringer. Vamos a crear otro tipo para demostrar esto:

main.go
package main

import "fmt"

type Article struct {
	Title  string
	Author string
}

func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Book struct {
	Title  string
	Author string
	Pages  int
}

func (b Book) String() string {
	return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
}

type Stringer interface {
	String() string
}

func main() {
	a := Article{
		Title:  "Understanding Interfaces in Go",
		Author: "Sammy Shark",
	}
	Print(a)

	b := Book{
		Title:  "All About Go",
		Author: "Jenny Dolphin",
		Pages:  25,
	}
	Print(b)
}

func Print(s Stringer) {
	fmt.Println(s.String())
}

Ahora agregamos un segundo tipo llamado Book. También tiene definido el método String. Esto significa que también satisface la interfaz Stringer. Por eso también se puede enviar a nuestra función Print:

Output
The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

Hasta ahora, hemos mostrado cómo usar sólo una interfaz. Sin embargo, una interfaz puede tener más de un comportamiento definido. Ahora veremos cómo podemos hacer nuestras interfaces más versátiles declarando métodos más.

Comportamientos múltiples en una interfaz

Una de las principales tenencias de escribir código en Go es escribir tipos pequeños y concisos y componerlos para crear tipos más grandes y complejos. Lo mismo es verdad cuando se compone una interfaz. Para ver cómo construimos una interfaz, primero comenzamos definiendo solo una interfaz. Definimos dos formas, Circle y Square, que ambas definen un método llamado Area. Este método devuelve el área geométrica de sus respectivos objetos:

main.go
package main

import (
	"fmt"
	"math"
)

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * math.Pow(c.Radius, 2)
}

type Square struct {
	Width  float64
	Height float64
}

func (s Square) Area() float64 {
	return s.Width * s.Height
}

type Sizer interface {
	Area() float64
}

func main() {
	c := Circle{Radius: 10}
	s := Square{Height: 10, Width: 5}

	l := Less(c, s)
	fmt.Printf("%+v is the smallest\n", l)
}

func Less(s1, s2 Sizer) Sizer {
	if s1.Area() < s2.Area() {
		return s1
	}
	return s2
}

Porque cada tipo declara el método Area, podemos crear una interfaz que defina ese comportamiento. Creamos la siguiente Sizer:

main.go
...
type Sizer interface {
	Area() float64
}
...

Luego definimos una función llamada Less que toma dos Sizer y devuelve el menor uno:

main.go
...
func Less(s1, s2 Sizer) Sizer {
	if s1.Area() < s2.Area() {
		return s1
	}
	return s2
}
...

Notice que no solo aceptamos ambos argumentos como el tipo Sizer, sino que también retornamos el resultado como Sizer también. Esto significa que ya no devuelven un Square o un Circle, sino la interfaz de Sizer.

Finalmente, imprimimos qué tuvo el área más pequeña:

Output
{Width:5 Height:10} is the smallest

A continuación, agregaremos otro comportamiento a cada tipo. Esta vez agregaremos el método String() que devuelve una cadena de texto. Esto satisfará la interfaz fmt.Stringer:

main.go
package main

import (
	"fmt"
	"math"
)

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * math.Pow(c.Radius, 2)
}

func (c Circle) String() string {
	return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}

type Square struct {
	Width  float64
	Height float64
}

func (s Square) Area() float64 {
	return s.Width * s.Height
}

func (s Square) String() string {
	return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}

type Sizer interface {
	Area() float64
}

type Shaper interface {
	Sizer
	fmt.Stringer
}

func main() {
	c := Circle{Radius: 10}
	PrintArea(c)

	s := Square{Height: 10, Width: 5}
	PrintArea(s)

	l := Less(c, s)
	fmt.Printf("%v is the smallest\n", l)

}

func Less(s1, s2 Sizer) Sizer {
	if s1.Area() < s2.Area() {
		return s1
	}
	return s2
}

func PrintArea(s Shaper) {
	fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Debido a que tanto el tipo Circle como el tipo Square implementan ambos métodos Area y String, ahora podemos crear otra interfaz para describir ese conjunto más amplio de comportamientos. Para esto, crearemos una interfaz llamada Shaper. Compondremos esta interfaz de la interfaz Sizer y la interfaz fmt.Stringer:

main.go
...
type Shaper interface {
	Sizer
	fmt.Stringer
}
...

Nota: Se considera idiomático intentar nombrar tu interfaz terminando en er, como en fmt.Stringer, io.Writer, etc. Esto es por qué llamamos a nuestra interfaz Shaper y no Shape.

Ahora podemos crear una función llamada PrintArea que toma una Shaper como argumento. Esto significa que podemos llamar a ambos métodos en el valor pasado en ambos métodos Area y String:

main.go
...
func PrintArea(s Shaper) {
	fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Si ejecutamos el programa, recibiremos la siguiente salida:

Output
area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest

Hemos visto ahora cómo podemos crear interfaces más pequeñas y construirlas en interfaces más grandes según sea necesario. Si bien podríamos haber comenzado con la interfaz más grande y pasarla a todas nuestras funciones, se considera mejor práctica enviar solo la interfaz más pequeña a una función que es necesaria. Esto generalmente resulta en código más claro, ya que cualquier cosa que acepta una interfaz específica más pequeña solo tiene la intención de trabajar con ese comportamiento definido.

Por ejemplo, si pasamos Shaper a la función Less, podemos suponer que va a llamar a ambos métodos Area y String. Sin embargo, ya que solo pretendemos llamar al método Area, hace que la función Less sea clara ya que sabemos que solo podemos llamar al método Area de cualquier argumento pasado a ella.

Conclusión

Hemos visto cómo la creación de interfaces más pequeñas y construirlas para formar interfaces más grandes nos permite compartir solo lo que necesitamos a una función o método. También hemos aprendido que podemos componer nuestros interfaces a partir de otros interfaces, incluidos los definidos en otros paquetes, no solo en nuestros paquetes.

Si desea aprender más sobre el lenguaje de programación Go, revise la serie completa Cómo programar en Go.

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go