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 (0)