Skip to content

Instantly share code, notes, and snippets.

@leo-aa88
Created February 2, 2025 18:28
Show Gist options
  • Save leo-aa88/86f00356c63f76791d3b238a05546b12 to your computer and use it in GitHub Desktop.
Save leo-aa88/86f00356c63f76791d3b238a05546b12 to your computer and use it in GitHub Desktop.
Uma introdução fácil a goroutines

Uma introdução fácil a goroutines

Se você está começando a programar em Go, provavelmente já ouviu falar de goroutines. Mas o que são elas e como funcionam? Neste artigo, vamos explorar esse conceito fundamental da linguagem de forma simples e prática.

Começando com o básico

Vamos começar com um exemplo simples:

package main

import (
    "fmt"
)

func goroutine() {
    fmt.Println("Ola mundo da goroutine!!")
}

func main() {
    fmt.Println("Ola mundo da funcao main!!!!!")
    go goroutine()
}

A keyword go (que dá nome à linguagem) é usada para iniciar uma goroutine para uma determinada função. Isso significa que a função vai rodar de forma CONCORRENTE com a função main. Mas ao rodar esse programa, percebemos algo curioso:

> Ola mundo da funcao main!!!!!

Ei, cadê o output da goroutine? Por que não vemos "Ola mundo da goroutine!!"?

O problema da sincronização

Toda vez que criamos uma goroutine em Go, o runtime do Go precisa chamar o scheduler (basicamente um orquestrador das goroutines). Isso demanda um certo tempo até ele alocar recursos da CPU para a goroutine criada, além de outras tarefas. O problema é que, até tudo isso acontecer, a função main já terminou sua execução!

WaitGroups ao resgate!

Para resolver isso, precisamos de um mecanismo de SINCRONIZAÇÃO que diga para a função main ESPERAR que a goroutine que a gente criou termine a execução e, só assim, termine a execução do programa. É aí que entram os waitGroups (grupos de espera, literalmente) em Go.

Pense em como se fosse um contador (é quase literalmente isso):

  • Ele começa em 0
  • Enquanto ele for maior que 0, a gente diz pra função main esperar pra continuar sua execução e terminar a execução do programa
  • Quando volta a 0, o programa pode terminar

Vejamos como implementar:

package main

import (
    "fmt"
    "sync"
)

func goroutine(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Ola mundo da goroutine!!")
}
func main() {
    var wg sync.WaitGroup
    fmt.Println("Ola mundo da funcao main!!!!!")
    wg.Add(1)
    go goroutine(&wg)
    wg.Wait()
}

Entendendo as operações do WaitGroup

Ok, vamos entender o que cada operação do waitGroup faz:

  • wg.Add(1) adiciona 1 ao contador, para sinalizar que estamos criando uma goroutine
  • wg.Done() diminui o contador em 1, sinalizando que a goroutine terminou
  • wg.Wait() pede para a função main esperar que esse contador vá a zero

Quando chamamos wg.Wait(), estamos dizendo explicitamente para a função main: "Ei, espere até que todas as goroutines terminem!". Só depois que o contador chegar a zero (ou seja, todas as goroutines chamaram wg.Done()), a função main pode continuar sua execução.

Por que usar ponteiros?

Agora estamos usando o tipo waitGroup da biblioteca sync em Go, que nos provê várias ferramentas de sincronização. Perceba que agora a goroutine requer um parâmetro a mais, que é um PONTEIRO para uma variável do tipo waitGroup para indicar que ela termina execução (usamos a keyword defer para ela sinalizar isso somente no FINAL de sua execução).

Por que passamos por referência usando & e a goroutine toma como argumento um ponteiro? Ora, se não passássemos por referência, a goroutine criaria uma CÓPIA do waitGroup e não usaria o mesmo da função main, e iniciaríamos o contador do zero! AMBAS as função main e goroutine precisam se basear no MESMO contador, que se encontra num local único da memória, e não em várias instâncias dele. Ao passar por referência, usamos a MESMA referência, o MESMO endereço na memória!

Cuidado com os deadlocks!

Se não chamarmos wg.Add(1) antes de criar a goroutine, teremos um problema sério. O contador ficaria negativo ao chamar wg.Done() (lembre que ele começa em zero), causando um DEADLOCK. O runtime do Go vai nos dar um erro nesse caso.

Criando múltiplas goroutines

Agora vamos complicar um pouco - que tal criar várias goroutines ao mesmo tempo?

package main

import (
    "fmt"
    "sync"
)

func goroutine(num int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("goroutine ", num)
}

func main() {
    var wg sync.WaitGroup
    fmt.Println("Ola mundo da funcao main!!!!!")
    
    for i := 1; i < 6; i++ {
        wg.Add(1)
        go goroutine(i, &wg)
    }
    wg.Wait()
}

