Soy culpable, me gusta poner títulos rimbombantes. En este caso no me siento tan mal porque el título ahorrará a más de uno la necesidad de hacer click por pura curiosidad 😉

Lo que pretendo en este es combinar los conceptos de los dos últimos posts (composición e interfaces), y presentar algunas implicaciones de esa unión.

Recordando las interfaces

Para los despistados, vamos a definir una interfaz y un tipo que cumpla el interfaz.


package main
import "fmt"
type motor struct {
ruido string
}
func (n motor) hacerRuido() string {
return fmt.Sprintf("Este motor hace: %s", n.ruido)
}
type cosaRuidosa interface {
hacerRuido() string
}
func main() {
var ruidoso cosaRuidosa
ruidoso = motor{"¡Brrroum!"}
fmt.Println(ruidoso.hacerRuido())
}

La salida es:

Este motor hace: ¡Brrroum!

Lo más importante de este ejemplo es comprobar que NO hay ninguna referencia en ningún sitio a que el tipo naveEspacial sea una cosaRuidosa.

No hace falta decir que la interfaz cosaRuidosa tendrá un tipo que lo implemente, ni decir que el tipo motor implementa esa interfaz. No hay ninguna fórmula equivalente a implements de Java; simplemente no es necesario.

En Go, basta con que un tipo cumpla un interfaz. No hace falta decírselo a nadie; el compilador es suficientemente listo como para darse cuenta.

Independencia entre interfaces, tipos y paquetes

El principal beneficio de la satisfacción implícita de interfaces (que no haya que poner implements en cada clase que implementa una interfaz) es que se eliminan dependencias.

En Java, si queremos decir que una clase cumple un interfaz, es necesario modificar el código de la clase. Esto crea dos complicaciones:

  • Se crea una dependencia entre el interfaz y la clase: si se modifica la interfaz, habrá que modificar la clase forzosamente.
  • Las interfaces que implementa una clase tienen que definirse antes que las clases que las implementan. Este tema puede parecer trivial, pero veremos situaciones en las que nos viene muy bien definir el interfaz a posteriori.

Añadiendo la composición a la ecuación

Vamos a añadir un tipo compuesto al ejemplo anterior y veremos qué pasa. Hay un cambio sutil en la definición del motor. Antes utilizábamos una variable de tipo ruidoso y ahora utilizamos concretamente un motor. Espero que este detalle no despiste a nadie.


package main
import "fmt"
type motor struct {
ruido string
}
func (n motor) hacerRuido() string {
return fmt.Sprintf("Este motor hace: %s", n.ruido)
}
type cosaRuidosa interface {
hacerRuido() string
}
type naveEspacial struct {
numLasers int
capcidadPasajeros int
motor
}
func main() {
var miMotor = motor{"¡Brrroum!"}
var miNave = naveEspacial{4, 2, miMotor}
fmt.Println(miNave.hacerRuido())
}

La salida es la misma que antes:

Este motor hace: ¡Brrroum!

Atención a lo siguiente:

  • Como naveEspacial está compuesto por motor, tiene todas las funciones asociadas a motor
  • Como naveEspacial tiene todas las funciones asociadas a motor, cumple todos los interfaces que cumple motor

De este modo, logramos cumplir todas las interfaces de los tipos que usamos en la composición.

Definiendo interfaces a posteriori

Uno de los usos más habituales (en mi caso) del desacoplamiento entre interfaces y tipos es la definición de interfaces para hacer testing.

Como no hemos entrado en los temas de testing, y no quiero introducir más de un concepto cada vez, voy a utilizar un fichero estándar con un main para ilustrar este caso.

Vamos a «lanzar una prueba» sobre nuestro tipo motor.


package main
import "fmt"
type motor struct {
ruido string
}
func (m motor) hacerRuido() string {
return fmt.Sprintf("Este motor hace: %s", m.ruido)
}
type ruidoso interface {
hacerRuido() string
}
type mockMotor string
func (mm mockMotor) hacerRuido() string {
return string(mm)
}
func main() {
var miMotor = motor{"¡Brrroum!"}
var ruidoResultante = mockMotor("Este motor hace: ¡Brrroum!")
fmt.Println("Probando ruido del motor")
if ejercitarRuido(miMotor) == ejercitarRuido(ruidoResultante) {
fmt.Println("Todo OK")
} else {
fmt.Println("Este motor no suena bien")
}
}
func ejercitarRuido(r ruidoso) string {
return r.hacerRuido()
}

Por supuesto, la prueba es correcta:

Probando ruido del motor
Todo OK

De este ejemplo quiero que prestemos atención a lo siguiente:

  • Hemos definido el interfaz ruidoso, el tipo mockMotor y la función ejercitarRuido después de definir motor. En un caso más real, lo definiríamos en otro fichero
  • La función ejercitarRuido no sabe si lo que recibe es un mock (una implementación simplificada) o la implementación real

Lo que nos ha posibilitado trabajar de esta manera ha sido la definición a posteriori de un interfaz que contenga las operaciones de la clase que queremos probar.

Y hasta aquí la unión de composición e interfaces. Espero que no haya nada extraño, ya que todos los conceptos se han visto antes. Si lo ha habido, dad uso a la sección de comentarios 😉

Una de las características que más me gusta de Go es la composición. Es sin duda una forma muy elegante de generar funcionalidad a partir de componentes existentes. En palabras de algunos, es «como la herencia, pero mejor«. Tal vez sea un poco exagerado, pero sin duda hay una predilección por la «composición sobre la herencia«.

Simplificándolo mucho, la composición consiste en meter unas estructuras dentro de otras y beneficiarse de sus atributos y funciones.

Yo vengo del mundo Java y no soy de entrar en polémicas (bueno, un poco sí), así que dejaré que gente más sabia que yo dé caña.

Disclaimer: no he encontrado ninguna referencia fiable a la siguiente anécdota, pero me parece veraz.

Cuando preguntaron a James Gosling (creador de Java) «¿Qué cambiaría si volviera a hacer Java?». Su respuesta fue «Dejaría fuera las clases». Antes de que a nadie le dé un infarto, quiero matizar que lo hizo refiriéndose principalmente a la relaciónextends entre ellas, prefiriendo relaciones implements y relaciones basadas en interfaces.

No lo digo yo. Lo dice el papá de la criatura.

En Go no tenemos extends ni implements. Lo primero, porque no hay herencia; lo segundo, porque la relación entre tipos e interfaces no hace falta indicarla (esto lo veremos en otra ocasión).

Una composición sencillita

Primero vamos a utilizar una composición de las de toda la vida. Como las que hacíamos en C; sin orientación a objetos ni nada parecido.


package main
import "fmt"
type empuñadura string
type filo string
type espada struct {
e empuñadura
f filo
}
type vaina string
type espadaEnvainada struct {
e espada
v vaina
}
func (ee espadaEnvainada) describe() string {
return fmt.Sprintf(
"Espada Envainada, con puño de '%s', filo de '%s' y vaina de '%s'",
ee.e.e, ee.e.f, ee.v)
}
func main() {
var miEspadaEnvainada = espadaEnvainada{
espada{"Cuero trenzado", "Acero"}, "Cuero curtido",
}
fmt.Println(miEspadaEnvainada.describe())
}

Espero que a nadie le sorprenda nada del código anterior ni de la salida del mismo.

Espada Envainada, con puño de 'Cuero trenzado', filo de 'Acero' y vaina 
de 'Cuero curtido'

Composición sin nombres

