Go e Dapr: Distributed Application Runtime

Dapr (Distributed Application Runtime) é uma plataforma portátil e event-driven que facilita a construção de aplicações distribuídas e microsserviços. Projetado pela Microsoft, é usado por empresas como Alibaba, Tencent e Ignition Group.

Neste guia, você aprenderá a usar Dapr com Go para resolver desafios comuns de microsserviços de forma simples e portátil.

Índice

  1. O que é Dapr?
  2. Arquitetura e Sidecar
  3. Configuração
  4. Service Invocation
  5. State Management
  6. Pub/Sub Messaging
  7. Bindings
  8. Observability

O que é Dapr?

Building Blocks

Dapr fornece 8 building blocks que resolvem problemas comuns:

Building BlockDescrição
Service InvocationChamadas resilientes entre serviços
State ManagementArmazenamento de estado chave-valor
Pub/SubMensageria publish/subscribe
BindingsIntegração com sistemas externos
ActorsModelo de atores virtual
ObservabilityRastreamento e métricas
SecretsGerenciamento de segredos
ConfigurationConfigurações dinâmicas

Benefícios

1. Portabilidade Mesmo código funciona com Redis, Kafka, RabbitMQ, etc. Troque componentes sem mudar código.

2. Vendor Lock-in Zero Abstração sobre serviços cloud. Migre de AWS para Azure sem reescrever código.

3. Sidecar Architecture Dapr roda como processo separado (sidecar). Sua aplicação permanece simples.

┌─────────────────────────────────────────────────────────────┐
│                    Kubernetes Pod                          │
│  ┌─────────────────────┐  ┌─────────────────────┐          │
│  │                     │  │                     │          │
│  │   App Container     │  │   Dapr Sidecar      │          │
│  │   (Go Application)  │◀─│   (daprd)           │          │
│  │                     │  │                     │          │
│  │   HTTP/gRPC         │  │   HTTP: 3500        │          │
│  │   localhost:5000    │  │   gRPC: 50001       │          │
│  │                     │  │                     │          │
│  └─────────────────────┘  └─────────┬───────────┘          │
│                                     │                      │
└─────────────────────────────────────┼──────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│              Componentes (Redis, Kafka, etc)               │
└─────────────────────────────────────────────────────────────┘

Arquitetura e Sidecar

Comunicação Sidecar

Sua aplicação Go comunica com Dapr via HTTP ou gRPC na porta local:

// HTTP API
// http://localhost:3500/v1.0/invoke/{app-id}/method/{method}
// http://localhost:3500/v1.0/state/{store-name}/{key}

// gRPC (mais performático)
// localhost:50001

SDK Go do Dapr

go get github.com/dapr/go-sdk/client
go get github.com/dapr/go-sdk/service/http

Cliente Dapr

package dapr

import (
    "context"
    "fmt"

    dapr "github.com/dapr/go-sdk/client"
)

type Client struct {
    dapr.Client
}

func NewClient() (*Client, error) {
    client, err := dapr.NewClient()
    if err != nil {
        return nil, fmt.Errorf("falha ao criar cliente Dapr: %w", err)
    }

    return &Client{client}, nil
}

func (c *Client) Close() {
    c.Client.Close()
}

Configuração

Componentes

Arquivos YAML definem componentes (state store, pub/sub, etc):

# components/statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: redis:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"
# components/pubsub.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: redis:6379

Docker Compose (Desenvolvimento)

# docker-compose.yml
version: '3.8'

