DEV Community

renanbastos93
renanbastos93

Posted on

Pipeline em Go - Compondo operações de forma elegante e simples

Introdução

Recentemente embarcamos em um pequeno projeto interno com o colega Jonas Freire: criar uma forma mais expressiva e limpa de encadear operações em Go. Eu participei ajudando a definir contratos, escrever testes e validar o comportamento do pacote. Em conjunto com a empresa, decidimos abrir esse trabalho como open source, para que outros desenvolvedores também possam usar, contribuir e nos dar feedback.

Esse artigo vai mostrar, de forma simples e prática, como usar o pacote pipeline, seus conceitos básicos e alguns exemplos reais para você começar rapidamente.


Por que usar pipeline?

Em Go, quando você precisa realizar várias transformações ou validações consecutivas, o código costuma ficar assim:

res, err := foo()
if err != nil { return err }
res, err = bar(res)
if err != nil { return err }
res, err = baz(res)
if err != nil { return err }
// ...
Enter fullscreen mode Exit fullscreen mode

Ou, se for uma cadeia de funções, você acaba fazendo algo como:

out := fn3(fn2(fn1(input)))
Enter fullscreen mode Exit fullscreen mode

Isso torna o fluxo difícil de ler e difícil de inserir tratamento de erro de forma padronizada.

O pipeline deste projeto provê um jeito idiomático de criar uma sequência de operações que:

  • Recebe um valor inicial
  • Aplica passos (funções) um após o outro
  • Interrompe a execução no primeiro erro
  • Retorna o valor final ou o erro ocorrido

Ou seja: deixa o encadeamento linear, evita repetição de checagem de erro e melhora a legibilidade.


Conceitos principais

Aqui estão os conceitos centrais que você precisa entender para usar o pipeline:

Conceito Descrição
Step[T] É um tipo de função que representa um passo da pipeline: func(ctx context.Context, T) (T, error)
Pipeline[T] O construtor da sequência de passos, começando com um valor inicial de tipo T
New(initial T) Cria uma nova pipeline com valor inicial
Do(step Step[T]) Adiciona mais um passo à pipeline
Execute() / ExecuteWithContext(ctx) Executa todos os passos até o fim ou até encontrar erro
ToStep(...) Função auxiliar que converte funções “normais” (por exemplo func(T) T ou func(T) (T, error)) para o formato Step[T]

O pipeline também incorpora suporte a contexto, para cancelamento ou timeout, e retorna erros enriquecidos com informações como em que passo ocorreu a falha.


Como usar (exemplos práticos)

Exemplo básico com strings

import (
    "context"
    "fmt"
    "strings"

    "github.com/lastro-co/pipeline"
)

func ensureNonEmpty(_ context.Context, s string) (string, error) {
    if s == "" {
        return "", fmt.Errorf("string vazia")
    }
    return s, nil
}

func main() {
    out, err := pipeline.
        New("  ola mundo  ").
        Do(pipeline.ToStep(strings.TrimSpace)).   // converte func(string) string
        Do(pipeline.ToStep(strings.ToUpper)).     // outra função simples
        Do(ensureNonEmpty).                       // função já no formato Step[T]
        Execute()

    if err != nil {
        fmt.Println("Erro:", err)
    } else {
        fmt.Println("Resultado:", out)
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui estamos compondo 3 passos:

  1. TrimSpace para remover espaços
  2. ToUpper para deixar em maiúsculo
  3. Validar que não ficou vazio

Se em algum passo houver erro (por exemplo valor vazio), a execução para ali e retorna o erro.

Adaptando funções com múltiplos parâmetros

Se você já tiver uma função como:

func add(x, y int) int { return x + y }
Enter fullscreen mode Exit fullscreen mode

Você pode adaptá-la dentro da pipeline usando uma closure:

.Do(func(ctx context.Context, x int) (int, error) {
    return add(x, 5), nil
})
Enter fullscreen mode Exit fullscreen mode

Assim você encapsula a lógica e transforma em um Step[int].

Uso de contexto / timeout

Você pode usar ExecuteWithContext para que a pipeline respeite cancelamento ou timeout:

ctx, cancel := context.WithTimeout(context.Background(), 100 * time.Millisecond)
defer cancel()

result, err := pipeline.
    New("algum valor").
    Do(pipeline.ToStep(strings.ToUpper)).
    Do(slowStep).   // passo que demora
    ExecuteWithContext(ctx)

if err != nil {
    // Por exemplo: context.DeadlineExceeded
    fmt.Println("Falha:", err)
}
Enter fullscreen mode Exit fullscreen mode

Se o contexto for encerrado antes do término, o pipeline interrompe e retorna o erro do contexto.

Erros com contexto de passo

Quando um passo falha, o pacote retorna um erro enriquecido (PipelineError) que contém:

  • StepIndex: índice do passo que falhou
  • TotalSteps: número total de passos
  • OriginalErr: o erro retornado pela função que falhou
  • LastValue: último valor processado até então

Isso ajuda a depurar exatamente onde a falha ocorreu.


Dicas boas práticas & considerações

  • Prefira criar passos pequenos, com responsabilidade única.
  • Use ToStep(...) sempre que possível para converter funções existentes e evitar boilerplate.
  • Teste cada passo isoladamente para garantir que o comportamento de erro está correto.
  • Para operações que recebem múltiplos parâmetros, use closures como mostrado acima.
  • Em casos especiais, você pode usar tipos de erro customizados para identificar falhas específicas e tratá-las de forma distinta após Execute.

Conclusão e convite à comunidade

Esse pacote surgiu como uma iniciativa interna do Jonas (e com minha ajuda nos contratos e testes) e agora está aberto como open source. A ideia é oferecer uma forma elegante e clara de encadear operações em Go, com tratamento automático de erros e suporte a contexto.

Se você gostou e quiser contribuir — seja com sugestões, documentação, exemplos ou novos recursos — fique à vontade para abrir issues, pull requests ou me chamar. Vamos crescer juntos! 🚀

Top comments (2)

Collapse
 
isafashiondev profile image
Isadora Ribeiro

Magnífica dica, Renan! Obrigada por contribuir!

Collapse
 
renanbastos93 profile image
renanbastos93

Show, fico feliz que estou ajudando de alguma forma. Qualquer coisa pode me chamar Isa.