Vamos a intentar «llevarnos» las funciones de los componentes de la espadaEnvaindada. Para ello, vamos a definir describe en empuñadura y vamos a intentar utilizarlo:


package main
import "fmt"
type empuñadura string
func (e empuñadura) describe() string {
return fmt.Sprintf("La empuñadura es de '%s'", e)
}
type filo string
type espada struct {
e empuñadura
f filo
}
type vaina string
type espadaEnvainada struct {
e espada
v vaina
}
func main() {
var miEspadaEnvainada = espadaEnvainada{
espada{"Cuero trenzado", "Acero"}, "Cuero curtido",
}
fmt.Println(miEspadaEnvainada.describe())
}

No podemos hacerlo porque no disponemos de esa operación:

# command-line-arguments
/tmp/sandbox346670597/main.go:29: miEspadaEnvainada.describe undefined 
(type espadaEnvainada has no field or method describe)

…pero basta un pequeño cambio para conseguirlo: eliminar el nombre de los elementos de las estructuras.


package main
import "fmt"
type empuñadura string
func (e empuñadura) describe() string {
return fmt.Sprintf("La empuñadura es de '%s'", e)
}
type filo string
type espada struct {
empuñadura
filo
}
type vaina string
type espadaEnvainada struct {
espada
vaina
}
func main() {
var miEspadaEnvainada = espadaEnvainada{
espada{"Cuero trenzado", "Acero"}, "Cuero curtido",
}
fmt.Println(miEspadaEnvainada.describe())
}

Y tenemos:

La empuñadura es de 'Cuero trenzado'

Por supuesto, el resultado no incluye información del filo ni la vaina, pero podemos re-implementar la función. Especial detalle a que ahora tenemos que referirnos al tipo en lugar de hacerlo al campo: usamos ee.espada.filo en lugar de ee.e.f.


package main
import "fmt"
type empuñadura string
func (e empuñadura) describe() string {
return fmt.Sprintf("La empuñadura es de '%s'", e)
}
type filo string
type espada struct {
empuñadura
filo
}
type vaina string
type espadaEnvainada struct {
espada
vaina
}
func (ee espadaEnvainada) describe() string {
return fmt.Sprintf(
"Espada Envainada, con puño de '%s', filo de '%s' y vaina de '%s'",
ee.espada.empuñadura, ee.espada.filo, ee.vaina)
}
func main() {
var miEspadaEnvainada = espadaEnvainada{
espada{"Cuero trenzado", "Acero"}, "Cuero curtido",
}
fmt.Println(miEspadaEnvainada.describe())
}

Y volvemos a tener el mismo resultado que antes, ya que describe definido para espadaEnvainada «sobreescribe» el definido para empuñadura.

Espada Envainada, con puño de 'Cuero trenzado', filo de 'Acero' y vaina 
de 'Cuero curtido'

La composición da para mucho más, pero con esto creo que podemos darnos por iniciados.

El concepto detrás de una interfaz es sencillo: poder trabajar con un objeto (una instancia de un tipo) sin preocuparnos por cómo está implementado, pudiendo intercambiar tipos distintos con sólo conocer sus operaciones. Los que hayan visto Airbag conocen la importancia de el concepto (no he podido resistirme).

La principal características de las interfaces en Go es que se evaluan al vuelo. No es necesario hacer referencia a una interfaz de forma explícita (por ejemplo, en Java se utiliza implements para identificar qué interfaces implementa una clase)

Utilizando un tipo concreto

Empezamos por lo sencillo: creamos dos tipos y les asociamos operaciones. Esto lo hemos hecho ya varias veces así que no nos vamos a entretener mucho.


package main
import "fmt"
type perro string
func (p perro) hablar() {
fmt.Println("Guau, guau! Soy un perro llamado", p)
}
type gato string
func (g gato) hablar() {
fmt.Println("Miau, miau! Soy un gato llamado:", g)
}
func main() {
var miPerro perro = "Lassie"
miPerro.hablar()
var miGato gato = "Mizifú"
miGato.hablar()
}

De momento no hemos hecho uso de los interfaces. La salida es muy predecible:

Guau, guau! Soy un perro llamado Lassie
Miau, miau! Soy un gato llamado: Mizifú

Utilizando un interfaz único para intercambiarlos a todos

En el ejemplo anterior, tanto gatos como perros son capaces de hablar. Vamos a utilizar un interfaz para utilizar operaciones de perro y de gato.

Las interfaces se definen indicando un nombre y un conjunto de operaciones.


package main
import "fmt"
type hablador interface {
hablar()
}
type perro string
func (p perro) hablar() {
fmt.Println("Guau, guau! Soy un perro llamado", p)
}
type gato string
func (g gato) hablar() {
fmt.Println("Miau, miau! Soy un gato llamado:", g)
}
func miMascotaHablando(h hablador) {
fmt.Print("A ver mi peluchito cómo habla… ")
h.hablar()
}
func main() {
var miPerro perro = "Lassie"
var miGato gato = "Mizifú"
miMascotaHablando(miPerro)
miMascotaHablando(miGato)
}

Lo que hemos hecho es pasar un perro o un gato como parámetros a una función que ejecuta uno de los métodos comunes que tienen.

La salida muestra cómo la función miMascotaHablando funciona para cualquier tipo que implemente hablar().

A ver mi peluchito cómo habla... Guau, guau! Soy un perro llamado Lassie
A ver mi peluchito cómo habla... Miau, miau! Soy un gato llamado: Mizifú

Lo más importante es ver que ni perro ni gato saben que existe la interfaz hablador. La interfaz hablador la podríamos haber definido en un fichero distinto, en un paquete distinto y dos años después de haber creado perro y gato. Esto da mucho, pero que mucho juego, pero vamos a dejar las interfaces por hoy.

Las interfaces, como casi todos los elementos de Go, son sencillas de entender y de utilizar, pero requiere práctica sacarles todo su potencial.

En el siguiente post hablaremos de la composición y la compararemos un poco con la herencia.

Ejemplos de lo que NO se puede hacer

Ejemplos de lo que NO se puede hacer

Como ya hemos visto cómo se asocian operaciones a estructuras, veremos qué pasa si asociamos operaciones a tipos predefinidos, a tipos propios, a arrays, a slices (trozos) y a distintos punteros.

Para mí, asociar operaciones a tipos de datos que no sean estructuras de datos es algo que no se me suele ocurrir. Como me figuro (al menos, espero) que no soy el único, he querido dedicar un post a esas «rara avis» que son para mí las operaciones asociadas a arrays….

Esto nos va a dar una idea bastante clara de la flexibilidad que tenemos a nuestra disposición en Go.

Operaciones en tipos básicos

Con esto acabamos rápido: no se puede. Veamos qué dice el compilador cuando definimos un método sobre int.


package main
import "fmt"
func (i int) doblar() int {
return i * 2
}
func main() {
a := 2
fmt.Println("Vamos a probar:", a.doblar())
}

El error que recibimos es:

prog.go:5: cannot define new methods on non-local type int

El motivo por el que no podemos añadir operaciones a int es que es un tipo “no local”.

Esta situación afecta a todos los tipos básicos, pero también a cualquier tipo definido en otro paquete.

La moraleja de la historia es que sólo podemos definir operaciones sobre los tipos que tenemos en nuestro paquete.

Definir operaciones en tipos propios

Este es el caso (que funciona) más sencillo de todos. Cuando definimos un tipo nuevo, podemos asociarle operaciones con la siguiente notación:


package main
import "fmt"
type nuevoEntero int
func (x nuevoEntero) elDoble() nuevoEntero {
x = x * 2
return x
}
func main() {
var varEntera nuevoEntero = 42
fmt.Println("La respuesta es", varEntera)
fmt.Println("y el doble es", varEntera.elDoble())
fmt.Println("La respuesta sigue siendo", varEntera)
}

El resultado es:

La respuesta es 42
y el doble es 84
La respuesta sigue siendo 42

El uso de la operación es muy razonable y sencillo de entender: varEntera.elDoble().

Sin embargo, hay que destacar que no podemos modificar la variable porque estamos definiendo la operación sobre el tipo; no sobre un puntero al tipo.

Dicho de otro modo: las operaciones que realicemos dentro de la función elDoble se realizarán sobre una copia del nuevoEntero, que se pasa al la función elDoble.

A continuación vemos cómo saltarnos esta restricción.

Definir operaciones en punteros

Vamos a definir una operación sobre un puntero a un tipo en lugar de hacerlo sobre el tipo en sí y veremos qué pasa.


package main
import "fmt"
type nuevoEntero int
func (x *nuevoEntero) elDoble() nuevoEntero {
(*x) = (*x) * 2
return (*x)
}
func main() {
var varEntera nuevoEntero = 42
fmt.Println("La respuesta es", varEntera)
fmt.Println("y el doble es", varEntera.elDoble())
fmt.Println("la variable ha cambiado. Ahora vale:", varEntera)
}

El resultado es:

La respuesta es 42
y el doble es 84
la variable ha cambiado. Ahora vale: 84

Podemos utilizar la operación sobre el puntero a nuevoEntero como si fuera el tipo directamente. Como la operación se realiza sobre el puntero al tipo, la función puede modificar el valor de manera permanente. Además, no hace falta que utilicemos no necesitamos escribir (&varEntera).elDoble(); el compilador es suficientemente listo como para entenderlo correctamente.

Para que la funcionalidad fuera más clara en un desarrollo, yo haría que elDoble pasara a llamarse doblar, y que no devolviera ningún valor, pero esa es otra historia y debe ser contada en otra ocasión.

Definir operaciones en arrays

El siguiente tipo sobre el que vamos a hacer operaciones es un array.

Recordemos que los arrays son tipos de datos con un número de elementos fijo, y que el tipo [3]int es distinto al tipo [4]int.

La primera aproximación es utilizar [3]int como tipo para definir los métodos.


package main
import "fmt"
func (x [3]int) elDoble() [3]int {
for i, _ := range x {
x[i] = x[i] * 2
}
return x
}
func main() {
var miArray [3]int = [3]int{1, 2, 3}
fmt.Println("miArray", miArray)
fmt.Println("y el doble es", miArray.elDoble())
fmt.Println("la variable sigue siendo", miArray)
}

No podemos, porque estamos definiendo operaciones sobre un tipo que no tiene nombre:

/tmp/sandbox052857290/main.go:5: invalid receiver type [3]int ([3]int is an unnamed type)

Personalmente comprendo los problemas de asociar una operación a un tipo de datos que no tenga un nombre, pero la primera vez que hice el ejemplo anterior pensaba que funcionaría…

Vamos a darle nombre al tipo y a repetir la jugada:


package main
import "fmt"
type tresEnteros [3]int
func (x tresEnteros) elDoble() [3]int {
for i, _ := range x {
x[i] = x[i] * 2
}
return x
}
func main() {
var miArray tresEnteros = [3]int{1, 2, 3}
fmt.Println("miArray", miArray)
fmt.Println("y el doble es", miArray.elDoble())
fmt.Println("la variable sigue siendo", miArray)
}

Ahora sí funciona, pero no se modifica la variable.

miArray [1 2 3]
y el doble es [2 4 6]
la variable sigue siendo [1 2 3]

Podemos definir la operación sobre el puntero (como en el caso anterior) para que las modificaciones sean permanentes.


package main
import "fmt"
type tresEnteros [3]int
func (x *tresEnteros) elDoble() [3]int {
for i, _ := range x {
x[i] = x[i] * 2
}
return *x
}
func main() {
var miArray tresEnteros = [3]int{1, 2, 3}
fmt.Println("miArray", miArray)
fmt.Println("y el doble es", miArray.elDoble())
fmt.Println("la variable ahora es:", miArray)
}

Que devuelve:

miArray [1 2 3]
y el doble es [2 4 6]
la variable ahora es: [2 4 6]

Un detalle curioso: para operar con las posiciones del array utilizamos x[i]. No es necesario utilizar (*x)[i], pero quien lo prefiera por claridad, lo puede usar.

Definir operaciones en slices

Con los Slices pasa lo mismo que con los arrays. Es necesario definir un tipo de datos y darle nombre para poder asociarle operaciones (nos vamos a saltar el ejemplo en que el compilador protesta porque el tipo []int no tiene nombre).


package main
import "fmt"
type variosEnteros []int
func (x variosEnteros) elDoble() []int {
for i, _ := range x {
x[i] = 2 * x[i]
}
return x
}
func main() {
var miArray variosEnteros = []int{1, 2, 3}
fmt.Println("miArray", miArray)
fmt.Println("y el doble es", miArray.elDoble())
fmt.Println("la variable ha cambiado", miArray)
}

Devuelve:

miArray [1 2 3]
y el doble es [2 4 6]
la variable ha cambiado [2 4 6]

Es importante entender que los slices ya contienen un puntero a la información (como ya hemos visto). Por eso se modifica el contenido del slice aunque la operación no se defina sobre un puntero al slice.

Hasta aquí por hoy. Hemos definido operaciones sobre todos los tipos de datos sobre los que no se suelen definir operaciones (os recuerdo que yo vengo de Java) y creo que eso nos va a ayudar a entender un poco mejor Go. En el próximo post definiremos operaciones sobre estructuras de datos con un poco más de detalle que hasta ahora.

Punteros slices y arraysTeniendo Arrays, Slices y punteros para elegir, parece complicado decidir qué pasar a una función. Las ideas son sencillas y en el fondo la decisión no es tan complicada.

Para tomar la decisión correcta, necesitamos entender qué implicaciones tiene cada caso, y para ello vamos a empezar recordando qué es cada uno de esos elementos.

Los bloques de construcción

Vamos a ver cada tipo por separado para asegurarnos de que los entendemos antes de jugar con ellos.

Array

En Go (y en muchos otros lenguajes), un array es un bloque estático que contiene elementos de un determinado tipo. Especial énfasis en estático y en tipo. Para Go, un array de 3 enteros es un tipo distinto de un array de 4 enteros.

Como Go realiza todas las comprobaciones de tipos durante la compilación, no almacena en memoria el tipo del array. Si tenemos un array de enteros, y cada entero ocupa 4 bytes, un array de 10 elementos ocupará 40 bytes. Tendremos nuestros 10 elementos uno detrás de otro y nada más.

Como Go inicia todos los valores al valor cero, todas las posiciones del array valdrán 0.

Vamos con un ejemplo.


package main
import "fmt"
func main() {
var arrayDeDiezEnteros [10]int
fmt.Println("Este es tu array, pequeño padawan:", arrayDeDiezEnteros)
}

El resultado es el siguiente:

Este es tu array, pequeño padawan: [0 0 0 0 0 0 0 0 0 0]

Ccómo mola que Go te formatee los arrays por defecto, en lugar de ponerte una dirección de memoria como en otros lenguajes.

Slice (Trozo)

Cuando creamos un Slice, tenemos tres elementos de información muy pequeños y muy sencillos:

  • Tamaño: cuántas posiciones tenemos a nuestra disposición
  • Capacidad: cuántas posiciones hemos reservado por si nuestro Slice necesita crecer
  • Referencia al array: Un enlace (puntero) al array que contiene la información. No podemos acceder a este Array directamente.

