Golang

Artigo 48 — APIs RESTful completas: projeto do zero ao deploy Já leu

13 min de leitura

Artigo 48 — APIs RESTful completas: projeto do zero ao deploy
Artigo 48 — APIs RESTful completas: projeto do zero ao deploy Do conceito ao sistema em produção Os artigos anteriores construíram um repertório sólido: HTTP,

Artigo 48 — APIs RESTful completas: projeto do zero ao deploy

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 8 — Arquitetura e Padrões


Do conceito ao sistema em produção

Os artigos anteriores construíram um repertório sólido: HTTP, banco de dados, autenticação, middlewares, arquitetura limpa e padrões de design. Este artigo une tudo isso em um projeto coeso — uma API RESTful de gerenciamento de tarefas, construída do zero e levada até o deploy.

O projeto seguirá uma estrutura profissional, com autenticação JWT, banco de dados PostgreSQL, testes automatizados, containerização com Docker e deploy em uma plataforma de nuvem.


Estrutura do projeto

taskapi/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── domain/
│   │   └── tarefa.go
│   ├── repository/
│   │   └── tarefa.go
│   ├── usecase/
│   │   └── tarefa/
│   │       ├── criar.go
│   │       ├── listar.go
│   │       ├── atualizar.go
│   │       └── deletar.go
│   ├── handler/
│   │   ├── tarefa.go
│   │   └── auth.go
│   ├── middleware/
│   │   ├── auth.go
│   │   └── cors.go
│   └── infra/
│       └── postgres/
│           └── tarefa_repo.go
├── migrations/
│   ├── 000001_criar_usuarios.up.sql
│   ├── 000001_criar_usuarios.down.sql
│   ├── 000002_criar_tarefas.up.sql
│   └── 000002_criar_tarefas.down.sql
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── go.mod
└── go.sum

Domínio e entidades

// internal/domain/tarefa.go
package domain

import (
    "errors"
    "time"
)

var (
    ErrTituloDeTarefaVazio  = errors.New("título da tarefa não pode ser vazio")
    ErrTarefaNaoEncontrada  = errors.New("tarefa não encontrada")
    ErrTarefaSemPermissao   = errors.New("sem permissão para esta tarefa")
)

type PrioridadeTarefa int

const (
    PrioridadeBaixa  PrioridadeTarefa = 1
    PrioridadeMedia  PrioridadeTarefa = 2
    PrioridadeAlta   PrioridadeTarefa = 3
    PrioridadeUrgente PrioridadeTarefa = 4
)

type Tarefa struct {
    ID          int64
    UsuarioID   int64
    Titulo      string
    Descricao   string
    Concluida   bool
    Prioridade  PrioridadeTarefa
    Prazo       *time.Time
    CriadoEm   time.Time
    AtualizadoEm time.Time
}

func NovaTarefa(usuarioID int64, titulo, descricao string, prioridade PrioridadeTarefa, prazo *time.Time) (*Tarefa, error) {
    if titulo == "" {
        return nil, ErrTituloDeTarefaVazio
    }
    if prioridade < PrioridadeBaixa || prioridade > PrioridadeUrgente {
        prioridade = PrioridadeMedia
    }
    agora := time.Now().UTC()
    return &Tarefa{
        UsuarioID:    usuarioID,
        Titulo:       titulo,
        Descricao:    descricao,
        Prioridade:   prioridade,
        Prazo:        prazo,
        CriadoEm:    agora,
        AtualizadoEm: agora,
    }, nil
}

func (t *Tarefa) Concluir() {
    t.Concluida = true
    t.AtualizadoEm = time.Now().UTC()
}

func (t *Tarefa) PertenceAo(usuarioID int64) bool {
    return t.UsuarioID == usuarioID
}

Migrações SQL

