Aula 30 - Tutorial Golang - Timeouts
Fontes usadas para esse post:
Timeouts
O
Timeout é uma técnica utilizada em programação para especificar o tempo máximo que uma ação ou operação deve levar para ser concluída.
Ele é usado para evitar que uma ação bloqueada continue sendo executada indefinidamente e assim, preservar recursos de sistema como
memória e
CPU.
Um exemplo de caso de uso para
Timeouts é quando uma aplicação precisa fazer uma solicitação a um serviço externo, como uma
API.
Se o serviço demorar muito tempo para responder ou estiver inacessível, a aplicação pode ficar bloqueada esperando pela resposta.
Ao adicionar um
Timeout à solicitação, a aplicação pode continuar a funcionar normalmente, sem ficar presa em uma solicitação pendente.
Exemplo
Neste exemplo, uma
goroutine é iniciada para esperar por
5 segundos antes de
enviar um
sinal para o
canal "
timeout".
A operação principal então usa a estrutura "
select" para
aguardar pelo
sinal do
canal "
timeout" ou por outro evento, como uma interrupção de teclado.
Se o sinal chegar antes do final do
timeout, a mensagem "
Timeout reached" será impressa.
package main
import (
"fmt"
"time"
)
func main() {
timeout := make(chan bool, 1)
go func() {
time.Sleep(5 * time.Second)
timeout <- true
}()
select {
case <-timeout:
fmt.Println("Timeout reached")
}
}
Uso em Requisições HTTP
Quando você faz uma requisição HTTP a um servidor externo, é importante ter um timeout para evitar que a aplicação fique presa esperando por uma resposta que nunca chegará.
Aqui está um exemplo usando o pacote "
net/http" da biblioteca padrão:
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
)
func main() {
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get("https://www.google.com")
if err != nil {
if err, ok := err.(*url.Error); ok && err.Timeout() {
fmt.Println("Timeout reached")
}
} else {
defer resp.Body.Close()
fmt.Println("Response received")
b, err := io.ReadAll(resp.Body)
// b, err := ioutil.ReadAll(resp.Body) Go.1.15 and earlier
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(b))
}
}
Uso em Operações de Banco de Dados
Quando você realiza uma operação de banco de dados, como uma consulta SQL, é importante ter um
timeout para evitar que a aplicação fique presa esperando por uma resposta interminável.
Para rodar esse exemplo, você tem que tá com o
mysql rodando,
local ou na
nuvem,
trocar o
nome do
banco para um que você tenha no seu
mysql, e a
senha também.
Depois instalar o drive do mysqlcom:
go mod init my_example.com/module
go mod tidy
go install github.com/go-sql-driver/mysql@latest
Exemplo usando o pacote "
database/sql" da biblioteca padrão:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"time"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
fmt.Println(err)
return
}
defer db.Close()
db.SetConnMaxLifetime(5 * time.Second)
rows, err := db.Query("SELECT * FROM users")
if err != nil {
fmt.Println(err)
return
}
defer rows.Close()
for rows.Next() {
var id int64
var first_name string
var last_name string
var email string
var password string
if err := rows.Scan(&id, &first_name, &last_name, &email, &password); err != nil {
log.Fatalf("Could not scan the result: %v", err)
}
fmt.Println(first_name)
if err := rows.Err(); err != nil {
log.Fatalf("Error while processing the rows: %v", err)
}
}
}
Para o exemplo abaixo, suponha que estamos fazendo uma chamada externa que retorna o resultado em um
canal c1 após
2s.
Observe que o
canal é
armazenado em
buffer, portanto, o envio na
goroutine é sem bloqueio.
Este é um padrão comum para evitar
vazamentos de
goroutine caso o canal nunca seja lido.
O
select implementa um
timeout.res := <-c1 que aguarda o resultado e o
`<-time.After` aguarda um valor a ser enviado
após o
timeout de
1s.
Uma vez que
select prossegue com o primeiro recebimento pronto, consideraremos o caso de
timeout se a operação demorar mais do que os 1s permitidos.
Se permitirmos um tempo limite
maior de
3s, o recebimento de
c2 será bem-sucedido e imprimiremos o resultado.
A execução deste programa mostra o tempo limite (
timeout) da primeira operação e o sucesso da segunda.
// _Timeouts_ are important for programs that connect to
// external resources or that otherwise need to bound
// execution time. Implementing timeouts in Go is easy and
// elegant thanks to channels and `select`.
package main
import (
"fmt"
"time"
)
func main() {
// For our example, suppose we're executing an external
// call that returns its result on a channel `c1`
// after 2s. Note that the channel is buffered, so the
// send in the goroutine is nonblocking. This is a
// common pattern to prevent goroutine leaks in case the
// channel is never read.
c1 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c1 <- "result 1"
}()
// Here's the `select` implementing a timeout.
// `res := <-c1` awaits the result and `<-time.After`
// awaits a value to be sent after the timeout of
// 1s. Since `select` proceeds with the first
// receive that's ready, we'll take the timeout case
// if the operation takes more than the allowed 1s.
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout 1")
}
// If we allow a longer timeout of 3s, then the receive
// from `c2` will succeed and we'll print the result.
c2 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c2 <- "result 2"
}()
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(3 * time.Second):
fmt.Println("timeout 2")
}
}
Explicação do código acima
O
time.After é uma função do pacote
time do
Go que retorna um canal que é preenchido com o valor
true após um período de tempo especificado.
Isso pode ser usado para implementar temporizadores simples.
Na prática,
time.After é útil quando você precisa aguardar por um determinado período de tempo, mas ainda quer ter a opção de interromper esse tempo se algum outro evento acontecer.
No exemplo acima, ele é usado para implementar um
timeout na execução de uma operação.
Existem
duas operações que estão sendo realizadas em paralelo: uma que retorna o resultado em um canal
c1 após
2 segundos, e outra que retorna o resultado em um canal
c2 após
2 segundos.
O
select é usado para esperar por
ambos os
resultados ou pelo
timeout.
Se a operação retornar o resultado antes de atingir o tempo limite, o resultado é impresso.
Caso contrário, a mensagem "
timeout 1" ou "
timeout 2" é impressa, dependendo do caso.
No
primeiro caso, o tempo limite é de
1 segundo e, portanto, o resultado não é recebido a tempo e a mensagem "
timeout 1" é impressa.
No
segundo caso, o tempo limite é de
3 segundos e, portanto, o resultado é recebido e a mensagem "
result 2" é impressa.
Uso em Leitura de Dados de Entrada
Quando você lê dados de entrada, como dados de teclado ou de uma conexão de rede, é importante ter um
timeout para evitar que a aplicação fique presa esperando por uma entrada interminável.
Aqui está um exemplo usando o pacote "
bufio" da biblioteca padrão:
package main
import (
"bufio"
"fmt"
"os"
"time"
)
func main() {
timeout := time.After(5 * time.Second)
input := make(chan string)
go func() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter text: ")
inputText, _ := reader.ReadString('\n')
input <- inputText
}()
select {
case <-timeout:
fmt.Println("Timeout reached")
case enteredText := <-input:
fmt.Printf("Entered text: %s", enteredText)
}
}
Neste exemplo, criamos uma
goroutine que envia um
sinal para o
canal "
timeout" após
5 segundos.
Em seguida, usamos a instrução "
select" para aguardar tanto o
sinal do
canal "
timeout" quanto a
leitura dos dados de entrada pelo objeto "
reader".
Se o
sinal do
canal "
timeout" chegar primeiro, é impressa a mensagem de "
Timeout reached".
Caso contrário, é impresso os dados de entrada lidos pelo objeto "
reader".
Eu fico por aqui e agradeço a você pela audiência.
Até mais. :)
Meus links de afiliados:
Obrigado e bons estudos. ;)