Vamos a por el ejemplo:


package main
import "fmt"
func main() {
var sliceDeDosHastaCuatro = make([]int, 2, 4)
fmt.Println("Este es tu slice, pequeño padawan:", sliceDeDosHastaCuatro)
}

El resultado es:

Este es tu slice, pequeño padawan: [0 0]

¡Eh, me han robado dos posiciones!

Calma, calma. En realidad están ahí, pero como todavía no están en uso, no se muestran.

Para los incrédulos que necesiten pruebas de que están ahí.


package main
import "fmt"
func main() {
var sliceDeDosHastaCuatro = make([]int, 2, 4)
fmt.Println("Este es tu slice, pequeño padawan:", sliceDeDosHastaCuatro)
fmt.Println("Este array tiene una capacidad de", cap(sliceDeDosHastaCuatro))
fmt.Println("De momento estamos utilizando", len(sliceDeDosHastaCuatro))
}

Nos dice que las posiciones están ahí, a nuestra disposición para cuando las necesitemos.

Este es tu slice, pequeño padawan: [0 0]
Este array tiene una capacidad de 4
De momento estamos utilizando 2

Las posiciones «ocultas» permiten ampliar rápidamente (sin tener que copiar el contenido a otro array más grande) mediante un append. No vamos a entrar en detalles en este post. Hay un artículo muy bueno sobre el append en el blog de Go (en inglés).

Nota para los lectores asiduos: efectivamente, estoy claudicando en lo de llamar a los Slices «trozos».

Punteros

Los punteros no son más que referencias. Referencias a lo que queramos. Vamos a jugar con un puntero a un entero.


package main
import "fmt"
func main() {
var miPuntero *int
fmt.Println("Tu puntero no vale nada, pequeño padawan:", miPuntero)
fmt.Println()
var miEntero int = 42
miPuntero = &miEntero
fmt.Println("Este es tu entero, pequeño padawan:", miEntero)
fmt.Println("Este es tu puntero, pequeño padawan:", miPuntero)
fmt.Println("Este es el entero al que apunta tu puntero, pequeño padawan:", *miPuntero)
fmt.Println("\nEl mundo cambia, pequeño padawan. Tu entero también\n")
miEntero = 33
fmt.Println("Este es tu entero, pequeño padawan:", miEntero)
fmt.Println("Este es tu puntero, pequeño padawan:", miPuntero)
fmt.Println("Este es el entero al que apunta tu puntero, pequeño padawan:", *miPuntero)
}

El resultado:

Tu puntero no vale nada, pequeño padawan: 

Este es tu entero, pequeño padawan: 42
Este es tu puntero, pequeño padawan: 0x10328100
Este es el entero al que apunta tu puntero, pequeño padawan: 42

El mundo cambia, pequeño padawan. Tu entero también

Este es tu entero, pequeño padawan: 33
Este es tu puntero, pequeño padawan: 0x10328100
Este es el entero al que apunta tu puntero, pequeño padawan: 33

Vamos a ver lo que hemos hecho paso a paso:

Primero, hemos definido el puntero. Cuando definimos un puntero, aun no apunta a nada (razonable, ¿no?).

Después, hemos creado una variable y hemos hecho que nuestro puntero apunte a esa variable. El puntero tiene ahora un valor, que es un gurruñito precioso e ininteligible, pero que nos permite saber si cambia o no cambia (voy adelantando que el puntero no va a cambiar).

Por último, hemos cambiado el valor de la variable entera sin cambiar el puntero. El valor del puntero es el mismo, pero podemos acceder a través de él al valor actualizado de la variable.

Por cierto, hemos visto la notación de punteros como el que no quiere la cosa :

  • El asterisco (*) significa «dime a qué apunta esto»
  • El ampersand (&) significa «dame un puntero a esto». Para los talibanes del idioma, decir que el símbolo en castellano se llama «et»

Pasando valores

En Go, todo lo que pasamos a una función lo pasamos por valor. Es decir, creamos una copia de nuestra variable y es la que enviamos.

Vamos a ver qué pasa con los distintos tipos que hemos visto.

Pasando un Array

Vamos a ver un ejemplo de una función a la que pasamos un array.


package main
import "fmt"
func main() {
var arrayDeDiezEnteros [10]int
fmt.Println("Este es tu array al principio, pequeño padawan:", arrayDeDiezEnteros)
cambiarPosiciones(arrayDeDiezEnteros)
fmt.Println("Este es tu array al final, pequeño padawan:", arrayDeDiezEnteros)
}
func cambiarPosiciones(array [10]int) {
fmt.Println("Este es el array al principio de la función: ", array)
array[3] = 42
fmt.Println("Este es el array al final de la función: ", array)
}

El resultado es el siguiente:

Este es tu array al principio, pequeño padawan: [0 0 0 0 0 0 0 0 0 0]
Este es el array al principio de la función:  [0 0 0 0 0 0 0 0 0 0]
Este es el array al final de la función:  [0 0 0 42 0 0 0 0 0 0]
Este es tu array al final, pequeño padawan: [0 0 0 0 0 0 0 0 0 0]

Los cambios que hemos hecho sólo han durado hasta que ha terminado la función.

A la función le hemos pasado una copia del array que teníamos. Por tanto, todas las modificaciones que hemos hecho en la función (que tampoco eran muchas) se han perdido cuando ha desaparecido esa copia que ha utilizado la función.

Un detalle importante: tanto la variable como el parámetro de la función tienen exactamente el mismo tipo: [10]int. Un array de 9 posiciones sería para Go un tipo distinto (para eso están los Slices).

Pasando un Slice

Vamos a hacer lo mismo de antes, pero con un Slice.


package main
import "fmt"
func main() {
var sliceDeTresHastaCuatro = make([]int, 3, 4)
fmt.Println("Este es tu slice al principio, pequeño padawan:", sliceDeTresHastaCuatro)
cambiarPosiciones(sliceDeTresHastaCuatro)
fmt.Println("Este es tu slice al final, pequeño padawan:", sliceDeTresHastaCuatro)
}
func cambiarPosiciones(slice []int) {
fmt.Println("Este es el slice al principio de la función: ", slice)
slice[1] = 42
fmt.Println("Este es el slice al final de la función: ", slice)
}

La salida es distinta a la anterior:

Este es tu slice al principio, pequeño padawan: [0 0 0]
Este es el slice al principio de la función:  [0 0 0]
Este es el slice al final de la función:  [0 42 0]
Este es tu slice al final, pequeño padawan: [0 42 0]

En este caso, los cambios han persistido después de la llamada a la función.

A la función el hemos pasado una copia del Slice, que contiene una referencia al array que teníamos. Por tanto, la modificación se hace en el array de la función main.

Pasando un puntero

Por si el ejemplo del Slice es complejo, vamos a hacer pruebas con enteros. El ejemplo es más largo, pero mucho más sencillo.