-- migrations/000001_criar_usuarios.up.sql
CREATE TABLE usuarios (
    id         BIGSERIAL PRIMARY KEY,
    nome       TEXT NOT NULL,
    email      TEXT NOT NULL UNIQUE,
    senha_hash TEXT NOT NULL,
    ativo      BOOLEAN NOT NULL DEFAULT TRUE,
    criado_em  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- migrations/000002_criar_tarefas.up.sql
CREATE TYPE prioridade_tarefa AS ENUM ('baixa', 'media', 'alta', 'urgente');

CREATE TABLE tarefas (
    id            BIGSERIAL PRIMARY KEY,
    usuario_id    BIGINT NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE,
    titulo        TEXT NOT NULL,
    descricao     TEXT NOT NULL DEFAULT '',
    concluida     BOOLEAN NOT NULL DEFAULT FALSE,
    prioridade    INT NOT NULL DEFAULT 2
                  CHECK(prioridade BETWEEN 1 AND 4),
    prazo         TIMESTAMPTZ,
    criado_em     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tarefas_usuario_id ON tarefas(usuario_id);
CREATE INDEX idx_tarefas_concluida ON tarefas(usuario_id, concluida);
CREATE INDEX idx_tarefas_prazo ON tarefas(prazo) WHERE prazo IS NOT NULL;

Repositório

// internal/infra/postgres/tarefa_repo.go
package postgres

import (
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"

    "taskapi/internal/domain"
    "taskapi/internal/repository"
)

type tarefaRepo struct{ db *pgxpool.Pool }

func NovoTarefaRepo(db *pgxpool.Pool) repository.TarefaRepository {
    return &tarefaRepo{db: db}
}

func (r *tarefaRepo) Criar(ctx context.Context, t *domain.Tarefa) error {
    query := `
        INSERT INTO tarefas (usuario_id, titulo, descricao, prioridade, prazo)
        VALUES ($1, $2, $3, $4, $5)
        RETURNING id, criado_em, atualizado_em
    `
    return r.db.QueryRow(ctx, query,
        t.UsuarioID, t.Titulo, t.Descricao, t.Prioridade, t.Prazo,
    ).Scan(&t.ID, &t.CriadoEm, &t.AtualizadoEm)
}

func (r *tarefaRepo) BuscarPorID(ctx context.Context, id, usuarioID int64) (*domain.Tarefa, error) {
    query := `
        SELECT id, usuario_id, titulo, descricao, concluida,
               prioridade, prazo, criado_em, atualizado_em
        FROM tarefas
        WHERE id = $1 AND usuario_id = $2
    `
    var t domain.Tarefa
    err := r.db.QueryRow(ctx, query, id, usuarioID).Scan(
        &t.ID, &t.UsuarioID, &t.Titulo, &t.Descricao, &t.Concluida,
        &t.Prioridade, &t.Prazo, &t.CriadoEm, &t.AtualizadoEm,
    )
    if errors.Is(err, pgx.ErrNoRows) {
        return nil, domain.ErrTarefaNaoEncontrada
    }
    return &t, err
}

func (r *tarefaRepo) Listar(ctx context.Context, usuarioID int64, f repository.FiltroTarefa) ([]*domain.Tarefa, int64, error) {
    query := `
        SELECT id, usuario_id, titulo, descricao, concluida,
               prioridade, prazo, criado_em, atualizado_em,
               COUNT(*) OVER() AS total
        FROM tarefas
        WHERE usuario_id = $1
          AND ($2::boolean IS NULL OR concluida = $2)
          AND ($3::int IS NULL OR prioridade = $3)
        ORDER BY prioridade DESC, criado_em DESC
        LIMIT $4 OFFSET $5
    `
    rows, err := r.db.Query(ctx, query,
        usuarioID, f.Concluida, f.Prioridade,
        f.Limite, (f.Pagina-1)*f.Limite,
    )
    if err != nil {
        return nil, 0, err
    }
    defer rows.Close()

    var tarefas []*domain.Tarefa
    var total int64
    for rows.Next() {
        var t domain.Tarefa
        if err := rows.Scan(
            &t.ID, &t.UsuarioID, &t.Titulo, &t.Descricao, &t.Concluida,
            &t.Prioridade, &t.Prazo, &t.CriadoEm, &t.AtualizadoEm, &total,
        ); err != nil {
            return nil, 0, err
        }
        tarefas = append(tarefas, &t)
    }
    return tarefas, total, rows.Err()
}

func (r *tarefaRepo) Atualizar(ctx context.Context, t *domain.Tarefa) error {
    query := `
        UPDATE tarefas
        SET titulo = $1, descricao = $2, concluida = $3,
            prioridade = $4, prazo = $5, atualizado_em = NOW()
        WHERE id = $6 AND usuario_id = $7
        RETURNING atualizado_em
    `
    err := r.db.QueryRow(ctx, query,
        t.Titulo, t.Descricao, t.Concluida,
        t.Prioridade, t.Prazo, t.ID, t.UsuarioID,
    ).Scan(&t.AtualizadoEm)
    if errors.Is(err, pgx.ErrNoRows) {
        return domain.ErrTarefaNaoEncontrada
    }
    return err
}

func (r *tarefaRepo) Deletar(ctx context.Context, id, usuarioID int64) error {
    result, err := r.db.Exec(ctx,
        "DELETE FROM tarefas WHERE id = $1 AND usuario_id = $2", id, usuarioID)
    if err != nil {
        return err
    }
    if result.RowsAffected() == 0 {
        return domain.ErrTarefaNaoEncontrada
    }
    return nil
}

Handler HTTP completo

// internal/handler/tarefa.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "strconv"
    "time"

    "github.com/go-chi/chi/v5"

    "taskapi/internal/domain"
    "taskapi/internal/middleware"
    "taskapi/internal/repository"
    "taskapi/internal/usecase/tarefa"
)

type TarefaHandler struct {
    criar     *tarefa.CriarTarefa
    listar    *tarefa.ListarTarefas
    atualizar *tarefa.AtualizarTarefa
    deletar   *tarefa.DeletarTarefa
}

func NovoTarefaHandler(
    criar *tarefa.CriarTarefa,
    listar *tarefa.ListarTarefas,
    atualizar *tarefa.AtualizarTarefa,
    deletar *tarefa.DeletarTarefa,
) *TarefaHandler {
    return &TarefaHandler{
        criar: criar, listar: listar,
        atualizar: atualizar, deletar: deletar,
    }
}

type criarTarefaReq struct {
    Titulo     string    `json:"titulo"`
    Descricao  string    `json:"descricao"`
    Prioridade int       `json:"prioridade"`
    Prazo      *time.Time `json:"prazo,omitempty"`
}

type atualizarTarefaReq struct {
    Titulo     string    `json:"titulo"`
    Descricao  string    `json:"descricao"`
    Concluida  bool      `json:"concluida"`
    Prioridade int       `json:"prioridade"`
    Prazo      *time.Time `json:"prazo,omitempty"`
}

func (h *TarefaHandler) Criar(w http.ResponseWriter, r *http.Request) {
    usuarioID := middleware.UsuarioIDDoContext(r.Context())

    var req criarTarefaReq
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        responderErro(w, http.StatusBadRequest, "corpo inválido")
        return
    }

    t, err := h.criar.Executar(r.Context(), tarefa.CriarInput{
        UsuarioID:  usuarioID,
        Titulo:     req.Titulo,
        Descricao:  req.Descricao,
        Prioridade: domain.PrioridadeTarefa(req.Prioridade),
        Prazo:      req.Prazo,
    })
    if err != nil {
        traduzirErro(w, err)
        return
    }

    responderJSON(w, http.StatusCreated, t)
}

func (h *TarefaHandler) Listar(w http.ResponseWriter, r *http.Request) {
    usuarioID := middleware.UsuarioIDDoContext(r.Context())

    q := r.URL.Query()
    pagina, _ := strconv.Atoi(q.Get("pagina"))
    if pagina < 1 {
        pagina = 1
    }
    limite, _ := strconv.Atoi(q.Get("limite"))
    if limite < 1 || limite > 100 {
        limite = 20
    }

    var concluida *bool
    if v := q.Get("concluida"); v != "" {
        b := v == "true"
        concluida = &b
    }

    resultado, err := h.listar.Executar(r.Context(), tarefa.ListarInput{
        UsuarioID: usuarioID,
        Filtro: repository.FiltroTarefa{
            Concluida: concluida,
            Pagina:    pagina,
            Limite:    limite,
        },
    })
    if err != nil {
        responderErro(w, http.StatusInternalServerError, "erro ao listar tarefas")
        return
    }

    w.Header().Set("X-Total-Count", strconv.FormatInt(resultado.Total, 10))
    responderJSON(w, http.StatusOK, resultado.Tarefas)
}

func (h *TarefaHandler) Atualizar(w http.ResponseWriter, r *http.Request) {
    usuarioID := middleware.UsuarioIDDoContext(r.Context())
    id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
    if err != nil {
        responderErro(w, http.StatusBadRequest, "id inválido")
        return
    }

    var req atualizarTarefaReq
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        responderErro(w, http.StatusBadRequest, "corpo inválido")
        return
    }

    t, err := h.atualizar.Executar(r.Context(), tarefa.AtualizarInput{
        ID:         id,
        UsuarioID:  usuarioID,
        Titulo:     req.Titulo,
        Descricao:  req.Descricao,
        Concluida:  req.Concluida,
        Prioridade: domain.PrioridadeTarefa(req.Prioridade),
        Prazo:      req.Prazo,
    })
    if err != nil {
        traduzirErro(w, err)
        return
    }

    responderJSON(w, http.StatusOK, t)
}