O comportamento não-determinístico

Algo interessante acontece quando rodamos esse código várias vezes. A ordem das goroutines muda! Por exemplo:

> Ola mundo da funcao main!!!!!
> goroutine 5
> goroutine 3
> goroutine 2
> goroutine 1
> goroutine 4

Isso acontece porque temos um comportamento NÃO-DETERMINÍSTICO do scheduler do Go. Os recursos de CPU, alocados pelo sistema operacional, estão sempre flutuando, e o scheduler aloca esses recursos de forma diferente para cada goroutine, buscando a máxima eficiência.

Controlando a ordem com channels

E se quisermos que as goroutines executem em sequência? Para isso, vamos usar channels (canais) - uma feature especial do Go que permite passar valores entre goroutines.

package main

import (
    "fmt"
    "sync"
)

func goroutine(num int, wg *sync.WaitGroup, ch chan bool) {
    defer wg.Done()
    fmt.Println("esperando permissao pra iniciar ...")
    <-ch
    fmt.Println("goroutine ", num)
    ch <- true
}

func main() {
    var wg sync.WaitGroup
    ch := make(chan bool, 1)
    ch <- true
    fmt.Println("Ola mundo da funcao main!!!!!")
    
    for i := 1; i < 6; i++ {
        wg.Add(1)
        go goroutine(i, &wg, ch)
        ch <- true
        fmt.Println("esperando a goroutine anterior terminar ...")
        <-ch
    }
    wg.Wait()
}

Entendendo os channels em detalhes

Vamos analisar cada parte do código com channels:

Na função main, primeiro criamos nosso channel:

ch := make(chan bool, 1)
ch <- true

Repare que criamos um channel BUFFERED usando o argumento 1 em make(chan bool, 1). Isso é crucial pois indica que o channel vai armazenar apenas UMA variável do tipo bool por vez. Se não fizéssemos isso, ele armazenaria várias variáveis do tipo bool (como se fosse um array) e não teríamos o comportamento desejado - na verdade, o programa daria erro!

Depois de criar o channel, já passamos o valor true dentro da própria função main usando ch <- true. Note que aqui não usamos o sinal = ou :=, pois os channels são variáveis que se comportam de forma diferente das demais.

No for loop, temos uma lógica especial:

for i := 1; i < 6; i++ {
    wg.Add(1)
    go goroutine(i, &wg, ch)
    ch <- true
    fmt.Println("esperando a goroutine anterior terminar ...")
    <-ch
}

Continuamos usando os waitGroups para sincronizar as goroutines com a função main, mas agora os channels garantem a execução em ordem. Para cada goroutine:

  1. Iniciamos ela com go goroutine(i, &wg, ch)
  2. Avisamos que ela tem permissão para executar com ch <- true
  3. Esperamos que a goroutine anterior termine execução com <-ch (note que mudamos a ordem dos operadores, pois ao invés de passarmos um valor para o channel, estamos recebendo!)

Dentro da própria goroutine, temos uma lógica similar:

func goroutine(num int, wg *sync.WaitGroup, ch chan bool) {
    defer wg.Done()
    fmt.Println("esperando permissao pra iniciar ...")
    <-ch
    fmt.Println("goroutine ", num)
    ch <- true
}

A goroutine agora:

  1. Recebe um parâmetro adicional ch chan bool
  2. Usa defer wg.Done() para sinalizar término ao waitGroup
  3. Espera receber permissão da goroutine anterior com <-ch (essa linha BLOQUEIA a execução até receber o valor true!)
  4. Executa seu código principal
  5. Escreve true no channel com ch <- true, sinalizando para a função main que pode seguir e criar a próxima goroutine

Com isso, conseguimos o output em sequência:

Ola mundo da funcao main!!!!!
esperando permissao pra iniciar ...
goroutine  1
esperando a goroutine anterior terminar ...
esperando permissao pra iniciar ...
goroutine  2
esperando a goroutine anterior terminar ...
esperando permissao pra iniciar ...
goroutine  3
esperando a goroutine anterior terminar ...
esperando permissao pra iniciar ...
goroutine  4
esperando a goroutine anterior terminar ...
esperando permissao pra iniciar ...
goroutine  5
esperando a goroutine anterior terminar ...

Conclusão

Goroutines são uma feature poderosa do Go que permite concorrência de forma simples e elegante. Com waitGroups e channels, podemos controlar precisamente como nossas goroutines se comportam e interagem entre si.

Lembre-se dos pontos principais:

  • Use go para criar goroutines
  • Use waitGroups para sincronizar com a função main
  • Passe waitGroups por referência
  • Channels ajudam a controlar o fluxo entre goroutines
  • O scheduler do Go é não-determinístico por padrão
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment