Ir para o conteúdo
Linguagem para Dominio1666 palavras

Go para Backend: Guia Pratico

Introdução

Existe um momento na carreira de todo desenvolvedor backend em que as ferramentas familiares começam a mostrar seus limites. Talvez você tenha sentido isso: aquela API em Node.js que consome memória demais sob carga, aquele serviço em Python que simplesmente não consegue lidar com milhares de conexões simultâneas sem uma arquitetura reativa complexa, ou aquele microsserviço em Java que precisa de 512MB de RAM só para iniciar.

Go (ou Golang) surgiu exatamente nesse ponto de dor. Criada pelo Google em 2009 e tornada open source em 2012, a linguagem foi projetada desde o início para resolver problemas reais de engenharia de backend: compilação rápida, concorrência nativa, binários estáticos e uma simplicidade quase brutal que força clareza no código.

Neste guia, vou mostrar por que Go se tornou uma das linguagens mais adotadas para backend, quais ferramentas e frameworks usar, padrões de arquitetura recomendados e exemplos práticos que você pode adaptar aos seus projetos. O público aqui é o desenvolvedor com pelo menos dois anos de experiência que já construiu APIs em outras linguagens e quer entender se Go faz sentido para o próximo passo.

Por que usar Go no backend

Performance e consumo de recursos

Go é uma linguagem compilada com tipagem estática. Isso significa que o binário final roda diretamente no sistema operacional, sem máquina virtual, sem interpretador, sem runtime pesado. Na prática, um serviço em Go consome uma fração da memória que o equivalente em Node.js ou Java consumiria.

Para colocar em perspectiva: uma API REST simples em Go ocupa entre 5 e 15MB de RAM em repouso. O mesmo serviço em Spring Boot facilmente ultrapassa 200MB. Em cenários de alta escala, essa diferença se traduz diretamente em custo de infraestrutura.

Concorrência nativa com goroutines

O modelo de concorrência de Go é baseado em goroutines e channels. Uma goroutine é essencialmente uma thread leve gerenciada pelo runtime da linguagem, não pelo sistema operacional. Você pode criar milhões delas sem problemas. Compare isso com o modelo single-threaded do Node.js ou com o custo de criar threads em Java.

Simplicidade como decisão de design

Go não tem herança, não tem generics complexos (os generics adicionados na versão 1.18 são deliberadamente simples), não tem exceções no sentido tradicional, não tem decorators, não tem metaprogramação sofisticada. Isso pode parecer limitante à primeira vista, mas na prática força o desenvolvedor a escrever código explícito e legível. Em equipes grandes, essa previsibilidade vale ouro.

Casos de uso reais onde Go brilha

Algumas das maiores plataformas do mundo usam Go como linguagem principal de backend:

  • Docker e Kubernetes: toda a orquestração de containers que domina a indústria foi escrita em Go.
  • Uber: migrou centenas de microsserviços de Node.js e Python para Go, reduzindo latência e custos.
  • Twitch: usa Go para os serviços de chat que lidam com milhões de conexões simultâneas.
  • Mercado Livre: adotou Go para serviços de alta demanda no ecossistema de pagamentos.
  • Cloudflare: grande parte da infraestrutura de edge computing roda em Go.

Os padrões são claros: APIs de alta concorrência, microsserviços que precisam ser leves, ferramentas de infraestrutura e sistemas que exigem baixa latência.

Setup do ambiente de desenvolvimento

Instalação e primeiros passos com Go

A instalação de Go é direta. Baixe o binário em go.dev/dl, configure o GOPATH e pronto. A partir da versão 1.16, o sistema de módulos (go mod) é o padrão, eliminando a necessidade de gerenciar dependências manualmente.

Para criar um novo projeto:

Text
mkdir minha-api && cd minha-api
go mod init github.com/seuusuario/minha-api

Ferramentas essenciais do ecossistema Go

O ecossistema de Go para backend é maduro e opinado. Estas são as ferramentas que recomendo após anos de uso em produção:

  • Router/Framework HTTP: o pacote net/http da standard library é surpreendentemente completo. Para projetos maiores, considere chi (leve e idiomático) ou Echo (mais features, ainda performático). Evite o Gin se puder: apesar de popular, ele diverge de convenções idiomáticas em alguns pontos.
  • Banco de dados: sqlx para SQL direto ou pgx para PostgreSQL. ORMs como GORM existem, mas a comunidade Go tende a preferir queries explícitas.
  • Validação: go-playground/validator é o padrão de fato.
  • Configuração: viper para carregar configs de arquivos, variáveis de ambiente e flags.
  • Testes: o pacote testing nativo resolve 90% dos casos. testify adiciona assertions mais expressivas.

Exemplo prático: API REST com Go e o router Chi

Vamos construir uma API simples de gerenciamento de tarefas. Esse exemplo mostra a estrutura que uso em projetos reais.

Estrutura do projeto

Text
minha-api/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── handler/
│   │   └── task.go
│   ├── model/
│   │   └── task.go
│   └── repository/
│       └── task.go
├── go.mod
└── go.sum

Código do servidor principal

Go
// cmd/server/main.go
package main
 
import (
    "encoding/json"
    "log"
    "net/http"
    "sync"
    "time"
 
    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)
 
type Task struct {
    ID          string    `json:"id"`
    Title       string    `json:"title"`
    Description string    `json:"description"`
    Done        bool      `json:"done"`
    CreatedAt   time.Time `json:"created_at"`
}
 
type TaskStore struct {
    mu    sync.RWMutex
    tasks map[string]Task
}
 
func NewTaskStore() *TaskStore {
    return &TaskStore{
        tasks: make(map[string]Task),
    }
}
 
func (s *TaskStore) Create(task Task) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.tasks[task.ID] = task
}
 
func (s *TaskStore) List() []Task {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]Task, 0, len(s.tasks))
    for _, t := range s.tasks {
        result = append(result, t)
    }
    return result
}
 
func (s *TaskStore) GetByID(id string) (Task, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    t, ok := s.tasks[id]
    return t, ok
}
 
func main() {
    store := NewTaskStore()
    r := chi.NewRouter()
 
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(middleware.Timeout(30 * time.Second))
 
    r.Route("/api/tasks", func(r chi.Router) {
        r.Get("/", listTasks(store))
        r.Post("/", createTask(store))
        r.Get("/{taskID}", getTask(store))
    })
 
    log.Println("Servidor iniciando na porta 8080")
    http.ListenAndServe(":8080", r)
}
 
func listTasks(store *TaskStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(store.List())
    }
}
 
func createTask(store *TaskStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var input struct {
            Title       string `json:"title"`
            Description string `json:"description"`
        }
        if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
            http.Error(w, "corpo da requisição inválido", http.StatusBadRequest)
            return
        }
 
        task := Task{
            ID:          fmt.Sprintf("%d", time.Now().UnixNano()),
            Title:       input.Title,
            Description: input.Description,
            Done:        false,
            CreatedAt:   time.Now(),
        }
        store.Create(task)
 
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(task)
    }
}
 
func getTask(store *TaskStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        id := chi.URLParam(r, "taskID")
        task, ok := store.GetByID(id)
        if !ok {
            http.Error(w, "tarefa não encontrada", http.StatusNotFound)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(task)
    }
}

Observe alguns pontos importantes nesse código. O sync.RWMutex garante acesso seguro ao mapa de tarefas em cenários concorrentes. Os handlers são funções que retornam http.HandlerFunc, um padrão chamado "closure handler" que permite injeção de dependências sem frameworks de DI. Os middlewares de logging, recovery e timeout vêm prontos no Chi.

Consumindo a API Go a partir de um cliente TypeScript