func (h *TarefaHandler) Deletar(w http.ResponseWriter, r *http.Request) {
    usuarioID := middleware.UsuarioIDDoContext(r.Context())
    id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
    if err != nil {
        responderErro(w, http.StatusBadRequest, "id inválido")
        return
    }

    if err := h.deletar.Executar(r.Context(), id, usuarioID); err != nil {
        traduzirErro(w, err)
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

func traduzirErro(w http.ResponseWriter, err error) {
    switch {
    case errors.Is(err, domain.ErrTarefaNaoEncontrada):
        responderErro(w, http.StatusNotFound, err.Error())
    case errors.Is(err, domain.ErrTituloDeTarefaVazio):
        responderErro(w, http.StatusUnprocessableEntity, err.Error())
    case errors.Is(err, domain.ErrTarefaSemPermissao):
        responderErro(w, http.StatusForbidden, err.Error())
    default:
        responderErro(w, http.StatusInternalServerError, "erro interno")
    }
}

func responderJSON(w http.ResponseWriter, status int, dados any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(dados)
}

func responderErro(w http.ResponseWriter, status int, msg string) {
    responderJSON(w, status, map[string]string{"erro": msg})
}

main.go: composição completa

// cmd/api/main.go
package main

import (
    "context"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/go-chi/chi/v5"
    chimw "github.com/go-chi/chi/v5/middleware"
    "github.com/golang-jwt/jwt/v5"
    "github.com/jackc/pgx/v5/pgxpool"
    _ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
    _ "github.com/golang-migrate/migrate/v4/source/file"

    "taskapi/internal/handler"
    "taskapi/internal/infra/postgres"
    "taskapi/internal/middleware"
    "taskapi/internal/usecase/tarefa"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    slog.SetDefault(logger)

    dbURL := os.Getenv("DATABASE_URL")
    if dbURL == "" {
        logger.Error("DATABASE_URL não definida")
        os.Exit(1)
    }

    jwtSegredo := os.Getenv("JWT_SECRET")
    if jwtSegredo == "" {
        logger.Error("JWT_SECRET não definida")
        os.Exit(1)
    }

    // Banco de dados
    ctx := context.Background()
    pool, err := pgxpool.New(ctx, dbURL)
    if err != nil {
        logger.Error("falha ao conectar ao banco", "err", err)
        os.Exit(1)
    }
    defer pool.Close()

    if err := pool.Ping(ctx); err != nil {
        logger.Error("banco inacessível", "err", err)
        os.Exit(1)
    }

    // Repositórios
    tarefaRepo := postgres.NovoTarefaRepo(pool)
    usuarioRepo := postgres.NovoUsuarioRepo(pool)

    // Casos de uso
    criarTarefa := tarefa.NovoCriarTarefa(tarefaRepo)
    listarTarefas := tarefa.NovoListarTarefas(tarefaRepo)
    atualizarTarefa := tarefa.NovoAtualizarTarefa(tarefaRepo)
    deletarTarefa := tarefa.NovoDeletarTarefa(tarefaRepo)

    // Handlers
    tarefaHandler := handler.NovoTarefaHandler(criarTarefa, listarTarefas, atualizarTarefa, deletarTarefa)
    authHandler := handler.NovoAuthHandler(usuarioRepo, jwtSegredo)

    // Roteamento
    r := chi.NewRouter()
    r.Use(chimw.RequestID)
    r.Use(chimw.RealIP)
    r.Use(chimw.Logger)
    r.Use(chimw.Recoverer)
    r.Use(chimw.Timeout(30 * time.Second))

    r.Get("/saude", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintln(w, `{"status":"ok"}`)
    })

    r.Route("/api/v1", func(r chi.Router) {
        r.Post("/auth/registro", authHandler.Registrar)
        r.Post("/auth/login", authHandler.Login)

        r.Group(func(r chi.Router) {
            r.Use(middleware.JWT(jwtSegredo))

            r.Route("/tarefas", func(r chi.Router) {
                r.Get("/", tarefaHandler.Listar)
                r.Post("/", tarefaHandler.Criar)
                r.Put("/{id}", tarefaHandler.Atualizar)
                r.Delete("/{id}", tarefaHandler.Deletar)
            })
        })
    })

    porta := os.Getenv("PORT")
    if porta == "" {
        porta = "8080"
    }

    srv := &http.Server{
        Addr:         ":" + porta,
        Handler:      r,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    go func() {
        logger.Info("API iniciada", "porta", porta)
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            logger.Error("servidor encerrado com erro", "err", err)
            os.Exit(1)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    logger.Info("encerrando servidor...")
    shutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(shutCtx); err != nil {
        logger.Error("erro no shutdown", "err", err)
    }
    logger.Info("servidor encerrado")
}

Dockerfile multistage

# Dockerfile
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s" \
    -o taskapi ./cmd/api

FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata

WORKDIR /app
COPY --from=builder /app/taskapi .
COPY --from=builder /app/migrations ./migrations

EXPOSE 8080
USER nobody:nobody

ENTRYPOINT ["./taskapi"]

docker-compose.yml para desenvolvimento

# docker-compose.yml
services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgres://usuario:senha@db:5432/taskapi?sslmode=disable
      JWT_SECRET: dev-secret-troque-em-producao
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: usuario
      POSTGRES_PASSWORD: senha
      POSTGRES_DB: taskapi
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U usuario -d taskapi"]
      interval: 5s
      timeout: 5s
      retries: 5

  migrate:
    image: migrate/migrate
    volumes:
      - ./migrations:/migrations
    command: -path=/migrations -database=postgres://usuario:senha@db:5432/taskapi?sslmode=disable up
    depends_on:
      db:
        condition: service_healthy

volumes:
  postgres_data:

Makefile

.PHONY: dev build test lint migrate-up migrate-down docker-up docker-down

dev:
    air -c .air.toml  # hot reload com air

build:
    CGO_ENABLED=0 go build -o bin/taskapi ./cmd/api

test:
    go test -race -count=1 -cover ./...

test-integration:
    go test -race -count=1 -tags=integration ./...

lint:
    golangci-lint run ./...

migrate-up:
    migrate -path migrations -database "${DATABASE_URL}" up

migrate-down:
    migrate -path migrations -database "${DATABASE_URL}" down 1

docker-up:
    docker compose up -d

docker-down:
    docker compose down

docker-logs:
    docker compose logs -f api

Deploy no Fly.io

# Instalar flyctl
curl -L https://fly.io/install.sh | sh

# Login e criação da app
fly auth login
fly launch --name taskapi --region gru

# Banco PostgreSQL gerenciado
fly postgres create --name taskapi-db --region gru
fly postgres attach taskapi-db

# Segredos
fly secrets set JWT_SECRET="$(openssl rand -hex 32)"

# Deploy
fly deploy

# Logs em tempo real
fly logs
# fly.toml — gerado pelo fly launch, ajuste conforme necessário
app = "taskapi"
primary_region = "gru"

[build]
  dockerfile = "Dockerfile"

[env]
  PORT = "8080"

[http_service]
  internal_port = 8080
  force_https = true

  [[http_service.checks]]
    grace_period = "10s"
    interval = "30s"
    method = "GET"
    path = "/saude"
    timeout = "5s"

[[vm]]
  memory = "256mb"
  cpu_kind = "shared"
  cpus = 1

Resumo do que foi coberto

Este artigo construiu uma API RESTful completa: estrutura de projeto profissional, domínio com regras de negócio, migrações SQL versionadas, repositório PostgreSQL com pgx, handler HTTP com tradução de erros de domínio, composição no main com todos os componentes, Dockerfile multistage para imagem mínima, docker-compose para desenvolvimento local e deploy no Fly.io. Com este artigo o Módulo 8 está completo — o próximo módulo aborda compilação, Docker e cloud.


Referências e leituras complementares

  • Fly.io — Go deployment — Guia oficial de deploy de aplicações Go. https://fly.io/docs/languages-and-frameworks/golang/

  • golang-migrate — Ferramenta de migrações de banco de dados. https://github.com/golang-migrate/migrate

  • Chi router — Documentação e exemplos do Chi. https://github.com/go-chi/chi

  • pgx v5 — Driver PostgreSQL para Go. https://github.com/jackc/pgx

  • Air — Live reload — Hot reload para desenvolvimento Go. https://github.com/air-verse/air

  • Docker multistage builds — Guia oficial de builds multistage. https://docs.docker.com/build/building/multi-stage/


Próximo artigo: Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go**


you asked

Sim


claude response

Comentários

Mais em Golang

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go
Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go O binário...

A biblioteca padrão: um tour pelas principais ferramentas
A biblioteca padrão: um tour pelas principais ferramentas

Uma das raz&otilde;es pelas quais Go se tornou a linguagem preferida para inf...

Leitura e escrita de arquivos com os pacotes os e bufio
Leitura e escrita de arquivos com os pacotes os e bufio

Antes de bancos de dados, filas de mensagens e APIs REST, existe o arquivo. L...