Capstone: CLI tool + API Go idiomática
- ⬜⚡ Go performance: pprof + escape analysis(Go Profissional)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Projeto proposto
| Entregável | O que ele prova a quem lê o repositório |
|---|---|
| Binário e servidor no mesmo módulo | Você separa domínio de interface — os dois consomem o mesmo pacote interno |
| Encerramento gracioso | Você já operou serviço: sabe que implantação sem isso gera erro para usuário |
| Teste com banco real em contêiner | Sua suíte cobre restrição de unicidade e transação, não só o caminho feliz |
| Imagem mínima sem shell | Você trata superfície de ataque como requisito, não como detalhe |
| Detector de corridas na esteira | Você sabe que concorrência sem verificação passa despercebida até produção |
| Registro estruturado desde o início | Você não vai depender de impressão em terminal para investigar incidente |
Construir um serviço de gerenciamento de links curtos (shortener): API REST + CLI administrativa no mesmo repo. API com chi router, Postgres, context em toda a stack, graceful shutdown, health/ready probes, métricas Prometheus. CLI em cobra que fala com a API. Dockerfile scratch, testes com Testcontainers, pprof habilitado.
Layout
shorty/
├── go.mod
├── cmd/
│ ├── shorty/ # API server
│ │ └── main.go
│ └── shortyctl/ # CLI admin
│ └── main.go
├── internal/
│ ├── link/
│ │ ├── link.go # domain
│ │ ├── service.go
│ │ ├── repo.go # interface
│ │ └── pg_repo.go # impl Postgres
│ ├── http/
│ │ ├── handler.go
│ │ └── middleware.go
│ └── config/
│ └── config.go
├── migrations/
│ └── 001_init.sql
├── Dockerfile
└── loadtest/
└── bench.gomain.go com graceful shutdown
func main() {
cfg := config.Load()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
db, err := sql.Open("pgx", cfg.DatabaseURL)
if err != nil { logger.Error("db open", "err", err); os.Exit(1) }
defer db.Close()
repo := link.NewPGRepo(db)
svc := link.NewService(repo)
r := chi.NewRouter()
r.Use(middleware.RequestID, middleware.Recoverer, httpmw.Logging(logger))
r.Mount("/api/v1", httpapi.Routes(svc))
r.Handle("/metrics", promhttp.Handler())
r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) })
srv := &http.Server{
Addr: cfg.Addr, Handler: r,
ReadHeaderTimeout: 5*time.Second,
WriteTimeout: 10*time.Second,
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
logger.Info("listening", "addr", cfg.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("listen", "err", err)
}
}()
<-ctx.Done()
logger.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error("shutdown", "err", err)
}
}CLI com cobra
var rootCmd = &cobra.Command{Use: "shortyctl"}
var createCmd = &cobra.Command{
Use: "create <url>",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
body, _ := json.Marshal(map[string]string{"url": args[0]})
resp, err := http.Post(apiURL+"/api/v1/links", "application/json", bytes.NewReader(body))
if err != nil { return err }
defer resp.Body.Close()
_, err = io.Copy(os.Stdout, resp.Body)
return err
},
}
func main() {
rootCmd.AddCommand(createCmd)
if err := rootCmd.Execute(); err != nil { os.Exit(1) }
}O capstone usa contêiner efêmero de Postgres no teste de integração em vez de um substituto em memória. O que isso passa a cobrir?
Dockerfile scratch
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/shorty ./cmd/shorty
FROM gcr.io/distroless/static
COPY --from=build /out/shorty /shorty
EXPOSE 8080
ENTRYPOINT ["/shorty"]Teste de integração com Testcontainers
func TestCreateAndResolve(t *testing.T) {
ctx := context.Background()
pg, err := postgres.Run(ctx, "postgres:16-alpine")
if err != nil { t.Fatal(err) }
defer pg.Terminate(ctx)
db := openAndMigrate(t, pg)
repo := link.NewPGRepo(db)
svc := link.NewService(repo)
l, err := svc.Create(ctx, "https://ffv.com")
if err != nil { t.Fatal(err) }
got, err := svc.Resolve(ctx, l.Code)
if err != nil { t.Fatal(err) }
if got.URL != "https://ffv.com" { t.Fatalf("got %s", got.URL) }
}Entregáveis finais
Repo público com README explicando decisões (chi vs gin, pgx vs database/sql, scratch vs distroless). CI verde: go test -race, go vet, staticcheck, build container. Screenshot de pprof mostrando CPU profile saudável. Binário ~15 MB. Esse é o pacote que convence recrutador sênior de Go.
Perguntas frequentes
❓ Como estruturar um projeto Go que é CLI e API?
❓ Precisa de arcabouço web em Go?
❓ Como distribuir a ferramenta?
Fixando
Por que o encerramento gracioso não é detalhe cosmético num serviço HTTP?
Compilar sem CGO e publicar sobre uma imagem mínima produz um contêiner de poucos megabytes. Qual é o ganho principal?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…