Streaming bidireccional en gRPC con Go

Resumen

Implementar server streaming y streaming bidireccional en gRPC con Go te permite intercambiar datos en pequeños pedazos entre cliente y servidor, ya sea en una sola dirección o de forma simultánea. Si ya completaste los métodos unario y de client streaming, este es el paso que te faltaba para dominar los cuatro patrones de comunicación que ofrece gRPC.

Aquí vas a ver cómo se escribe el cliente para ambos casos, cómo se manejan los streams con stream.Recv y stream.Send, y por qué necesitas goroutines y channels cuando la comunicación es bidireccional.

Cómo implementar server streaming en gRPC desde el cliente

El server streaming aplica cuando el cliente hace una petición única y el servidor responde con múltiples mensajes. Un caso típico es traer una lista de estudiantes asociados a un test [01:00].

La función doServerStreaming recibe como parámetro un testpb.TestServiceClient y llama al método GetStudentsPerTest. Antes de invocarlo, construyes el request con el test_id correspondiente, por ejemplo T1, y luego ejecutas la llamada que devuelve un stream y un posible error.

¿Qué hace stream.Recv en gRPC? Es el método que lee mensajes del servidor uno a uno. Devuelve el mensaje y un error; si el error es io.EOF, significa que el servidor terminó de enviar datos.

El flujo de lectura se hace dentro de un for indefinido porque no sabes cuántos estudiantes vas a recibir. En cada iteración:

  • Llamas a stream.Recv() para obtener el mensaje y el error.
  • Si el error es io.EOF, haces break para salir del ciclo.
  • Si hay otro tipo de error, ejecutas log.Fatalf con un mensaje descriptivo.
  • Si todo salió bien, imprimes la respuesta del servidor.

Al probarlo, el cliente recibe al primer estudiante, después al segundo, luego al tercero, y finaliza limpiamente cuando el servidor cierra el stream [02:30].

Cómo implementar streaming bidireccional en gRPC con goroutines

El streaming bidireccional es el patrón más potente: cliente y servidor pueden enviar y recibir mensajes de forma simultánea sobre el mismo stream. Para emular ese comportamiento en Go, necesitas dos goroutines, una para escribir y otra para leer, coordinadas mediante un channel [03:30].

Por qué usar un channel de control vacío

La función doBidirectionalStreaming invoca a TakeTest, que abre un stream donde se envían respuestas y se reciben preguntas o feedback. Como vas a lanzar dos goroutines concurrentes, defines un canal de tipo chan struct{} que no transporta datos, solo sirve para bloquear el programa principal hasta que la lectura termine.

go waitChannel := make(chan struct{})

Este patrón es muy común en Go cuando solo te interesa señalizar finalización, no transmitir información. Después construyes la respuesta, por ejemplo un TakeTestRequest con answer = 42, y defines un total de cuatro preguntas a responder.

¿Para qué sirve un chan struct{} en Go? Es un channel sin payload que se usa como mecanismo de señalización. Ocupa cero bytes de memoria y permite sincronizar goroutines sin enviar datos reales.

Cómo separar la goroutine de envío y la de recepción

La primera goroutine itera con un for desde i = 0 hasta i < numero_de_preguntas y en cada vuelta ejecuta stream.Send(answer). Para observar el comportamiento asíncrono, agregas un time.Sleep que emula un tiempo de respuesta entre envíos [04:30].

La segunda goroutine itera de forma indefinida porque no sabes cuántas respuestas vendrán del servidor. Su lógica es:

  1. Llamar a stream.Recv() para obtener res y err.
  2. Si el error es io.EOF, hacer break.
  3. Si el error es distinto de nulo, ejecutar log.Fatalf y romper el ciclo.
  4. Si todo es correcto, imprimir la respuesta del servidor.

Al terminar el for de lectura, cierras el waitChannel. En el cuerpo principal de la función, bloqueas con una lectura sobre ese mismo channel para que el programa no termine antes de que el servidor cierre el stream.

Cuando ejecutas go run client/main.go, ves cómo se envían las respuestas con el valor 42 y cómo el servidor devuelve sus mensajes en paralelo, demostrando que el mismo stream funciona para lectura y escritura simultáneas [06:30].

Cuáles son los cuatro métodos de gRPC y cuándo usar cada uno

Con esta implementación cierras el recorrido por los cuatro patrones de comunicación que ofrece gRPC, cada uno pensado para un escenario distinto.

  • Unario: un request y un response. Es el equivalente más cercano a una REST API tradicional y funciona bien cuando el intercambio es puntual.
  • Client streaming: el cliente envía múltiples mensajes en pedazos pequeños y el servidor responde una sola vez al final. Útil para cargar grandes volúmenes de datos por partes.
  • Server streaming: el cliente envía una sola petición y el servidor responde con un flujo continuo de mensajes. Ideal para listados largos o feeds de datos.
  • Streaming bidireccional: cliente y servidor intercambian mensajes simultáneamente sobre el mismo stream. Perfecto para chats, juegos en tiempo real o cualquier interacción conversacional.

¿Cuál es la diferencia entre stream.Send y stream.Recv? Send empuja un mensaje hacia el otro extremo del stream; Recv lee el siguiente mensaje entrante. En streaming bidireccional, ambos métodos coexisten en el mismo stream.

Ya tienes los cuatro métodos implementados tanto en servidor como en cliente. ¿Cuál de los cuatro patrones crees que vas a usar más en tus proyectos? Cuéntame en los comentarios qué caso de uso tienes en mente.