Projeto Completo (Parte 2)
Lição 30: Projeto Completo (Parte 2)
Analogia do Mundo Real
Na lição anterior completamos a análise de requisitos, design de estrutura de diretórios, modelos de dados e lógica de negócio principal do "Sistema de Gerenciamento de Tarefas TaskFlow" — como construir uma casa com a fundação lançada e a estrutura erguida. Nesta lição, completaremos o acabamento e entrega:
- REST API = Instalar portas e janelas para que pessoas de fora possam entrar
- Middleware = Sistema de controle de acesso (autenticação), câmeras de segurança (logging), registro de visitantes (CORS)
- Integração com banco de dados = Substituir móveis temporários por móveis personalizados, duráveis e de longa duração
- Testes de integração = Inspeção de aceitação, garantindo que água, eletricidade e gás funcionem corretamente
- Documentação da API = Manual do usuário
- Dockerfile = Empacotar a casa em um container, habitável onde quer que seja movida
Após esta lição, você terá um projeto Go completo implantável, testável e documentado.
Revisão do Projeto
Na lição anterior construímos o esqueleto principal do TaskFlow. Esta lição continua a refiná-lo. Se você não leu Lição 29: Projeto Completo (Parte 1), recomenda-se completar a primeira metade primeiro.
Estrutura final do projeto:
taskflow/
├── cmd/
│ └── server/
│ └── main.go # Ponto de entrada do programa
├── internal/
│ ├── model/
│ │ ├── task.go # Modelo de dados
│ │ └── task_test.go # Testes do modelo
│ ├── store/
│ │ ├── store.go # Interface de armazenamento
│ │ ├── memory.go # Implementação em memória (lição anterior)
│ │ └── sqlite.go # Implementação SQLite (esta lição)
│ ├── service/
│ │ ├── task.go # Lógica de negócio
│ │ └── task_test.go # Testes unitários
│ ├── handler/
│ │ ├── task.go # Handler HTTP
│ │ └── task_test.go # Testes do handler
│ └── middleware/
│ ├── auth.go # Middleware de autenticação
│ ├── logging.go # Middleware de logging
│ └── cors.go # Middleware CORS
├── docs/
│ └── api.md # Documentação da API
├── Dockerfile # Empacotamento container
├── docker-compose.yml # Configuração de orquestração
├── go.mod
├── go.sum
└── README.md
1. Conclusão da Camada REST API
1.1 Design de Rotas
Rotas da API projetadas seguindo convenções RESTful:
| Método | Caminho | Descrição |
|---|---|---|
GET |
/api/tasks |
Obter lista de tarefas |
GET |
/api/tasks/{id} |
Obter tarefa única |
POST |
/api/tasks |
Criar tarefa |
PUT |
/api/tasks/{id} |
Atualizar tarefa |
DELETE |
/api/tasks/{id} |
Excluir tarefa |
GET |
/health |
Health check |
1.2 Implementação Completa do Handler
// internal/handler/task.go
package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"taskflow/internal/model"
"taskflow/internal/service"
)
// TaskHandler handler HTTP de tarefas
type TaskHandler struct {
svc *service.TaskService
}
// NewTaskHandler cria uma instância de handler
func NewTaskHandler(svc *service.TaskService) *TaskHandler {
return &TaskHandler{svc: svc}
}
// ErrorResponse formato de resposta de erro unificado
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Message string `json:"message,omitempty"`
}
// SuccessResponse formato de resposta de sucesso unificado
type SuccessResponse struct {
Data interface{} `json:"data"`
Message string `json:"message,omitempty"`
}
// writeJSON escreve uma resposta JSON
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// writeError escreve uma resposta de erro
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, ErrorResponse{
Error: msg,
Code: status,
})
}
// extractID extrai o parâmetro ID do caminho da URL
func extractID(r *http.Request) (int, error) {
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(parts) < 3 {
return 0, errors.New("parâmetro ID ausente")
}
return strconv.Atoi(parts[2])
}
// ListTasks trata GET /api/tasks
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
filter := service.TaskFilter{
Status: model.TaskStatus(query.Get("status")),
}
page, _ := strconv.Atoi(query.Get("page"))
pageSize, _ := strconv.Atoi(query.Get("page_size"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
tasks, total, err := h.svc.ListTasks(r.Context(), filter, page, pageSize)
if err != nil {
writeError(w, http.StatusInternalServerError, "Falha na consulta: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"data": tasks,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// GetTask trata GET /api/tasks/{id}
func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "ID de tarefa inválido")
return
}
task, err := h.svc.GetTask(r.Context(), id)
if err != nil {
if errors.Is(err, service.ErrTaskNotFound) {
writeError(w, http.StatusNotFound, "Tarefa não encontrada")
return
}
writeError(w, http.StatusInternalServerError, "Falha na consulta")
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Data: task})
}
// CreateTask trata POST /api/tasks
func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
var req model.CreateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Formato de requisição inválido")
return
}
task, err := h.svc.CreateTask(r.Context(), &req)
if err != nil {
if errors.Is(err, service.ErrValidation) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeError(w, http.StatusInternalServerError, "Falha na criação")
return
}
writeJSON(w, http.StatusCreated, SuccessResponse{
Data: task,
Message: "Tarefa criada com sucesso",
})
}
// UpdateTask trata PUT /api/tasks/{id}
func (h *TaskHandler) UpdateTask(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "ID de tarefa inválido")
return
}
var req model.UpdateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Formato de requisição inválido")
return
}
task, err := h.svc.UpdateTask(r.Context(), id, &req)
if err != nil {
switch {
case errors.Is(err, service.ErrTaskNotFound):
writeError(w, http.StatusNotFound, "Tarefa não encontrada")
case errors.Is(err, service.ErrValidation):
writeError(w, http.StatusBadRequest, err.Error())
default:
writeError(w, http.StatusInternalServerError, "Falha na atualização")
}
return
}
writeJSON(w, http.StatusOK, SuccessResponse{
Data: task,
Message: "Tarefa atualizada com sucesso",
})
}
// DeleteTask trata DELETE /api/tasks/{id}
func (h *TaskHandler) DeleteTask(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "ID de tarefa inválido")
return
}
if err := h.svc.DeleteTask(r.Context(), id); err != nil {
if errors.Is(err, service.ErrTaskNotFound) {
writeError(w, http.StatusNotFound, "Tarefa não encontrada")
return
}
writeError(w, http.StatusInternalServerError, "Falha na exclusão")
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Message: "Tarefa excluída com sucesso"})
}
// HealthCheck endpoint de health check
func (h *TaskHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
"status": "ok",
"service": "taskflow",
})
}
1.3 Registro de Rotas e Inicialização do Servidor
// cmd/server/main.go
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"taskflow/internal/handler"
"taskflow/internal/middleware"
"taskflow/internal/service"
"taskflow/internal/store"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
dbPath := getEnv("DB_PATH", "taskflow.db")
st, err := store.NewSQLiteStore(dbPath)
if err != nil {
slog.Error("Falha na inicialização do banco de dados", "error", err)
os.Exit(1)
}
defer st.Close()
svc := service.NewTaskService(st)
h := handler.NewTaskHandler(svc)
mux := http.NewServeMux()
mux.HandleFunc("GET /health", h.HealthCheck)
mux.HandleFunc("GET /api/tasks", h.ListTasks)
mux.HandleFunc("POST /api/tasks", h.CreateTask)
mux.HandleFunc("GET /api/tasks/{id}", h.GetTask)
mux.HandleFunc("PUT /api/tasks/{id}", h.UpdateTask)
mux.HandleFunc("DELETE /api/tasks/{id}", h.DeleteTask)
apiKey := getEnv("API_KEY", "dev-secret-key")
handlerChain := middleware.Chain(
mux,
middleware.CORS(),
middleware.Logging(),
middleware.Auth(apiKey),
)
addr := ":" + getEnv("PORT", "8080")
srv := &http.Server{
Addr: addr,
Handler: handlerChain,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
slog.Info("Serviço TaskFlow iniciado", "addr", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("Servidor saiu anormalmente", "error", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("Sinal de encerramento recebido, encerrando graciosamente...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("Erro no encerramento do servidor", "error", err)
}
slog.Info("Serviço TaskFlow parado")
}
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}
net/http suporta correspondência de método ("GET /path") e parâmetros de caminho ({id}), permitindo construir APIs RESTful sem bibliotecas de roteamento de terceiros.
2. Integração de Middleware
2.1 Padrão de Cadeia de Middleware
Middlewares são "interceptadores" no pipeline de processamento de requisições HTTP que podem executar lógica comum antes e depois das requisições chegarem ao handler:
Requisição → [CORS] → [Logging] → [Auth] → [Handler] → Resposta
↓ ↓ ↓
Controle Registrar Verificar
cross-origin duração identidade
2.2 Infraestrutura de Middleware
// internal/middleware/middleware.go
package middleware
import "net/http"
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
2.3 Middleware CORS
// internal/middleware/cors.go
package middleware
import "net/http"
func CORS() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID")
w.Header().Set("Access-Control-Max-Age", "86400")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
2.4 Middleware de Logging
// internal/middleware/logging.go
package middleware
import (
"log/slog"
"net/http"
"time"
)
type responseWriter struct {
http.ResponseWriter
statusCode int
written int64
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.written += int64(n)
return n, err
}
func Logging() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := newResponseWriter(w)
next.ServeHTTP(rw, r)
duration := time.Since(start)
slog.Info("Requisição HTTP",
"method", r.Method,
"path", r.URL.Path,
"status", rw.statusCode,
"duration_ms", duration.Milliseconds(),
"remote_addr", r.RemoteAddr,
"user_agent", r.UserAgent(),
)
})
}
}
2.5 Middleware de Autenticação
// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
)
func Auth(apiKey string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/health" {
next.ServeHTTP(w, r)
return
}
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.Header().Set("WWW-Authenticate", `Bearer realm="taskflow"`)
http.Error(w, `{"error":"Autenticação ausente","code":401}`, http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, `{"error":"Formato de autenticação inválido","code":401}`, http.StatusUnauthorized)
return
}
if parts[1] != apiKey {
http.Error(w, `{"error":"Falha na autenticação: API key inválida","code":403}`, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
3. Integração com Banco de Dados (SQLite)
3.1 Por que SQLite?
| Recurso | SQLite | MySQL/PostgreSQL |
|---|---|---|
| Instalação | Zero config, arquivo único | Requer serviço separado |
| Caso de uso | Projetos pequenos-médios, embarcado | Ambientes de grande produção |
| Concorrência | Leitura concorrente, escrita serial | Alta concorrência |
| Migração | Auto-implementação necessária | Ferramentas ricas |
3.2 Implementação de Armazenamento SQLite
// internal/store/sqlite.go
package store
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
"taskflow/internal/model"
)
type SQLiteStore struct {
db *sql.DB
}
func NewSQLiteStore(dbPath string) (*SQLiteStore, error) {
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
if err != nil {
return nil, fmt.Errorf("falha ao abrir banco de dados: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("falha na conexão com banco de dados: %w", err)
}
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0)
if err := migrate(db); err != nil {
return nil, fmt.Errorf("falha na migração do banco de dados: %w", err)
}
return &SQLiteStore{db: db}, nil
}
func migrate(db *sql.DB) error {
query := `
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
priority TEXT NOT NULL DEFAULT 'medium',
due_date DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date);
`
_, err := db.Exec(query)
return err
}
func (s *SQLiteStore) Close() error {
return s.db.Close()
}
func (s *SQLiteStore) Create(ctx context.Context, task *model.Task) error {
query := `INSERT INTO tasks (title, description, status, priority, due_date, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`
now := time.Now()
result, err := s.db.ExecContext(ctx, query, task.Title, task.Description, task.Status, task.Priority, task.DueDate, now, now)
if err != nil {
return fmt.Errorf("falha ao inserir tarefa: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("falha ao obter ID de inserção: %w", err)
}
task.ID = int(id)
task.CreatedAt = now
task.UpdatedAt = now
return nil
}
func (s *SQLiteStore) GetByID(ctx context.Context, id int) (*model.Task, error) {
query := `SELECT id, title, description, status, priority, due_date, created_at, updated_at FROM tasks WHERE id = ?`
var task model.Task
var dueDate sql.NullTime
err := s.db.QueryRowContext(ctx, query, id).Scan(&task.ID, &task.Title, &task.Description, &task.Status, &task.Priority, &dueDate, &task.CreatedAt, &task.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("falha ao consultar tarefa: %w", err)
}
if dueDate.Valid {
task.DueDate = &dueDate.Time
}
return &task, nil
}
type TaskFilter struct {
Status model.TaskStatus
Priority model.TaskPriority
}
3.3 Interface de Armazenamento Unificada
// internal/store/store.go
package store
import (
"context"
"taskflow/internal/model"
)
type TaskStore interface {
Create(ctx context.Context, task *model.Task) error
GetByID(ctx context.Context, id int) (*model.Task, error)
List(ctx context.Context, filter TaskFilter, page, pageSize int) ([]model.Task, int, error)
Update(ctx context.Context, task *model.Task) error
Delete(ctx context.Context, id int) error
Close() error
}
TaskStore, sem se importar se o armazenamento subjacente é SQLite ou memória. Injete mock para teste, implementação real para produção.
4. Testes de Integração
Testes de integração verificam se múltiplos componentes trabalham juntos corretamente, usando httptest para simular requisições HTTP reais.
4.1 Executando Testes de Integração
# Executar todos os testes
go test ./... -v
# Executar apenas testes de integração
go test -run TestIntegration -v
# Ver cobertura
go test ./... -cover -coverprofile=coverage.out
# Gerar relatório de cobertura HTML
go tool cover -html=coverage.out -o coverage.html
# Detecção de race condition
go test -race ./...
5. Documentação da API (Swagger/OpenAPI)
OpenAPI é a especificação padrão para descrever APIs RESTful. Para projetos Go, recomenda-se usar swaggo/swag para gerar documentação automaticamente a partir de comentários no código.
# Instalar ferramenta swag
go install github.com/swaggo/swag/cmd/swag@latest
# Gerar documentação no diretório raiz do projeto
swag init -g cmd/server/main.go
6. Diretrizes de Escrita do README
Um excelente README deve incluir: título, funcionalidades, início rápido, estrutura do projeto, testes, implantação e licença.
7. Empacotamento Dockerfile e Lançamento
7.1 Build Multi-Stage
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache gcc musl-dev
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w" -o /app/taskflow ./cmd/server/main.go
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
RUN adduser -D -g '' appuser
WORKDIR /app
COPY --from=builder /app/taskflow .
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget -qO- http://localhost:8080/health || exit 1
ENTRYPOINT ["./taskflow"]
7.2 Docker Compose
version: "3.8"
services:
taskflow:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
- DB_PATH=/data/taskflow.db
- API_KEY=${API_KEY:-change-me-in-production}
- TZ=America/Sao_Paulo
volumes:
- taskflow-data:/data
restart: unless-stopped
volumes:
taskflow-data:
8. Guia de Implantação
8.1 Checklist de Implantação
✅ Verificações de Segurança
□ API_KEY de produção alterada para senha forte
□ Configuração CORS restringe domínios de origem permitidos
□ Logs não contêm informações sensíveis
□ Usando HTTPS (configurar proxy reverso)
✅ Verificações de Desempenho
□ Pool de conexões do banco de dados configurado
□ Timeouts HTTP definidos (Read/Write/Idle)
□ Tamanho do corpo da requisição limitado
✅ Verificações de Confiabilidade
□ Endpoint de health check disponível
□ Lógica de encerramento gracioso implementada
□ Formato de log é JSON estruturado
❓ Perguntas Frequentes
P1: SQLite reporta "database is locked" no Docker?
R: SQLite tem limitações em escritas concorrentes. Defina db.SetMaxOpenConns(1), habilite modo WAL com PRAGMA journal_mode=WAL e adicione ?_busy_timeout=5000 à string de conexão.
P2: A ordem de execução do middleware importa?
R: Sim. Ordem recomendada: CORS → Logging → Auth → Handler de negócio. CORS deve ser tratado primeiro pois requisições preflight não carregam informações de autenticação.
P3: Como gerenciar API keys em produção?
R: Nunca hardcode chaves. Use variáveis de ambiente, arquivos .env ou Docker Secrets.
P4: Devo usar SQLite ou PostgreSQL?
R: Para aprendizado e baixo tráfego, use SQLite. Para alta concorrência ou múltiplos serviços, use PostgreSQL.
📖 Resumo do Curso
Parabéns por completar todas as 30 lições do tutorial de Go!
Fase 1 — Fundamentos Go (Lições 1-6)
Variáveis e tipos, fluxo de controle, funções, arrays/slices, maps.
Fase 2 — Structs e Interfaces (Lições 7-12)
Structs, métodos, interfaces, tratamento de erros, pacotes e módulos.
Fase 3 — Programação Concorrente (Lições 13-18)
Goroutines, channels, select, pacote sync, context.
Fase 4 — Biblioteca Padrão (Lições 19-24)
Strings, arquivos, JSON, HTTP, testes, regex.
Fase 5 — Projetos Abrangentes (Lições 25-30)
CLI, REST API, banco de dados, deploy, projeto completo.
Competências Adquiridas
✅ Sintaxe básica e sistema de tipos Go
✅ Programação orientada a objetos (structs + interfaces)
✅ Programação concorrente (Goroutine + Channel + sync)
✅ Pacotes principais da biblioteca padrão
✅ Design e desenvolvimento de API RESTful
✅ Operações com banco de dados
✅ Desenvolvimento orientado a testes
✅ Engenharia de projeto
✅ Implantação containerizada
📝 Sugestões de Aprendizado Adicional
Direções Avançadas
| Direção | Recursos Recomendados |
|---|---|
| Framework Web | Gin, Echo |
| ORM | GORM, sqlx |
| Microsserviços | go-kit, Kratos |
| Config | Viper |
| Logging | zap, zerolog |
Livros Recomendados
- "The Go Programming Language" — Donovan & Kernighan
- "Go in Action" — William Kennedy
- "Concurrency in Go" — Katherine Cox-Buday
- "100 Go Mistakes and How to Avoid Them" — Teiva Harsanyi
Recursos Online
🎉 Conclusão
"Less is more" — Filosofia de design do Go
30 lições, de fmt.Println("Hello, World!") a construir um serviço RESTful API implantável, você completou toda a jornada de aprendizado da linguagem Go.
O encanto do Go está em sua simplicidade — ele não te dá muitas escolhas, mas cada escolha é cuidadosamente considerada. Programação é um ofício, e a melhor maneira de aprender é escrever código. Digite todos os exemplos das 30 lições, complete todos os exercícios, e depois encontre um pequeno projeto real para praticar.
╔══════════════════════════════════════════════════╗
║ Parabéns por completar as 30 lições ║
║ do tutorial de Go! ║
║ Você agora tem a base para desenvolver ║
║ aplicações Go de nível produção. ║
║ Vá construir algo incrível! ║
╚══════════════════════════════════════════════════╝
Próxima Lição
Esta é a última lição. Se você gostaria de revisitar o ponto de partida do curso, volte para Lição 1: Introdução ao Go.



