Uso de interfaces en Go
Las interfaces en Go son un tipo de datos que se usa para representar el comportamiento de otros tipos. Una interfaz es como un plano técnico o un contrato que un objeto debe cumplir. Cuando se usan interfaces, el código base se vuelve más flexible y adaptable, porque se escribe código que no está vinculado a una implementación concreta. Por tanto, se puede extender la funcionalidad de un programa rápidamente. Entienda por qué en este módulo.
A diferencia de las interfaces de otros lenguajes de programación, las de Go se satisfacen de forma implícita. Go no ofrece palabras clave para implementar una interfaz, Por lo tanto, si está familiarizado con las interfaces en otros lenguajes de programación, pero no está familiarizado con Go, esta idea podría resultar confusa.
En este módulo, trabajamos con varios ejemplos para explorar interfaces en Go y demostrar cómo sacar el máximo partido a ellas.
Declaración de una interfaz
Una interfaz en Go es como un plano técnico. Un tipo abstracto que solo incluye los métodos que un tipo concreto debe poseer o implementar.
Imagine que quiere crear una interfaz en el paquete de geometría que indica qué métodos debe implementar una forma. Podría definir una interfaz como esta:
type Shape interface {
Perimeter() float64
Area() float64
}
La interfaz Shape
significa que cualquier tipo que quiera considerar Shape
debe tener los métodos Perimeter()
y Area()
. Por ejemplo, al crear una estructura Square
, tiene que implementar los dos métodos, no solo uno. Además, tenga en cuenta que una interfaz no contiene detalles de implementación para esos métodos (por ejemplo, para calcular el perímetro y el área de una forma). Son simplemente un contrato. Las formas como triángulos, círculos y cuadrados tienen otras maneras de calcular el área y el perímetro.
Implementar una interfaz
Como se ha explicado antes, en Go no hay una palabra clave para implementar una interfaz. Un tipo cumple de forma implícita una interfaz en Go cuando tiene todos los métodos que necesita una interfaz.
Ahora se creará una estructura Square
que tenga los dos métodos de la interfaz Shape
, como se muestra en el código de ejemplo siguiente:
type Square struct {
size float64
}
func (s Square) Area() float64 {
return s.size * s.size
}
func (s Square) Perimeter() float64 {
return s.size * 4
}
Observe cómo la firma del método de la estructura Square
coincide con la firma de la interfaz Shape
. Pero es posible que otra interfaz tenga otro nombre pero los mismos métodos. ¿Cómo o cuándo sabe Go qué interfaz implementa un tipo concreto? Go sabe cuándo se usa, en tiempo de ejecución.
Para demostrar cómo se usan las interfaces, puede escribir el siguiente código:
func main() {
var s Shape = Square{3}
fmt.Printf("%T\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
}
Al ejecutar el programa anterior, se obtiene la salida siguiente:
main.Square
Area: 9
Perimeter: 12
En este momento, es irrelevante si se usa una interfaz o no. Ahora se creará otro tipo, como Circle
, y luego se explorará por qué las interfaces son útiles. Este es el código de la estructura Circle
:
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.radius
}
Ahora vamos a refactorizar la función main()
y crear una función que pueda imprimir el tipo del objeto que recibe, junto con su área y perímetro, de la siguiente manera:
func printInformation(s Shape) {
fmt.Printf("%T\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
fmt.Println()
}
Observe cómo la función printInformation
tiene Shape
como parámetro. Puede enviar un Square
o un objeto Circle
a esta función y funciona, aunque la salida es diferente. Ahora la función main()
tiene este aspecto:
func main() {
var s Shape = Square{3}
printInformation(s)
c := Circle{6}
printInformation(c)
}
Observe que para el objeto c
, no se especifica que sea un objeto Shape
. Pero la función printInformation
espera un objeto que implemente los métodos que se definen en la interfaz Shape
.
Al ejecutar el programa, debería obtener la salida siguiente:
main.Square
Area: 9
Perimeter: 12
main.Circle
Area: 113.09733552923255
Perimeter: 37.69911184307752
Observe cómo no recibe un error y la salida varía en función del tipo de objeto que recibe. También puede ver que el tipo de objeto de la salida no indica nada sobre la interfaz Shape
.
La ventaja de usar interfaces es que, para cada nuevo tipo o implementación de Shape
, la función printInformation
no tiene que cambiar. Tal y como hemos mencionado antes, su código se vuelve más flexible y fácil de extender cuando usa interfaces.
Implementación de una interfaz Stringer
Un ejemplo sencillo de ampliación de la funcionalidad existente consiste en usar una instancia de Stringer
, que es una interfaz que tiene un método String()
, similar al siguiente:
type Stringer interface {
String() string
}
La fmt.Printf
función usa esta interfaz para imprimir valores, lo que significa que puede escribir su método String()
personalizado para imprimir una cadena personalizada, como esta:
package main
import "fmt"
type Person struct {
Name, Country string
}
func (p Person) String() string {
return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
rs := Person{"John Doe", "USA"}
ab := Person{"Mark Collins", "United Kingdom"}
fmt.Printf("%s\n%s\n", rs, ab)
}
Al ejecutar el programa anterior, se obtiene la salida siguiente:
John Doe is from USA
Mark Collins is from United Kingdom
Como puede ver, usó un tipo personalizado (una estructura) para escribir una versión personalizada del método String()
. Esta técnica es una forma común de implementar una interfaz en Go y encontrará ejemplos de ella en muchos programas, ya que estamos a punto de explorar.
Ampliación de una implementación existente
Imagine que tiene el código siguiente y le gustaría extender su funcionalidad escribiendo una implementación personalizada de un método Writer
que se encarga de manipular algunos datos.
Con el código siguiente, puede crear un programa que use la API de GitHub para obtener tres repositorios de Microsoft:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=3")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
io.Copy(os.Stdout, resp.Body)
}
Al ejecutar el código anterior, se obtiene algo parecido a la salida siguiente (abreviada para mejorar la legibilidad):
[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....
Observe que la llamada io.Copy(os.Stdout, resp.Body)
es la que imprime en el terminal el contenido que obtuvo de la llamada a la API de GitHub. Imagine que quiere escribir una implementación propia para acortar el contenido que se ve en el terminal. Al examinar el origen de la io.Copy
función, ve lo siguiente:
func Copy(dst Writer, src Reader) (written int64, err error)
Si profundiza en los detalles del primer parámetro, dst Writer
, observa que Writer
es una interfaz:
type Writer interface {
Write(p []byte) (n int, err error)
}
Puede seguir explorando el código fuente del paquete de io
hasta que encuentre dónde Copy
llama al Write
método, pero dejemos esta exploración solo por ahora.
Como Writer
es una interfaz y es un objeto que la función Copy
espera, podría escribir la implementación personalizada del método Write
. Por tanto, puede personalizar el contenido que se imprime en el terminal.
Lo primero que necesita para implementar una interfaz es crear un tipo personalizado. En este caso, puede crear una estructura vacía, porque simplemente tiene que escribir el método Write
personalizado, de esta manera:
type customWriter struct{}
Ahora está preparado para escribir la función Write
personalizada. También debe escribir una estructura para analizar la respuesta de la API en formato JSON en un objeto Golang. Podría usar el sitio de JSON-to-Go para crear una estructura a partir de una carga JSON. Por tanto, el método Write
tendría este aspecto:
type GitHubResponse []struct {
FullName string `json:"full_name"`
}
func (w customWriter) Write(p []byte) (n int, err error) {
var resp GitHubResponse
json.Unmarshal(p, &resp)
for _, r := range resp {
fmt.Println(r.FullName)
}
return len(p), nil
}
Por último, tiene que modificar la función main()
para usar el objeto personalizado, de esta manera:
func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
writer := customWriter{}
io.Copy(writer, resp.Body)
}
Al ejecutar el programa, debería obtener la salida siguiente:
microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template
Ahora la salida es mejor, gracias al método Write
personalizado que ha escrito. Esta es la versión final del programa:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type GitHubResponse []struct {
FullName string `json:"full_name"`
}
type customWriter struct{}
func (w customWriter) Write(p []byte) (n int, err error) {
var resp GitHubResponse
json.Unmarshal(p, &resp)
for _, r := range resp {
fmt.Println(r.FullName)
}
return len(p), nil
}
func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
writer := customWriter{}
io.Copy(writer, resp.Body)
}
Escritura de una API de servidor personalizada
Por último, se examinará otro caso de uso para las interfaces que puede resultarle útil si va a crear una API de servidor. La forma habitual de escribir un servidor web consiste en usar la interfaz http.Handler
del paquete net/http
, que tiene un aspecto similar al siguiente (no es necesario escribir este código):
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
Observe cómo la función ListenAndServe
espera una dirección de servidor, como http://localhost:8000
, y una instancia del Handler
que envía la respuesta de la llamada a la dirección del servidor.
Ahora se creará y examinará el siguiente programa:
package main
import (
"fmt"
"log"
"net/http"
)
type dollars float32
func (d dollars) String() string {
return fmt.Sprintf("$%.2f", d)
}
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
Antes de explorar el código anterior, se ejecutará de esta forma:
go run main.go
Si no obtiene ninguna salida, es buena señal. Ahora abra http://localhost:8000
en una nueva ventana del explorador o, en el terminal, ejecute el comando siguiente:
curl http://localhost:8000
Debería obtener esta salida:
Go T-Shirt: $25.00
Go Jacket: $55.00
Ahora se revisará detenidamente el código anterior para comprender lo que hace y observar la eficacia de las interfaces de Go. En primer lugar, empiece por crear un tipo personalizado para un tipo float32
, con la idea de escribir una implementación personalizada del método String()
, que se usará más adelante.
type dollars float32
func (d dollars) String() string {
return fmt.Sprintf("$%.2f", d)
}
Después, se escribe la implementación del método ServeHTTP
que http.Handler
puede usar. Observe cómo se ha vuelto a crear un tipo personalizado, pero esta vez es un mapa, no una estructura. Después, se escribe el método ServeHTTP
mediante el tipo database
como receptor. La implementación de este método usa los datos del receptor, los recorre en bucle e imprime cada elemento.
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
Por último, en la función main()
, se ha creado una instancia de un tipo database
y se ha inicializado con algunos valores. Se ha iniciado el servidor HTTP mediante la función http.ListenAndServe
, donde se ha definido la dirección del servidor, incluido el puerto que se va a usar y el objeto db
que implementa una versión personalizada del método ServeHTTP
. Al ejecutar el programa, Go usa la implementación de ese método y así se usa e implementa una interfaz en una API de servidor.
func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
Puede encontrar otro caso de uso para las interfaces en una API de servidor al usar la función http.Handle
. Para obtener más información, vea la publicación Writing web applications (Escritura de aplicaciones web) en el sitio de Go.