Aquel viejo bolero, que comienza con la frase del título de este artículo, me recuerda una anécdota, pero no se las contaré porque no es el lugar ni el momento adecuado 😜.
Pero también nos da el pie para continuar con esta guía de introducción al lenguaje de programación Go.
El problema que vamos a resolver en esta ocasión es crear un servidor TCP que emite la hora a través de un socket. Este servidor debe ser capaz de atender a varios clientes en forma simultánea.
Afortunadamente, la biblioteca estándar de Go permite escribir este tipo de servidores en pocas líneas. El código es el siguiente:
package main
import (
"io"
"log"
"net"
"time"
)
func main() {
listener, err := net.Listen("tcp", "localhost:8888")
if err != nil {
log.Fatal(err) // finaliza el programa
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
log.Print(err) // por ejemplo el cliente cierra la conexión
continue
}
handleConn(conn)
}
}
func handleConn(c net.Conn) {
defer c.Close()
for {
_, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
if err != nil {
return
}
time.Sleep(1 * time.Second)
}
}
La función main()
es relativamente fácil de entender. Primero abrimos un listener
, que es la abstracción que implementa Go para un servidor que estará escuchando en el puerto 8888 en localhost
. Luego tenemos un ciclo infinito en que aceptamos una conexión (con el método listener.Accept()
) e invocamos a la función handleConn()
que es donde implementamos la lógica de nuestro servicio.
La función handleConn()
recibe una conexión, que corresponde a abstracción provista por la función Accept()
provista por el paquete estándar net
. En este caso, la conexión se modela como un stream de datos, que ha sido abierto en la función Accept()
, cuando el cliente se conecta a nuestro servicio.
Después de escribir algo en este stream debemos cerrarlo, para esto usaremos la sentencia defer c.Close()
, esto indica que el cierre de la conexión se realizará al finalizar esta función. En general, defer
ejecuta la llamada a la función que recibe cuando el programa sale del ámbito (o bloque de código) en que se declara (señalado por las llaves {}
).
Luego entramos en otro ciclo, en que escribimos la hora cada 1 segundo. La llamada time.Now()
nos retorna la hora, y la función time.Sleep(1 * time.Second)
detiene la ejecución del servicio por un segundo. Noten el uso de la unidad de tiempo, si quisiéramos que durmiese por dos horas entonces debemos escribir time.Sleep(2 * time.Hour)
.
Si el cliente cierra la conexión al momento de tratar de escribir en el stream se producirá un error, y retornamos, con lo que conexión se cerrará gracias al defer c.Close().
El problema es que tal como está implementado, el servidor es capaz de atender solo una conexión a la vez, tal como se aprecia en este video:
Lo que queremos es que cada conexión sea atendida de modo concurrente. En otros lenguajes iniciaríamos un thread (o hilo) por cada conexión establecida. La solución en Go es sorprendentemente simple, basta con cambiar la llamada a handleConn()
por esto:
go handleConn(conn)
Con esto hemos procesado handleConn()
como una “go routine”, es similar a un thread liviano, y el efecto se nota en este otro video:
El modelo de concurrencia de Go es simple, pero bastante poderoso, y vamos a seguir explorándolo en los siguientes artículos de esta serie.
Si te gustó te invito a suscribirte:
O puedes compartir este artículo con colegas o amigos:
Hay que ir a Guarilihue por la anécdota?? 😊