Una de las características de diseño más sutiles en Go son las interfaces. Después de leer esta sección, probablemente quedara impresionado por su implementación.
En pocas palabras, una interfaz es un conjunto de métodos, que se usan para definir un conjunto de acciones.
Al igual que en los ejemplos de las secciones anteriores, ambos, Student y Employee pueden llamar a SayHi()
, pero ellos no hacen lo mismo.
Vamos a trabajar un poco mas, vamos a agregarle a ellos el método Sing()
, y también vamos a agregar BorrowMoney()
a Student y SpendSalary()
a Employee.
Ahora Student tiene tres métodos llamados SayHi()
, Sing()
, BorrowMoney()
, y Employee tiene SayHi()
, Sing()
y SpendSalary()
.
Esta combinación de métodos es llamada interfaz, y es implementada por Student y Employee. Entonces Student y Employee implementan la interfaz: SayHi()
, Sing()
. Al mismo tiempo, Employee no implementa la interfaz: SayHi()
, Sing()
, BorrowMoney()
, y Student no implementa la interfaz: SayHi()
, Sing()
, SpendSalary()
. Esto es porque Employee no tiene el método BorrowMoney()
y Student no tiene el método SpendSalary()
.
Una interfaz define un grupo de métodos, entonces si un tipo implementa todos los métodos entonces nosotros decimos que ese tipo implementa la interfaz.
type Human struct {
name string
age int
phone string
}
type Student struct {
Human
school string
loan float32
}
type Employee struct {
Human
company string
money float32
}
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func (h *Human) Sing(lyrics string) {
fmt.Println("La la, la la la, la la la la la...", lyrics)
}
func (h *Human) Guzzle(beerStein string) {
fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}
// Employee sobrescribe Sayhi
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Si acá podes cortarlo en 2 líneas.
}
func (s *Student) BorrowMoney(amount float32) {
s.loan += amount // (otra vez...)
}
func (e *Employee) SpendSalary(amount float32) {
e.money -= amount
}
// definimos algunas interfaces
type Men interface {
SayHi()
Sing(lyrics string)
Guzzle(beerStein string)
}
type YoungChap interface {
SayHi()
Sing(song string)
BorrowMoney(amount float32)
}
type ElderlyGent interface {
SayHi()
Sing(song string)
SpendSalary(amount float32)
}
Sabemos que una interfaz puede ser implementada por cualquier tipo, y un tipo puede implementar muchas interfaces al mismo tiempo.
Tenga en cuenta que todos los tipos implementan la interfaz vacía interface{}
porque esta no tiene ningún método y todos los tipos tienen cero métodos por defecto.
Entonces, ¿qué tipo de valores se pueden poner en una interfaz? Si definimos una variable como tipo interfaz, cualquier tipo que implemente la interfaz puede asignarse a esta variable.
Al igual que en el ejemplo anterior, si definimos una variable m como la interfaz Men, entonces cualquier Student, Human o Employee puede ser asignado a m. Entonces podríamos tener una lista de Men, y cualquier tipo que implemente la interfaz Men puede agregarse a esa lista (slice). Sin embargo sea consciente que una lista de interfaces no tiene el mismo comportamiento que una lista de otros tipos.
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human
school string
loan float32
}
type Employee struct {
Human
company string
money float32
}
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
func (e Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Si acá podes cortarlo en 2 líneas.
}
// La interfaz Men es implementada por Human, Student y Employee
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}
// definimos la interfaz i
var i Men
//i podemos guardar un Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//i podemos guardar un Employee
i = Tom
fmt.Println("This is Tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
// lista de Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
// estos tres elementos son de diferentes tipos pero todos ellos implementan la interfaz Men
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x {
value.SayHi()
}
}
Una interfaz es un conjunto de métodos abstractos, y puede ser implementada por tipos que no son interfaces. Por lo tanto esta no puede implementarse a si misma.
Una interfaz vacía es una interfaz que no contiene ningún método, entonces todos los tipos implementan una interfaz vacía. Es muy útil cuando buscamos guardar todos los tipos en el mismo lugar, y es similar a void* en C.
// definimos una interfaz vacía
var a interface{}
var i int = 5
s := "Hello world"
// a puede guardar un valor de cualquier tipo
a = i
a = s
Si una función usa una interfaz vacía como su tipo de argumento, esta puede aceptar cualquier tipo; si una función usa una interfaz vacía como el tipo de valor de retorno, esta puede devolver cualquier tipo.
Cualquier variable puede usarse en una interfaz, entonces podemos pensar sobre como podemos usar esta característica para pasar cualquier tipo de variable a una función.
Por ejemplo, usamos mucho fmt.Println, pero alguna vez notaste que ¿acepta cualquier tipo de argumento? Si vemos el código fuente de fmt que es libre, podemos ver la siguiente definición.
type Stringer interface {
String() string
}
Esto significa que cualquier tipo que implemente la interfaz Stringer puede ser pasada a fmt.Println como argumento. Vamos a probarlo.
package main
import (
"fmt"
"strconv"
)
type Human struct {
name string
age int
phone string
}
// Human implementa fmt.Stringer
func (h Human) String() string {
return "Name:" + h.name + ", Age:" + strconv.Itoa(h.age) + " years, Contact:" + h.phone
}
func main() {
Bob := Human{"Bob", 39, "000-7777-XXX"}
fmt.Println("This Human is : ", Bob)
}
Volviendo atrás al ejemplo de Box, podemos ver que Color también implementa la interfaz Stringer, por lo que somos capaces de personalizar el formato de impresión. Si no implementamos esa interfaz, fmt.Println imprimirá el tipo con el formato por defecto.
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())
Atención: Si el tipo implementa la interfaz error
, fmt va a llamar a error()
, entonces en este punto no tendrás que implementar Stringer.
Si una variable es del tipo que implementa una interfaz, sabemos que cualquier otro tipo que implemente la misma interfaz puede ser asignada a esta variable. La pregunta es ¿Cómo podemos saber cual es el tipo específico almacenado en la interfaz? Tenemos dos formas, que te voy a comentar a continuación.
- Patron de Aserción Comma-ok
Go tiene la sintaxis value, ok := element.(T)
. Esto comprueba si la variable es del tipo que se espero, donde value es el valor de la variable, y ok es un valor de tipo booleano, element es la variable interfaz y T es el tipo que se afirma tener.
Si el elemento es del tipo que esperamos, ok será true, en otro caso será false.
Veamos un ejemplo para verlo con más claridad.
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List []Element
type Person struct {
name string
age int
}
func (p Person) String() string {
return "(Nombre: " + p.name + " - edad: " + strconv.Itoa(p.age) + " años)"
}
func main() {
list := make(List, 3)
list[0] = 1 // un int
list[1] = "Hola" // una string
list[2] = Person{"Dennis", 70}
for index, element := range list {
if value, ok := element.(int); ok {
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
} else if value, ok := element.(string); ok {
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
} else if value, ok := element.(Person); ok {
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
} else {
fmt.Println("list[%d] is of a different type", index)
}
}
}
Es bastante sencillo usar este patrón, pero si tenemos que verificar varios tipos, es mejor que usemos un switch
.
- Verificación con un switch
Vamos a ver el uso de switch
para reescribir el ejemplo anterior.
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List []Element
type Person struct {
name string
age int
}
func (p Person) String() string {
return "(Nombre: " + p.name + " - edad: " + strconv.Itoa(p.age) + " años)"
}
func main() {
list := make(List, 3)
list[0] = 1 //un int
list[1] = "Hello" //una string
list[2] = Person{"Dennis", 70}
for index, element := range list {
switch value := element.(type) {
case int:
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
case string:
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
case Person:
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
default:
fmt.Println("list[%d] is of a different type", index)
}
}
}
Una cosa que deberíamos recordar es que element.(type)
no puede ser usado fuera del cuerpo del switch
, lo que significa que en este caso no puedes usar el patrón comma-ok
.
La cosa más atractiva es que Go tiene mucha lógica embebida en su sintaxis, como campos anónimos en un struct. No es para sorprenderse, que podamos usar interfaces también como campos anónimos, pero vamos a llamarlas Interfaces embebidas
. Aquí, vamos a seguir las mismas reglas que para los campos anónimos. Más específicamente, si una interfaz tiene otra interfaz como una interfaz embebida, esta tendrá todos los métodos que la clase embebida tiene.
Podemos ver el archivo fuente container/heap
que tiene una definición como la siguiente.
type Interface interface {
sort.Interface // embebida sort.Interface
Push(x interface{}) //el método Push para empujar elementos a la pila
Pop() interface{} //el elemento Pop que saca elementos de la pila
}
Podemos ver que sort.Interface
es una interfaz embebida, por lo que la interfaz anterior tiene tres métodos que son explícitos de sort.Interface
.
type Interface interface {
// Len es el número de elementos en la colección.
Len() int
// Less devuelve si el elemento de índice i se debe ordenar
// antes que el elemento con índice j.
Less(i, j int) bool
// Swap intercambia los elementos con índices i y j.
Swap(i, j int)
}
Otro ejemplo es el de io.ReadWriter
en el paquete io
.
// io.ReadWriter
type ReadWriter interface {
Reader
Writer
}
La reflexión en Go es usada para obtener información en tiempo de ejecución. Vamos a usar el paquete reflect
, y este articulo oficial articulo explica como funciona la reflexión en Go.
Tenemos que realizar tres pasos para usar reflect. Primero, necesitamos convertir una interfaz en un tipo reflect types (reflect.Type o reflect.Value, depende en que situación nos encontremos).
t := reflect.TypeOf(i) // tomamos la meta-data de el tipo i, y usamos t para tomar todos los elementos
v := reflect.ValueOf(i) // tomamos el valor actual de el tipo i, y usamos v para cambiar este valor
Después de eso, necesitamos convertir el tipo reflect de el valor que tomamos a el tipo que necesitamos.
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
Finalmente, si buscamos cambiar el valor que vino del tipo reflects, necesitamos hacerlo modificable. Como hablamos antes, esta es una diferencia entre pasar por valor o por referencia. El siguiente código no compilará.
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
En lugar de eso, debemos usar el siguiente código para cambiar el valor de los tipos reflect.
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
Acabamos de hablar sobre los conocimientos básicos de sobre el uso de reflexión (reflection), deberías practicar más para entenderlo mejor.
- Índice
- Sección anterior: Orientado a objetos
- Siguiente sección: Concurrencia