package main
import "fmt"
func main() {
var miEntero int = 42
var miPuntero *int = &miEntero
fmt.Println("Este es tu entero al principio, pequeño padawan:", miEntero)
fmt.Println("Este es tu puntero al principio, pequeño padawan:", miPuntero)
fmt.Println("Este es el entero al que apunta tu puntero al principio, pequeño padawan:", *miPuntero)
fmt.Println("\nTu entero no va a cambiar de momento, pequeño padawan.\n")
cambiarEnteroPasado(miEntero)
fmt.Println("Este sigue siendo tu entero, pequeño padawan:", miEntero)
fmt.Println("Este sigue siendo tu puntero, pequeño padawan:", miPuntero)
fmt.Println("Este sigue siendo el entero al que apunta tu puntero, pequeño padawan:", *miPuntero)
fmt.Println("\nTu entero ahora sí va a cambiar, pequeño padawan.\n")
cambiaEnteroApuntado(miPuntero)
fmt.Println("Finalmente, tu entero es:", miEntero)
fmt.Println("Finalmente, tu puntero es:", miPuntero)
fmt.Println("Finalmente, el entero al que apunta tu puntero es:", *miPuntero)
}
func cambiarEnteroPasado(i int) {
fmt.Println("He recibido el valor", i)
i = i + 17
fmt.Println("He transformado el valor en", i)
}
func cambiaEnteroApuntado(ptrI *int) {
fmt.Println("He recibido el puntero", ptrI)
fmt.Println("El valor apuntado es", *ptrI)
*ptrI = *ptrI + 17
fmt.Println("He transformado el puntero en", ptrI)
fmt.Println("He transformado el valor apuntado en", *ptrI)
}

Os recuerdo que cuando en la función aparece *int significa «este parámetro es un puntero a un entero».

La salida es:

Este es tu entero al principio, pequeño padawan: 42
Este es tu puntero al principio, pequeño padawan: 0x10328100
Este es el entero al que apunta tu puntero al principio, pequeño padawan:42

Tu entero no va a cambiar de momento, pequeño padawan.

He recibido el valor 42
He transformado el valor en 59
Este sigue siendo tu entero, pequeño padawan: 42
Este sigue siendo tu puntero, pequeño padawan: 0x10328100
Este sigue siendo el entero al que apunta tu puntero, pequeño padawan: 42

Tu entero ahora sí va a cambiar, pequeño padawan.

He recibido el puntero 0x10328100
El valor apuntado es 42
He transformado el puntero en 0x10328100
He transformado el valor apuntado en 59
Finalmente, tu entero es: 59
Finalmente, tu puntero es: 0x10328100
Finalmente, el entero al que apunta tu puntero es: 59

Los primeros cambios los hemos hecho en una función que recibe un entero (una copia de un entero). Por tanto, no se modifica nada.

Los cambios de después, los hemos hecho con una función que recibe un puntero (una copia de la referencia a un entero). Por eso las modificaciones perduran.

Claro, claro, pero ¿qué le paso a una función; un valor o una referencia?

Vamos a decidir si utilizamos un puntero o no cuando pasemos algo a una función.

En líneas generales (independiente del lenguaje)

¿Queremos guardar los cambios que haga la función? Si la respuesta es , pasamos una referencia

…else…

¿La función tiene que modificar la información que le demos? Si la respuesta es , pasamos un valor

…else…

¿Nos preodupa el rendimiento? Cuando hacemos una copia de una variable para pasarla como parámetro, eso tiene un coste. Si la respuesta es , pasamos una referencia. Si la respuesta es NO, pasamos un valor.

En el caso de Go

En el caso de Go, aplica todo lo anterior. El único matiz es que los Slices son variables pequeñas que contienen una referencia a un array. Por tanto, pasar un Slice, es pasar una referencia a un array. Por muy grande que sea el array al que referencie, copiar un Slice es muy rápido.

Bueno. Hasta aquí por hoy. Prometo que quería hacer un post cortito, pero he preferido hacer un post claro. Espero que os ayude.

¡Un saludo y hasta pronto!

Cada cual construye su aprendizaje sobre sus conocimientos, experiencias y opiniones.

Cuando expliqué de la orientación a objetos (de Java) a mi primer grupo de alumnos (¡hace más de una década; cómo pasa el tiempo!), cada cual intentó entenderlo a su manera. Para una alumna, la orientación a objetos era como tablas Access. Las clases eran el modelo de datos, y los objetos el contenido de la tabla. Para otro era un Excel con fórmulas. Para otro (que había programado en C), los objetos no eran más que estructuras de datos con funciones «pegadas».

No toda la orientación a objetos se puede implementar con Excel (al menos no de manera razonable), pero creo que es bueno empezar por una idea sencilla

Vamos a explorar la orientación a objetos de Go como funciones «pegadas» a datos y veremos hasta dónde nos lleva.

Un tipo nuevo en la ciudad

Vamos a crear un tipo y vamos a asociarle una operación. Algo muy sencillo. Para ello, vamos a basar nuestro nuevo tipo en int.

Primero definamos el tipo:


package main
import "fmt"
type nuevoEntero int
func main() {
var varEntera nuevoEntero = 42
fmt.Println("La respuesta es", varEntera)
}

Por supuesto, el resultado es:

La respuesta es 42

Una operación en el tipo nuevo

Ahora vamos a añadirle una operación sencilla:


package main
import "fmt"
type nuevoEntero int
func (x nuevoEntero) elDoble() nuevoEntero {
return x * 2
}
func main() {
var varEntera nuevoEntero = 42
fmt.Println("La respuesta es", varEntera)
fmt.Println("y el doble es", varEntera.elDoble())
}

El resultado es:

La respuesta es 42
y el doble es 84

¿Y para qué creamos un tipo nuevo?

Creamos un tipo nuevo porque no podemos definir operaciones sobre los tipos definidos en otros paquetes.

Es decir: si defino el tipo en mi paquete, puedo ponerle las funciones que quiera; si el tipo se ha definido en otro paquete, no.

Vamos a demostrarlo:


package main
import "fmt"
func (i int) printX() {
fmt.Println("Hola", i)
}
func main() {
i := 1
i.printX
}

El resultado es un precioso error diciendo que no podemos darle más funcionalidades a un int (bueno, más de uno, pero pondré el que nos interesa):

cannot define new methods on non-local type int

Vamos a un ejemplo clásico

Personalmente suelo preferir crear ejemplos «raros»; distintos a los que se suelen utilizar en la mayor parte de libros que leo. Lo hago porque creo que los ejemplos «típicos» suelen estar bastante viciados. Sin embargo, voy a utilizar un clásico para ilustrar que se pueden definir funciones asociadas a una estructura:


package main
import "fmt"
type rectángulo struct {
lado1, lado2 float32
}
func (r rectángulo) área() float32 {
return r.lado1 * r.lado2
}
func (r rectángulo) perímetro() float32 {
return (2 * r.lado1) + (2 * r.lado2)
}
func main() {
var miRectángulito rectángulo = rectángulo{3.0, 4.0}
fmt.Println("Este es mi rectángulo", miRectángulito)
fmt.Println("¡Hay otros muchos, pero este es el mío!")
fmt.Println("Su área es", miRectángulito.área())
fmt.Println("Su perímetro es", miRectángulito.perímetro())
}

Efectivamente, disfruto mucho escribiendo programas con tildes (pero sólo para jugar) 😉

El resultado es:

Este es mi rectángulo {3 4}
¡Hay otros muchos, pero este es el mío!
Su área es 12
Su perímetro es 14

Modificando el objeto

Si hasta aquí está todo claro, vamos a dar el siguiente pasito: vamos a crear una función que modifica el propio objeto.


package main
import "fmt"
type rectángulo struct {
lado1, lado2 float32
}
func (r rectángulo) área() float32 {
return r.lado1 * r.lado2
}
func (r rectángulo) perímetro() float32 {
return (2 * r.lado1) + (2 * r.lado2)
}
func (r *rectángulo) agrandar() {
r.lado1 = 2 * r.lado1
r.lado2 = 2 * r.lado2
}
func main() {
var miRectángulito rectángulo = rectángulo{3.0, 4.0}
fmt.Println("Este es mi rectángulo", miRectángulito)
fmt.Println("Su área es", miRectángulito.área())
fmt.Println("Su perímetro es", miRectángulito.perímetro())
miRectángulito.agrandar()
fmt.Println("\nAgrando mi rectángulo\n")
fmt.Println("Este es mi rectángulo agrandado", miRectángulito)
fmt.Println("Su área es", miRectángulito.área())
fmt.Println("Su perímetro es", miRectángulito.perímetro())
}

El resultado es:

Este es mi rectángulo {3 4}
Su área es 12
Su perímetro es 14

Agrando mi rectángulo

Este es mi rectángulo agrandado {6 8}
Su área es 48
Su perímetro es 28

Supongo que con esto, las cosas quedan bastante claras. Hemos definido un tipo nuevo y le hemos asociado operaciones. Cuando hemos querido modificar el objeto hemos tenido que definir la operación sobre un puntero al objeto.

Para aquellos que se hayan quedado con ganas de más, en la próxima entrega seguiremos.

¡Hasta pronto!

No InheritanceGo tiene muchas peculiaridades y su enfoque hacia la orientación a objetos es una de ellas. Para los impacientes, diré que Go tiene algunas de las características típicas esperables de un lenguaje orientado a objetos, pero las implementa sin herencia y eso hace que no todo sea como esperamos.

El hecho de que Go no tenga herencia no es significa que le «falte» una funcionalidad. Es una decisión deliberada del lenguaje (entre otras cosas, mejora el rendimiento en la ejecución). Vamos a ver qué es lo que supone para un desarrollador.

Lo que un humilde servidor pensaba al oír «Orientación a Objetos» (OO)

Un humilde servidor viene de Java, donde todas las estructuras de datos son clases y son hijas de la clase Object.

Las clases se pueden extender (pueden tener clases hijas). Una clase hija tiene todos los métodos de la clase padre más los que tú le pongas y pueden re-escribir los métodos de la clase padre. Por ejemplo, el método «toString» puede devolver cosas muy distintas dependiendo de la clase, pero siempre existe (porque está en la clase Object).

Si necesitamos más, podemos implementar interfaces. Definimos un grupo de métodos y les ponemos un nombre (y un lacito si queremos). Más tarde, podemos decir que nuestra nueva clase implementa esa interfaz. Eso significa que nos comprometemos a escribir código para todos y cada uno de los métodos definidos en la interfaz.

El tipo de OO que puedes conseguir con Go

Las variables que defines tienen un tipo asociado. Por ejemplo, si definimos var i int, el tipo de i es int. Eso significa que con i podremos hacer las cosas que nos permitan los int.

Reconozco que es una pequeña perogrullada, pero nos sirve de base. Si cambiamos int por cualquier otro tipo, nuestra variable i tendrá todas las operaciones definidas para ese otro tipo (y ninguna más).

En Go no podemos extender un tipo (crear un tipo «hijo»). Si creamos un tipo basándonos en otro, copiaremos su estructura, pero no nos llevaremos sus operaciones.

Podemos escribir type tipoChu tipoGuan. A partir de ese momento, las variables de tipo tipoChu tendrán los atributos de tipoGuan, pero no las operaciones.

Si no hay herencia, ¿qué hay de la orientación a objetos? Bueno, nos quedan las interfaces, que funcionan de una manera distinta pero muy interesante.

Al igual que en Java, en Go las interfaces definen conjuntos de operaciones. A diferencia de Java, las interfaces se cumplen de manera implícita.

Esto significa que yo no tengo que decir que mi tipo cumple una interfaz determinado. Simplemente lo cumple y ya está. Un ejemplo sencillo es la interfaz io.Reader, que define una única operación: Read. Todo lo que implemente Read es un io.Reader.

He ignorado los parámetros de Read por simplificar, pero para implementar la interfaz hay que implementar las funciones con el conjunto de parámetros (entrada y salida) correctos.

Preguntas que me hago

Ahora que sabemos cuáles son las bases de la OO en Go, surgen bastantes preguntas sobre qué implicaciones tiene y cómo se comporta este modelo con el desarrollo de software moderno.

A continuación, os dejo un par de preguntas que yo me he hecho. No tienen por qué ser las más acuciantes para los demás, pero a mí me han inquietado. Os dejo también las respuestas que he encontrado.

¿Pero entonces, Go es orientado a objetos o no?

Me temo que esta pregunta es más filosófica que otra cosa. Básicamente depende de los matices de la pregunta.

Go tiene su propia respuesta en las FAQ del lenguaje. Para quienes no queráis hacer click, la respuesta es «Sí y no» 😉

Según la Wikipedia, a Go le faltan algunas de las funcionalidades básicas para ser considerado un lenguaje orientado a objetos. Por ejemplo, la herencia.

Sobre este tema estuvimos charlando José Luis Esteban y Javier Vélez después de la charla de GOMAD de Toni Cárdenas «Desarrollo web desde un lenguaje de sistemas«. Reconozco que yo me pasé la mayor parte del tiempo escuchando, y que no llegamos a ninguna conclusión definitiva. Pero me pareció curiosísimo ver cuántos puntos de vista se pueden utilizar para definir qué necesita un lenguaje para ser oficialmente considerado «Orientado a Objetos».

Para mí la respuesta es sencilla: Go es un lenguaje que admite una parte importante de las funcionalidades de la orientación a objetos, pero que de ningún modo está orientado hacia los objetos.

Para responder esta pregunta para otros, tiraría de mis raíces gallegas y diría: «Depende de lo que signifique para ti que un lenguaje sea orientado a objetos».

¿Qué pasa con los patrones de diseño?

En el primer meetup de go al que asistí (¿Por que Go? de Victor Castell), surgió esta pregunta: «¿Qué hacemos con las soluciones ampliamente aceptadas, como por ejemplo los patrones de diseño del GoF?».

El tema de los patrones de diseño también los he visto comentados en la charla de Rob Pike «Public static void«. En ella, Rob cita a Peter Norvig (director de investigación de Google) diciendo que los patrones son una demostración de las debilidades de un lenguaje.

A mí personalmente me gustan los patrones de diseño y no voy a cargar contra ellos. Son soluciones probadas a problemas «típicos».

Personalmente creo que a medida que se vaya extendiendo el uso del lenguaje, se encontrarán problemas «típicos» y se aprenderá cuáles son las soluciones «típicas». Tal vez esto no sean «Patrones de diseño», sino «Buenas prácticas», pero no voy a entrar en discusiones semánticas…

¡Madre mía, otra vez que he escrito un tostón sin una sola línea de código! Prometo que en el próximo post jugaremos un poco. Veremos algo de código y le daremos una vuelta al tema de las interfaces.

Las librería estándar de Go son bastante completas. Yo que vengo de Java, creo que las librerías del lenguaje deben ser muy potentes. Sin embargo, como comentaba en mi charla sobre concurrencia, parece que Java se pasa.

Digamos que Java tiene muchas funcionalidades: cosas que funcionan, pero Go tiene utilidades: cosas realmente útiles para programar.

Para finalizar nuestro viaje por la concurrencia en Go, vamos a ver algunos elementos de las librerías de sincronización.

Operaciones atómicas

Para estar seguros de que no hay dos go-rutinas manipulando simultáneamente una variable y dejándola en un estado inváildo, es necesario protegerla. Go tiene funciones para operar con tipos básicos de manera atómica en el paquete sync/atomic.

Veamos un ejemplo:


package main
import (
"fmt"
"sync/atomic"
)
type contador int32
func (c *contador) incrementar() int32 {
return atomic.AddInt32((*int32)(c), 1)
}
func (c *contador) decrementar() int32 {
return atomic.AddInt32((*int32)(c), -1)
}
func (c *contador) get() int32 {
return atomic.LoadInt32((*int32)(c))
}
func main() {
var cont contador
fmt.Println("Pequeño Padawan, el contador es", cont)
cont.incrementar()
fmt.Println("Pequeño Padawan, el contador es", cont)
cont.decrementar()
fmt.Println("Pequeño Padawan, el contador es", cont)
}

La salida es:

Pequeño Padawan, el contador es 0
Pequeño Padawan, el contador es 1
Pequeño Padawan, el contador es 0

Hay que tener en cuenta que estas operaciones son de bajo nivel y probablemente deberíamos utilizar alternativas como canales.

Grupo de espera

Un motivo para tener un contador compartido como el del ejemplo anterior es saber cuántos hilos asociados a una tarea siguen activos. El objetivo de saber cuántos hilos hay activos es poder avanzar cuanto todos los hilos han terminado.

En Go, para esperar a la finalización de un grupo de go-rutinas, se utiliza un WaitGroup. En él, indicamos a cuántos hilos queremos esperar y dejamos a cada uno de los hilos que notifique su finalización.

A continuación, un pequeño ejemplo en el que no utilizamos la sincronización:


package main
import (
"fmt"
)
func funcioncilla() {
fmt.Println("Padawan en una funcioncilla")
}
func main() {
go funcioncilla()
go funcioncilla()
}

La salida si lo ejecutamos en el playground de go es:

[no output]

El motivo por el que no hay salida es que el hilo principal ha finalizado antes de que las go-rutinas hayan tenido tiempo de escribir nada en pantalla.

A continuación, el mismo ejemplo pero utilizando un WaitGroup:


package main
import (
"fmt"
"sync"
)
func funcioncilla(wg *sync.WaitGroup) {
fmt.Println("Padawan en una funcioncilla")
wg.Done()
}
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
go funcioncilla(wg)
go funcioncilla(wg)
wg.Wait()
}

En este caso, la salida sí es la esperada:

Padawan en una funcioncilla
Padawan en una funcioncilla

Cerrojos

Una de las herramientas clásicas para la sincronización son los cerrojos. Cerramos un cerrojo y hacemos que los hilos que lleguen después de nosotros tengan que esperar hasta que lo abramos.

Vamos a ver dos cosas importantes en el siguiente ejemplo:

  1. Vamos a ver un Mutex en uso (como era de esperar)
  2. Vamos a utilizar defer

defer es una sentencia que ejecuta una función (función diferida) justo antes de que termine la función en que se utiliza (tal y como se explica en la documentación de Go). Lo fabuloso de defer es que da igual dónde, cuándo y por qué se salga de la función. Nos permite tener varios return, se ejecuta aunque haya un panic (un error grave que hace que se salga de la función) y nos permite tener la línea de cierre de conexión, de apertura de candado o de liberación de recursos muy poco después de la de reserva de esos recursos.

Veámoslo:


package main
import (
"fmt"
"sync"
"time"
)
func usarCerrojo(c *sync.Mutex, i int, wg *sync.WaitGroup) {
c.Lock()
defer c.Unlock()
time.Sleep(time.Second)
fmt.Println("Padawan", i, "usando el cerrojo")
wg.Done()
}
func main() {
cerrojo := new(sync.Mutex)
wg := new(sync.WaitGroup)
wg.Add(4)
go usarCerrojo(cerrojo, 1, wg)
go usarCerrojo(cerrojo, 2, wg)
go usarCerrojo(cerrojo, 3, wg)
go usarCerrojo(cerrojo, 4, wg)
wg.Wait()
}

La salida es:

Padawan 1 usando el cerrojo
Padawan 2 usando el cerrojo
Padawan 3 usando el cerrojo
Padawan 4 usando el cerrojo

Por supuesto, las líneas están separadas un segundo entre sí.

Una y no más [Santo Tomás]

Go tiene una utilidad que me resulta curiosa. Permite definir una función que se ejecutará exclusivamente la primera vez que se invoque. Si se vuelve a invocar, simplemente no se ejecutará.

Es útil por ejemplo cuando tenemos dos hilos haciendo un cálculo, o buscando la información en un recurso distinto. Nos interesa que nos de el resultado el primero que pueda, pero que no nos den el resultado más de una vez.

Vamos a ver un ejemplo con un sólo hilo. La primera llamada a Do es la única que se toma en cuenta:


package main
import (
"fmt"
"sync"
)
func main() {
var unaYNoMas sync.Once
tocalaSam := func() { fmt.Println("Tocando") }
tocalaOtraVezSam := func() { fmt.Println("Volviendo a tocarla") }
unaYNoMas.Do(tocalaSam)
unaYNoMas.Do(tocalaSam)
unaYNoMas.Do(tocalaOtraVezSam)
}

La salida sólo muestra la ejecución de la primera función llamada:

Tocando

Como siempre, Go da para mucho más (especialmente en cuanto a concurrencia se refiere), pero hay que poner el punto en algún sitio. En la sección de recursos hay bastante información adicional sobre la concurrencia en Go para quien se anime.

Desde mi punto de vista, los «select» son la piedra de toque de la concurrencia en Go. Comprenderlos y explotarlos es la clave para sacar a Go todo su jugo para la programación concurrente.

No voy a hacer una disertación sobre cómo implementar «select» en distintos lenguajes, ni cómo está implementado en Go. Símplemente diré que a mí me parece complicado, y que quien quiera que le de una vuelta.

Nos vamos a centrar en jugar un poco con el select. Hemos visto ya bastante código y estamos en condiciones para hacer cosas un poco más complicadas que de costumbre.

Canales como sincronización de lectura y escritura

con un canal, sincronizamos operaciones de lectura y escritura y con un select, agrupamos varios canales. Vamos a ver un ejemplo de sincronización de distintas operaciones a través de un ejemplo de contador con acceso sincronizado.

El siguiente código presenta un contador sincronizado. Las operaciones de lectura y escritura están implementadas para poderse llamar por varias go-rutinas simultáneamente.


package main
import "fmt"
type contador struct {
l, s chan int //leer y sumar
v int //valor
}
func (c *contador) leer() int {
return <-c.l
}
func (c *contador) sumar(in int) {
c.s <- in
}
func (c *contador) matar() {
close(c.l)
close(c.s)
}
func newContador() *contador {
res := new(contador)
res.l = make(chan int)
res.s = make(chan int)
go res.gestionar()
return res
}
func (c *contador) gestionar() {
for {
select {
case i, abierto := <-c.s:
if !abierto {
break
}
c.v += i
case c.l <- c.v:
}
}
}
func main() {
c := newContador()
fmt.Println("Contador", c.leer())
c.sumar(42)
fmt.Println("Contador", c.leer())
c.matar()
fmt.Println("Contador", c.leer())
}

Como se ve en el código, la go-rutina que ejecuta las operaciones (línea 31), está ejecutando una de las operaciones o está ejecutando la otra (o está esperando a que le indiquen qué operación ejecutar). Como tenemos un único hilo de ejecución realizando todas las operaciones, no necesitamos una sincronización adicional.

En lugar de definir secciones críticas alrededor de una estructura de datos, y permitir que haya muchos hilos tratando de acceder, definimos un único hilo «autorizado» y permitimos que todos los hilos le hagan peticiones mediante canales.

Cuando bloquearse está bien, pero sólo un ratito

Hay situaciones en las que esperar indefinidamente puede ser un error (mensaje NO patrocinado por el Dr. Amor).

A mí el primer motivo que se me ocurre para cancelar una espera es la liberación de recursos para evitar ataques de denegación de servicio (uno tiene sus dejes…), pero supongo que hay casos más intuitivos para los demás.

Siendo muy impaciente

En Go podemos definir un valor de default para salir en caso de que el resto de opciones sea bloqueante.


package main
import "fmt"
func main() {
c := make(chan bool)
select {
case d := <-c:
fmt.Println("He leído", d)
default:
fmt.Println("Aquí nadie dice nada")
}
}

Como no se ha escrito nada en el canal, no hay nada que leer. La salida es:

Aquí nadie dice nada

Esto es útil (por ejemplo) para confirmar que no hay peticiones pendientes, y que podemos dedicarnos a realizar otras cosas.

Siendo sólo un poco impaciente

Hay situaciones donde podemos esperar, pero solo un poco. En caso de que el emisor tarde más de un determinado tiempo, podemos enviarle al garete y pasar a otra cosa. Lo que comúnmente denominamos un «Time-out». Esta ide al explica muy bien Andrew Gerrand (del equipo de Go en Google) aquí.

Vamos a esperar por dos canales y vamos a enviar un valor por uno pasado 1 segundo.


package main
import "fmt"
import "time"
func main() {
c := make(chan bool)
cEspera := make(chan bool, 1) //Atención al buffer
go espera(cEspera)
select {
case d := <-c:
fmt.Println("He leído", d)
case <-cEspera:
fmt.Println("Me he cansado de esperar")
}
}
func espera(cEspera chan bool) {
time.Sleep(1 * time.Second)
//Como el canal tiene un buffer, no es bloqueante y la función termina
cEspera <- true
}

El resultado es el siguiente (después de un segundo de espera, claro):

Me he cansado de esperar

También puede implementarse de manera más compacta utilizando time.After, que nos devuelve un canal en el que se ejecuta básicamente el mismo código que teníamos:


package main
import "fmt"
import "time"
func main() {
c := make(chan bool)
select {
case d := <-c:
fmt.Println("He leído", d)
case <-time.After(time.Second):
fmt.Println("Me he cansado de esperar")
}
}

Select sobre un array de canales

Una de las limitaciones de los select es que sólo podemos esperar en un número limitado de canales, y además solo podemos esperar por tantos como hayamos definido en el código.

Es decir, no podemos esperar en un array (o un trozo) de canales.

No voy a decir que esperar por un array de canales sea una buena idea, pero vamos a hacer una implementación para saltarnos la limitación del lenguajes. Para ello, vamos a multiplexar un array de canales en uno solo.


package main
import (
"fmt"
"sync"
)
func multiplexar(inputs []chan int, output chan int) {
var group sync.WaitGroup
for i := range inputs {
group.Add(1)
go func(input <-chan int) {
for val := range input {
output <- val
}
group.Done()
}(inputs[i])
}
go func() {
group.Wait()
close(output)
}()
}
func main() {
in := make([]chan int, 10)
for i := 0; i < len(in); i += 1 {
in[i] = make(chan int)
go func(c chan int, i int) {
c <- i
close(c)
}(in[i], i)
}
out := make(chan int)
multiplexar(in, out)
for x := range out {
fmt.Println("He leído", x)
}
}
//Extraído de: http://stackoverflow.com/a/10985364/1600421

Esta implementación no es gratis. Creamos una go-rutina por cada canal en el array. Las go-rutinas son baratas (4-8 kB, pero no gratis. Además, hay que tener en cuenta el retraso en tiempo de ejecución).

Las herramientas de go para la concurrencia son muy flexibles y potentes. Seguramente se te estarán ocurriendo otras formas (creativas o curiosas) de aprovecharlas. Mi consejo es que juegues, aunque sea en el playground. No te arrepentirás y te lo pasarás bien.

En el próximo post, veremos algunas de las herramientas que nos da la librería de sincronización.

Voy a cambiar sutilmente la orientación del blog con este post. Aunque sea un poco narcisista, voy a escribir sobre la charla que he dado en el meetup GOMAD.

Un servidor dando la charla de concurrencia

Un servidor dando la charla (sorprendido ante las cámaras)

El objetivo de la charla es ayudar a los que nos acercamos a Go desde Java a entender el modelo de concurrencia, y compartir los pasos que un humilde servidor ha seguido para aprender (o al menos comprender) el modelo de concurrencia de Go.

Como creo que el propio lenguaje tiene mucha y muy clara información sobre la concurrencia, quería dar un enfoque distinto y opté por seguir los tutoriales de Java en lugar de seguir los de Go.

A un nivel más general, hay una charla muy amena de Francesc Campoy que compara los dos lenguajes (Go for Javaneros). Mi charla está centrada en el tema de concurrencia.

El foro

La charla se enmarca en el meetup de Go de Madrid, llamado GOMAD. Ahora mismo, somos algo más de 100 gophers (que no está nada mal), aunque el número de asistentes fue inferior a 30. En su defensa diré que llovía y no apetecía salir de casa.

Desde mi humilde punto de vista, el nivel técnico de los asistentes es asombroso. No sólo por sus conocimientos de Go en profundidad (que también), sino por sus conocimientos horizontales sobre desarrollo, testing, otros lenguajes de programación, frameworks…

Es fantástico formar parte de este grupo de eruditos.

El coso

La charla tuvo lugar en las oficinas de Tuenti en el centro de Madrid, y como siempre, todo estaba perfectamente preparado.

Contamos como en otras ocasiones con una sala excelentemente acondicionada, con un sistema de proyección y sonido muy bueno…y con aperitivos y refrescos.

Saber que todo estará preparado para cuando llegues, y no tener que preocuparse por nada más que por la presentación es genial.

Desde aquí quiero agradecer al equipo de Tuenti por cedernos sus instalaciones.

La charla

La charla fue mejor de lo que me esperaba (no me quedé en blanco, no me tropecé, ni pasó nada raro ;)). Los asistentes asentían con la cabeza y yo iba soltando el rollo. Contaba con que el nivel de los asistentes era alto, pero pensaba que dedicaríamos más tiempo a explicar varias veces alguno de los conceptos (de Go o de Java). Al final todos los asistentes pareció coincidir con que las explicaciones se habían entendido, que el modelo de Go quedaba claro y el de Java (que era nuevo para algunos) también.

Por supuesto, siempre cabe la posibilidad de que los asistentes mintieran como bellacos y sean más hábiles mintiendo que yo detectando las mentiras. Pero creo que no.

Las cervezas de después

Lo mejor de la charla fue por supuesto la sesión cervecera de después. En un ambiente más distendido, las ideas, las preguntas, las reflexiones y las opiniones fluyen mejor. Me gustaron sobre todo algunos comentarios del estilo: «Muchas gracias por la charla. Ahora sé un poco sobre la concurrencia de Java (que antes no conocía), y tengo claro que me gusta infinitamente más el de Go.».

…y el material

Para los que no estuvieron, la charla está disponible en: Charla: Concurrencia Go Vs Java

Por si el material puede ser útil para alguna otra charla, he distribuido la charla mediante CC para que pueda servir a la comunidad en la medida de lo posible. ¡Me encantará saber quién la usa!Se puede hacer un fork del repositorio de GitHub.

En el siguiente post, volveremos con la temática habitual para seguir aprendiendo Go.