services:
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

  service-a:
    build: ./service-a
    ports:
      - "8080:8080"
    environment:
      - APP_PORT=8080
      - DAPR_HTTP_PORT=3500
    networks:
      - dapr-network

  service-a-dapr:
    image: daprio/daprd:latest
    command: [
      "./daprd",
      "--app-id", "service-a",
      "--app-port", "8080",
      "--dapr-http-port", "3500",
      "--components-path", "/components"
    ]
    volumes:
      - ./components:/components
    depends_on:
      - redis
      - service-a
    network_mode: "service:service-a"

  service-b:
    build: ./service-b
    ports:
      - "8081:8081"
    environment:
      - APP_PORT=8081
      - DAPR_HTTP_PORT=3501

  service-b-dapr:
    image: daprio/daprd:latest
    command: [
      "./daprd",
      "--app-id", "service-b",
      "--app-port", "8081",
      "--dapr-http-port", "3501",
      "--components-path", "/components"
    ]
    volumes:
      - ./components:/components
    depends_on:
      - redis
      - service-b
    network_mode: "service:service-b"

networks:
  dapr-network:
    driver: bridge

Service Invocation

Chamando Outros Serviços

package service

import (
    "context"
    "encoding/json"
    "fmt"

    dapr "github.com/dapr/go-sdk/client"
)

type UserService struct {
    client dapr.Client
}

func NewUserService(client dapr.Client) *UserService {
    return &UserService{client: client}
}

// GetUser chama outro serviço via Dapr
func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
    // Invoca service-b método /users/{id}
    resp, err := s.client.InvokeMethodWithContent(
        ctx,
        "service-b",           // app-id do serviço destino
        fmt.Sprintf("users/%s", userID), // método
        "get",                 // HTTP method
        &dapr.DataContent{
            ContentType: "application/json",
        },
    )
    if err != nil {
        return nil, fmt.Errorf("falha na invocação: %w", err)
    }

    var user User
    if err := json.Unmarshal(resp, &user); err != nil {
        return nil, err
    }

    return &user, nil
}

// CreateUser envia POST para outro serviço
func (s *UserService) CreateUser(ctx context.Context, user *User) error {
    data, err := json.Marshal(user)
    if err != nil {
        return err
    }

    _, err = s.client.InvokeMethodWithContent(
        ctx,
        "service-b",
        "users",
        "post",
        &dapr.DataContent{
            ContentType: "application/json",
            Data:        data,
        },
    )

    return err
}

Recebendo Chamadas

package main

import (
    "encoding/json"
    "net/http"

    "github.com/dapr/go-sdk/service/common"
    daprHttp "github.com/dapr/go-sdk/service/http"
)

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func main() {
    // Cria serviço Dapr na porta da aplicação
    s := daprHttp.NewService(":8080")

    // Handler para invocação
    s.AddServiceInvocationHandler("users", userHandler)
    s.AddServiceInvocationHandler("health", healthHandler)

    // Inicia serviço
    if err := s.Start(); err != nil {
        panic(err)
    }
}

func userHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
    var user User
    if err := json.Unmarshal(in.Data, &user); err != nil {
        return nil, err
    }

    // Processa usuário
    response, _ := json.Marshal(map[string]string{
        "status": "created",
        "id":     user.ID,
    })

    return &common.Content{
        Data:        response,
        ContentType: "application/json",
    }, nil
}

func healthHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
    return &common.Content{
        Data:        []byte(`{"status":"healthy"}`),
        ContentType: "application/json",
    }, nil
}

State Management

Operações Básicas

package state

import (
    "context"
    "encoding/json"
    "fmt"
    "time"

    dapr "github.com/dapr/go-sdk/client"
)

const stateStoreName = "statestore"

type StateManager struct {
    client dapr.Client
}

func NewStateManager(client dapr.Client) *StateManager {
    return &StateManager{client: client}
}

// SaveState salva estado com TTL
func (sm *StateManager) SaveState(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }

    return sm.client.SaveState(ctx, stateStoreName, key, data, map[string]string{
        "ttlInSeconds": fmt.Sprintf("%d", int(ttl.Seconds())),
    })
}

// GetState recupera estado
func (sm *StateManager) GetState(ctx context.Context, key string, target interface{}) error {
    item, err := sm.client.GetState(ctx, stateStoreName, key, nil)
    if err != nil {
        return err
    }

    if item.Value == nil {
        return fmt.Errorf("key not found: %s", key)
    }

    return json.Unmarshal(item.Value, target)
}

