
Aprendiendo Concurrencia en Golang

Quería aprender un nuevo lenguaje, así que después de probar algunos, terminé con Golang como uno de mis favoritos por su simplicidad y capacidades. Tiene características que no había usado en años, como multithreading y concurrencia.
Golang (o Go) soporta concurrencia mediante hilos ligeros llamados goroutines. Son diferentes del multithreading tradicional de Java, donde hay que manejar sincronización y coordinación para gestionar recursos compartidos de forma segura. Las goroutines son ligeras, las gestiona el runtime de Go, y son más baratas de crear y manejar.
El paralelismo es hacer varias cosas simultáneamente. La concurrencia es lidiar con varias cosas a la vez. En ambos casos, no conocemos el orden de ejecución: no sabemos qué pasará primero ni qué terminará antes.
Imagina que cocinas: preparas una sopa, una ensalada y una tortilla. Eres una sola persona, pero preparas varios platos. Podrías terminar primero la ensalada, la sopa o la tortilla… no hay garantía. Esto es concurrencia: lidias con varias cosas a la vez. Cuando tu pareja viene a ayudarte, eso ya es paralelismo.

Recuerdo haber construido un juego similar en Java cuando aprendía multithreading hace diez años. Aprovecho esta oportunidad para hacerlo de nuevo con Go moderno.
Construi un juego de terminal que simula una carrera de caballos. Cada caballo es una goroutine que corre en una matriz bidimensional compartida. Cuando un caballo llega al final, notifica al canal compartido con los demas caballos (que corren en diferentes procesos) y todos se detienen, mostrando al ganador.
Separé el código en cuatro áreas para ayudar a visualizarlo:
- Punto de entrada
- Generando el tablero
- Renderizando el juego
- Moviendo los caballos

Punto de entrada
La estructura Horse representa cada Caballo en la carrera.
El juego consiste en una lista de líneas, en las cuales cada Caballo está corriendo.
type Horse struct {
Name string // El nombre del caballo
Line int // La línea de competición
}
func (h Horse) Letter() string {
return fmt.Sprintf("%c", h.Name[0])
}
func (h Horse) Equals(other *Horse) bool {
return other != nil &&
h.Line == other.Line &&
h.Name == other.Name
}
func (h Horse) String() string {
return fmt.Sprintf("%s (line:%d)", h.Name, h.Line)
}Puedes generar un nuevo proceso usando la palabra clave go al invocar cualquier función.
En este juego, esto se usa 1) para renderizar el juego RenderGame() y 2) para el movimiento de cada caballo startRuningHorseInLine(). El objetivo es mantener la “renderización” y la “lógica de movimiento” trabajando en paralelo.
func main() {
const lines, lineLength = 12, 30
board := NewRaceBoard(lines, lineLength)
go RenderGame(board)
winnerChan := make(chan Horse)
for line := range board {
// cada caballo será movido en diferentes procesos
go startRunningHorseInLine(board, line, winnerChan)
}
// esperar hasta que un caballo llegue al final
winner := <-winnerChan
// renderizar una última vez para asegurar el último estado del tablero
RenderRaceBoard(board, &winner)
fmt.Println("Race finished!")
fmt.Printf("# Winner: %s\n", winner)
}Generando el tablero
El tablero de carreras es una matriz bidimensional de punteros a Horses. Cada línea “contiene” un solo Caballo: solo un puntero apunta a un Caballo real, el resto son nil. Al generar el Tablero, creamos un Caballo en la primera posición de cada línea.
func NewRaceBoard(lines, lineLength int) [][]*Horse {
board := make([][]*Horse, lines)
for line := range board {
board[line] = make([]*Horse, lineLength)
board[line][0] = &Horse{
Name: generateHorseName(),
Line: line,
}
}
return board
}Los nombres se generan aleatoriamente usando HorseNames.
var HorseNames = [][2]string{
{"Alloping", "Giggles"},
{"A-lot", "Gallop"},
{"BoJack", "Jack"},
{"Baroness", "Belle"},
// ...
}
func generateHorseName() string {
name := HorseNames[rand.Intn(len(HorseNames))][0]
surname := HorseNames[rand.Intn(len(HorseNames))][1]
return name + " " + surname
}Renderizando el juego
Los métodos RenderGame(), renderRaceBoard(), renderRaceLine() y renderRacePosition() están separados para que cada uno tenga una responsabilidad clara: renderizar su sujeto correspondiente.
RenderGame()se está ejecutando en otro proceso usandogo.
func RenderGame(board [][]*Horse) {
for {
time.Sleep(renderDelay * time.Millisecond)
RenderRaceBoard(board, nil)
}
}
func RenderRaceBoard(board [][]*Horse, winner *Horse) {
// usar un "buffer de string" para guardar todo el estado del tablero
// para que luego podamos usar una sola llamada IO para renderizarlo
var buffer bytes.Buffer
buffer.WriteString("\n")
for line := range board {
renderRaceLine(board, line, &buffer, winner)
}
clearScreen()
fmt.Println(buffer.String())
}
func clearScreen() {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
}
func renderRaceLine(
board [][]*Horse,
line int,
buffer *bytes.Buffer,
winner *Horse,
) {
buffer.WriteString(fmt.Sprintf(" %.2d | ", line))
var current Horse
for col := range board[line] {
h := renderRacePosition(board, line, col, buffer, winner)
if h != nil {
current = *h
}
}
buffer.WriteString(fmt.Sprintf("| %s", current.Name))
if current.Equals(winner) {
buffer.WriteString(" [Won!]")
}
buffer.WriteString("\n")
}
func renderRacePosition(
board [][]*Horse,
line, col int,
buffer *bytes.Buffer,
winner *Horse,
) *Horse {
if board[line][col] == nil {
buffer.WriteString(" ")
return nil
}
current := board[line][col]
if current.Equals(winner) {
removeChars(buffer, col+1)
for range board[line] {
buffer.WriteString("-")
}
}
buffer.WriteString(current.Letter())
return current
}Moviendo los caballos
En main(...), el winnerChan es un canal compartido que usará el primer Caballo que llegue a la última posición de su línea.
Cada Caballo ejecuta un bucle hasta llegar al final de la línea o recibir (vía winnerChan) el mensaje de que otro Caballo ya ganó. Hasta entonces, cada caballo se mueve de forma independiente, durmiendo milisegundos aleatorios antes de avanzar a la siguiente posición.
startRuningHorseInLine()se ejecuta en otro proceso usandogo.
func main() {
//...
winnerChan := make(chan Horse)
for line := range board {
// cada caballo será movido en diferentes procesos
go startHorseRunning(board, line, winnerChan)
}
// esperar hasta que un caballo llegue al final
winner := <-winnerChan
//...
}
func startRunningHorseInLine(board [][]*Horse, line int, winnerChan chan Horse) {
for {
select {
case <-winnerChan: // verificar si otro caballo terminó
return // en tal caso, entonces detener el bucle for
default:
time.Sleep(time.Millisecond * time.Duration(rand.Intn(maxSleepDelay)))
moveHorseOnePos(board, line, winnerChan)
}
}
}
func moveHorseOnePos(board [][]*Horse, line int, winnerChan chan Horse) {
cols := len(board[line])
for col := cols - 1; col > 0; col-- {
if board[line][col-1] == nil {
continue
}
// aquí identificamos que hay un caballo en
// la siguiente columna, así que lo movemos a la
// columna actual, y ponemos `nil` en la otra
board[line][col] = board[line][col-1]
board[line][col-1] = nil
if col+1 == cols {
winnerChan <- *board[line][col]
}
break
}
}Código fuente
El código de este post es una versión simplificada. Si quieres ver el código completo funcionando, está aquí: Chemaclass/go-horse-racing.
Gracias a mi antiguo Team Lead, Andrei Boar, que me ayudó a revisar mi solución original y proporcionó una solución alternativa (más simple y mejor) que apliqué a mi código. Lo principal que aprendí fue usar un
chan Horsepara pasar el Caballo ganador desdemain(), en vez de usar unchan booly unsync.WaitGroupentre todos los hilos.