Mucho se ha hablado de generics
en Go y es que su ausencia es acusante desde casi el principio del lenguaje, detrás de esto hay tanto mucha gente que apoya que el lenguaje no necesita de esta funcionalidad como mucha más que apoya que deberían ser incluidos cuanto antes.
Con este artículo queremos aclarar que no han sido implementados todavía, tranquilos, pero ya tenemos una buena idea de su posible diseño. Ya que tenemos un borrador de su diseño el cual parece empezar a ser bastante estable y por si eso fuera poco tenemos un prototipo creado por el equipo de Go.
Por otro lado queremos también remarcar que nos basaremos no sólo en el draft, sino también en un artículo creado por Chris Brown, el cual ha tenido para bien crear un nuevo playground sobre WebAssembly donde podremos probar nuestras implementaciones sobre este prototipo de Go que incluye los generics
.
Sistema de Caché
El código genérico es ese código que es escrito usando un type
que será especificado más tarde.
Para ver un poco la sintaxis que tendremos os pongo un ejemplo muy simple, un sistema básico de caché, con los métodos Get
, Add
y Delete
.
func main() {
c := make(Cache)
fmt.Println(c)
c.Add("key1", 1)
c.Add("key2", "value")
fmt.Println(c)
c.Delete("key2")
fmt.Println(c.Get("key1"))
}
type Cache map[string]interface{}
func (c Cache) Get(key string) interface{} {
v, ok := c[key]
if !ok {
return nil
}
return v
}
func (c Cache) Add(key string, value interface{}) {
c[key] = value
}
func (c Cache) Delete(key string) bool {
if _, ok := c[key]; !ok {
return false
}
delete(c, key)
return true
}
Simplificando mucho podríamos tener una implementación similar a esta, pero como vemos estaríamos utilizando interface{}
que sabemos que no brilla por su eficiencia, además como se muestra en el ejemplo podríamos almacenar valores de todo tipo y tener que realizar castings por todo nuestro código allí donde lo usemos, y podría no ser lo que estamos buscando, podríamos solucionar el mantener un sólo tipo con algún serializado, y crear un map[string][]byte
por ejemplo, pero nada nos ahorraría las conversiones de tipo, a no ser que creemos una caché especifica para cada tipo.
Con la llegada de generics
podríamos cambiar la implementación para inicializar nuestra Caché a un tipo concreto, sin tener diferentes implementaciones. Vamos a ver como se hace.
Vamos por partes, lo primero que haremos es cambiar la implementación de nuestro tipo Cache
.
type Cache(type T) map[string]T
Como vemos hemos cambiado la interface{}
por T
, ¿qué es esa T
? básicamente el parámetro con el que tiparemos nuestro type Cache
y se declara, justo a continuación del nombre de nuestro type
, pudiendo poner el nombre que nosotros queramos, no es obligatorio usar T
pero suele ser un estándar.
Pero esto sería igual de válido:
type Cache(type C) map[string]C
A continuación tendremos que cambiar los tipados de todas nuestras funciones, para que en vez de recibir una simple Cache
reciban una Cache(T)
y no sólo eso sino que pasaremos a devolver T
en lugar de interface{}
, es decir obtendremos directamente el tipo que hayamos declarado cuando la función nos devuelva el valor.
func (c Cache(T)) Get(key string) T {
v, ok := c[key]
if !ok {
var not T
return not
}
return v
}
func (c Cache(T)) Add(key string, value T) {
c[key] = value
}
func (c Cache(T)) Delete(key string) bool {
if _, ok := c[key]; !ok {
return false
}
delete(c, key)
return true
}
Ahora sólo nos queda jugar con nuestra nueva caché, por ejemplo podríamos crear dos tipos de caché una para valores string
y otra para valores int
.
cacheInt := make(Cache(int))
cacheString := make(Cache(string))
cacheInt.Add("key1", 1)
cacheInt.Add("key2", 2)
fmt.Println(cacheInt.Get("key1") + 5) // output: 6
cacheString.Add("key", "val1")
fmt.Println(cacheString.Get("key1"))
Cómo veis hemos pasado a tener una implementación mucho más optima de la implementación de nuestra caché ya que para el compilador será mucho más sencillo optimizar los resultados mejorando la performance, aunque a expensas de un binario quizá algo más grande.
Contracts
Ahora que ya sabemos jugar con los generics
es el turno de los contracts
, vamos a poner un ejemplo muy muy sencillo imaginemos que queremos realizar una función que dado un tipo, humanicemos dicho valor para tener algo legible en string
.
Para ello deberemos crear una función con un type parameter
, antes vimos como crear types
con su type parameter
, el type parameter
es aquel del cual aún no sabemos su valor, pero ahora lo que veremos es como hacer eso mismo con las funciones.
func Humanize(type T)(s T, separator string) string {
// code here
}
Esa sería la forma natural de llamar a una función como la que hemos pensado, es decir primero el nombre de la función acompañado de su type parameter
, a continuación pasaríamos los valores a dicha función que en este caso el primero es también un type parameter
, porque no conocemos su valor, y el segundo un separador, por último devolvemos un string
.
Pero tenemos un pequeño problema con dicha implementación, sabemos que para humanizar el tipo dado, necesitaremos convertir dicho tipo a string
, pero claro, no conocemos el tipo ¿así que cómo lo hacemos?
Para ello existen los contracts
son como una especia de interfaz, que nos permitiran dotar de cierto comportamiento a nuestros type parameters
.
contract stringer(T) {
T String() string
}
Así que ahora podremos cambiar nuestra función para no sólo aceptar un type parameter
sino que podemos llegar a indicar que ese type parameter
tiene un comportamiento.
func Humanize(type T stringer)(s T, separator string) string {
str := s.String()
var humanized string
for k, v := range str {
if k == 0 {
humanized = strings.ToUpper(string(s[0]))
} else {
if string(v) == separator {
humanized += string(" ")
} else {
humanized += string(v)
}
}
}
return humanized
}
Cómo vemos en la primera línea ya podremos llamar a nuestro método, String()
con seguridad porque sabremos que el tipo que vayamos a pasar implementa dicho método.
Conclusión
Los generics
pueden resultarnos muy útiles para abstraer implementaciones que en otros casos deberíamos de hacer de forma concreta, pero no por ello deberemos abusar de ellos sino pensar bien en cada caso si es lo que necesitamos.
Si queréis más información sobre ellos no olvidéis visitar los artículos que hemos mencionado al principio, y obviamente estamos a vuestra entera disposición para resolver vuestras dudas, ya sea por los comentarios del blog o en nuestro twitter @FriendsofGoTech