// DeleteState remove estado
func (sm *StateManager) DeleteState(ctx context.Context, key string) error {
    return sm.client.DeleteState(ctx, stateStoreName, key, nil)
}

// SaveBulkState salva múltiplos itens
func (sm *StateManager) SaveBulkState(ctx context.Context, items map[string]interface{}) error {
    var states []*dapr.SetStateItem

    for key, value := range items {
        data, err := json.Marshal(value)
        if err != nil {
            return err
        }

        states = append(states, &dapr.SetStateItem{
            Key:   key,
            Value: data,
        })
    }

    return sm.client.SaveBulkState(ctx, stateStoreName, states)
}

Uso em Workflow

package order

type OrderWorkflow struct {
    stateManager *state.StateManager
}

func (w *OrderWorkflow) ProcessOrder(ctx context.Context, order Order) error {
    orderID := order.ID

    // Salva estado inicial
    if err := w.stateManager.SaveState(ctx, orderID, order, 24*time.Hour); err != nil {
        return err
    }

    // Processa pagamento
    payment, err := w.processPayment(ctx, order)
    if err != nil {
        // Atualiza estado com erro
        order.Status = "payment_failed"
        w.stateManager.SaveState(ctx, orderID, order, 24*time.Hour)
        return err
    }

    // Atualiza estado
    order.PaymentID = payment.ID
    order.Status = "paid"
    if err := w.stateManager.SaveState(ctx, orderID, order, 24*time.Hour); err != nil {
        return err
    }

    // Envia para fulfillment
    if err := w.sendToFulfillment(ctx, order); err != nil {
        return err
    }

    order.Status = "fulfillment"
    return w.stateManager.SaveState(ctx, orderID, order, 24*time.Hour)
}

Pub/Sub Messaging

Publicando Mensagens

package messaging

import (
    "context"
    "encoding/json"

    dapr "github.com/dapr/go-sdk/client"
)

const pubsubName = "pubsub"

type Publisher struct {
    client dapr.Client
}

func NewPublisher(client dapr.Client) *Publisher {
    return &Publisher{client: client}
}

// PublishEvent publica evento em um tópico
func (p *Publisher) PublishEvent(ctx context.Context, topic string, data interface{}) error {
    payload, err := json.Marshal(data)
    if err != nil {
        return err
    }

    return p.client.PublishEvent(ctx, pubsubName, topic, payload, map[string]string{
        "content-type": "application/json",
    })
}

// Exemplo: OrderCreated event
type OrderCreatedEvent struct {
    OrderID   string  `json:"order_id"`
    UserID    string  `json:"user_id"`
    Total     float64 `json:"total"`
    Timestamp int64   `json:"timestamp"`
}

func (p *Publisher) PublishOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
    return p.PublishEvent(ctx, "orders", event)
}

// Exemplo: PaymentProcessed event
type PaymentProcessedEvent struct {
    OrderID   string `json:"order_id"`
    PaymentID string `json:"payment_id"`
    Status    string `json:"status"`
}

func (p *Publisher) PublishPaymentProcessed(ctx context.Context, event PaymentProcessedEvent) error {
    return p.PublishEvent(ctx, "payments", event)
}

Subscrevendo em Eventos

package main

import (
    "context"
    "encoding/json"
    "log"

    "github.com/dapr/go-sdk/service/common"
    daprHttp "github.com/dapr/go-sdk/service/http"
)

func main() {
    s := daprHttp.NewService(":8080")

    // Subscrição em tópico "orders"
    sub := &common.Subscription{
        PubsubName: "pubsub",
        Topic:      "orders",
        Route:      "/orders",
    }
    s.AddTopicEventHandler(sub, orderEventHandler)

    // Subscrição em tópico "payments"
    paymentSub := &common.Subscription{
        PubsubName: "pubsub",
        Topic:      "payments",
        Route:      "/payments",
    }
    s.AddTopicEventHandler(paymentSub, paymentEventHandler)

    if err := s.Start(); err != nil {
        panic(err)
    }
}

func orderEventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
    var event OrderCreatedEvent
    if err := json.Unmarshal(e.RawData, &event); err != nil {
        log.Printf("Erro ao parsear evento: %v", err)
        return false, err // Não retry
    }

    log.Printf("Order criada: %s, Total: %.2f", event.OrderID, event.Total)

    // Processa evento
    // ...

    return false, nil // Sucesso
}

func paymentEventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
    var event PaymentProcessedEvent
    if err := json.Unmarshal(e.RawData, &event); err != nil {
        return false, err
    }

    if event.Status == "success" {
        log.Printf("Pagamento aprovado: %s", event.OrderID)
        // Atualiza ordem, envia email, etc
    } else {
        log.Printf("Pagamento falhou: %s", event.OrderID)
        // Compensação
    }

    return false, nil
}

Bindings

Input Bindings (Triggers)

# components/cron-binding.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: daily-report
spec:
  type: bindings.cron
  version: v1
  metadata:
  - name: schedule
    value: "0 9 * * *"  # 9h todo dia
// Handler para cron trigger
func main() {
    s := daprHttp.NewService(":8080")

    s.AddBindingInvocationHandler("daily-report", func(ctx context.Context, in *common.BindingEvent) ([]byte, error) {
        log.Println("Executando relatório diário...")
        
        // Gera relatório
        report := generateDailyReport()
        
        // Envia email
        sendEmail(report)
        
        return nil, nil
    })

    s.Start()
}

Output Bindings

# components/email-binding.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: sendgrid
spec:
  type: bindings.twilio.sendgrid
  version: v1
  metadata:
  - name: apiKey
    secretKeyRef:
      name: sendgrid-key
      key: apiKey
func (s *Service) SendEmail(ctx context.Context, to, subject, body string) error {
    req := map[string]string{
        "emailTo":   to,
        "subject":   subject,
        "emailBody": body,
    }

    data, _ := json.Marshal(req)

    _, err := s.daprClient.InvokeBinding(ctx, &dapr.InvokeBindingRequest{
        Name:      "sendgrid",
        Operation: "create",
        Data:      data,
    })

    return err
}

Observability

Tracing

Dapr automaticamente gera traces distribuídos:

# configuration/tracing.yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: app-config
spec:
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: http://zipkin:9411/api/v2/spans
// Extrai trace context para propagar
func (s *Service) CallWithTrace(ctx context.Context) {
    // Dapr injeta trace context automaticamente
    // Acesse via OpenTelemetry ou similar
}

Métricas

// Métricas expostas pelo Dapr sidecar
// http://localhost:3500/v1.0/metrics

import "github.com/prometheus/client_golang/prometheus"

var (
    requestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "app_requests_total",
            Help: "Total requests",
        },
        []string{"method", "status"},
    )
)

Conclusão

Neste guia, você aprendeu:

Arquitetura: Sidecar pattern e building blocks ✅ Service Invocation: Chamadas resilientes entre serviços ✅ State Management: Armazenamento de estado distribuído ✅ Pub/Sub: Mensageria publish-subscribe ✅ Bindings: Integração com sistemas externos ✅ Observability: Tracing e métricas

Próximos Passos

  1. Go e Temporal - Workflows resilientes
  2. Go Microservices - Arquitetura completa
  3. Go e Kubernetes - Orquestração de containers

FAQ

Q: Dapr adiciona overhead? R: Mínimo. Latência adicional é tipicamente <5ms para chamadas locais.

Q: Posso usar Dapr sem Kubernetes? R: Sim! Funciona com Docker Compose, VMs ou bare metal.

Q: Preciso mudar meu código para trocar componentes? R: Não. Troque o YAML do componente, o código permanece igual.

Q: Dapr é production-ready? R: Sim. Usado em produção por empresas como Microsoft, Alibaba, Ignition Group.