Na maioria dos projetos reais, sua API em Go será consumida por um frontend ou por outros serviços. Aqui está como construir um client HTTP tipado em TypeScript que se conecta à nossa API de tarefas. Se você leu o post sobre API Layers em aplicações fullstack, vai reconhecer o padrão.

TYPESCRIPT
// api/taskClient.ts
 
interface Task {
  id: string;
  title: string;
  description: string;
  done: boolean;
  created_at: string;
}
 
interface CreateTaskInput {
  title: string;
  description: string;
}
 
class TaskApiClient {
  private baseUrl: string;
 
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }
 
  async listTasks(): Promise<Task[]> {
    const response = await fetch(`${this.baseUrl}/api/tasks`, {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    });
 
    if (!response.ok) {
      throw new Error(
        `Falha ao listar tarefas: ${response.status} ${response.statusText}`
      );
    }
 
    return response.json();
  }
 
  async createTask(input: CreateTaskInput): Promise<Task> {
    const response = await fetch(`${this.baseUrl}/api/tasks`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(input),
    });
 
    if (!response.ok) {
      const errorBody = await response.text();
      throw new Error(
        `Falha ao criar tarefa: ${response.status} - ${errorBody}`
      );
    }
 
    return response.json();
  }
 
  async getTask(taskId: string): Promise<Task | null> {
    const response = await fetch(`${this.baseUrl}/api/tasks/${taskId}`, {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    });
 
    if (response.status === 404) {
      return null;
    }
 
    if (!response.ok) {
      throw new Error(
        `Falha ao buscar tarefa: ${response.status} ${response.statusText}`
      );
    }
 
    return response.json();
  }
}
 
// Uso prático
const client = new TaskApiClient("http://localhost:8080");
 
async function main() {
  const newTask = await client.createTask({
    title: "Estudar goroutines",
    description: "Entender channels e patterns de concorrência em Go",
  });
 
  console.log("Tarefa criada:", newTask);
 
  const tasks = await client.listTasks();
  console.log("Total de tarefas:", tasks.length);
}
 
main();

Client com retry e timeout

Para cenários de produção, você precisa de resiliência. Conforme discuti no post sobre fetch, retry e timeout, um client HTTP sem tratamento de falhas é uma bomba relógio. Aqui está uma versão aprimorada:

TYPESCRIPT
// api/resilientClient.ts
 
interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  timeoutMs: number;
}
 
const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 3,
  baseDelayMs: 500,
  timeoutMs: 10000,
};
 
async function fetchWithRetry(
  url: string,
  options: RequestInit,
  config: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<Response> {
  let lastError: Error | null = null;
 
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(
        () => controller.abort(),
        config.timeoutMs
      );
 
      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
      });
 
      clearTimeout(timeoutId);
 
      // Não faz retry em erros do cliente (4xx)
      if (response.status >= 400 && response.status < 500) {
        return response;
      }
 
      // Faz retry em erros do servidor (5xx)
      if (response.status >= 500) {
        lastError = new Error(`Erro do servidor: ${response.status}`);
        if (attempt < config.maxRetries) {
          const delay = config.baseDelayMs * Math.pow(2, attempt);
          await new Promise((resolve) => setTimeout(resolve, delay));
          continue;
        }
        return response;
      }
 
      return response;
    } catch (err) {
      lastError = err instanceof Error ? err : new Error(String(err));
 
      if (attempt < config.maxRetries) {
        const delay = config.baseDelayMs * Math.pow(2, attempt);
        console.warn(
          `Tentativa ${attempt + 1} falhou. Retentando em ${delay}ms...`
        );
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }
 
  throw new Error(
    `Todas as ${config.maxRetries + 1} tentativas falharam. ` +
    `Último erro: ${lastError?.message}`
  );
}
 
// Uso com a API Go
async function listTasksResilient(): Promise<void> {
  const response = await fetchWithRetry(
    "http://localhost:8080/api/tasks",
    {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    },

Guias Relacionados

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias