Pratica: Sistema de Gerenciamento de Alunos
Pratica: Sistema de Gerenciamento de Alunos
Imagine que voce e um professor com um caderno em maos: cada pagina registra o nome de um aluno, numero de matricula e notas de cada disciplina. Quando um novo aluno se transfere, voce abre uma pagina em branco para registra-lo; apos as provas, voce encontra a pagina correspondente para registrar as notas; no final do semestre, voce imprime boletins classificados por nota total. Todo o processo esta realizando operacoes de "CRUD".
Hoje estamos movendo esse caderno para o computador — construindo um sistema de gerenciamento de alunos em linha de comando do zero em Go, conectando tudo que aprendemos sobre structs, metodos, interfaces e tratamento de erros.
Requisitos do Projeto
| Funcionalidade | Descricao |
|---|---|
| Adicionar aluno | Inserir nome e numero de matricula para criar um novo registro |
| Adicionar nota de disciplina | Registrar uma nota para uma disciplina especifica de um aluno |
| Consultar aluno | Buscar por numero de matricula, exibir informacoes do aluno e todas as notas |
| Listar todos os alunos | Mostrar todos os alunos registrados com suas medias |
| Remover aluno | Remover um aluno por numero de matricula |
| Sair do programa | Salvar dados e sair |
Projeto do Sistema
+---------------------------------------------+
| Menu CLI |
+---------------------------------------------+
| GerenciadorAlunos |
| (Armazena interface Armazenador, |
| executa logica) |
+---------------------------------------------+
| Interface Armazenador |
| Salvar / Carregar -- backend de |
| armazenamento substituivel |
+-------------------+-------------------------+
| ArmazenadorMem | ArmazenadorArq |
| (map em memoria) | (arquivo JSON) |
+-------------------+-------------------------+
Tipos principais:
- Aluno — Informacoes do aluno + notas das disciplinas
- Disciplina — Nota de uma unica disciplina
- Armazenador — Interface de abstracao de armazenamento
- GerenciadorAlunos — Camada de logica de negocio
Exemplo 1: Código Completo
Crie um arquivo main.go e copie o codigo completo a seguir:
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"sort"
"strconv"
"strings"
)
// ============================================================
// Definicoes de Estrutura de Dados
// ============================================================
// Disciplina representa uma nota de disciplina
type Disciplina struct {
Nome string `json:"nome"`
Nota float64 `json:"nota"`
}
// Aluno representa um aluno
type Aluno struct {
ID string `json:"id"`
Nome string `json:"nome"`
Disciplinas []Disciplina `json:"disciplinas"`
}
// Media calcula a media do aluno
func (a Aluno) Media() float64 {
if len(a.Disciplinas) == 0 {
return 0
}
total := 0.0
for _, d := range a.Disciplinas {
total += d.Nota
}
return total / float64(len(a.Disciplinas))
}
// AdicionarDisciplina adiciona uma nota de disciplina ao aluno
func (a *Aluno) AdicionarDisciplina(nome string, nota float64) {
// Se a disciplina ja existe, atualizar a nota
for i, d := range a.Disciplinas {
if d.Nome == nome {
a.Disciplinas[i].Nota = nota
return
}
}
a.Disciplinas = append(a.Disciplinas, Disciplina{Nome: nome, Nota: nota})
}
// String implementa a interface fmt.Stringer para impressao facilitada
func (a Aluno) String() string {
return fmt.Sprintf("[%s] %s (Media: %.1f)", a.ID, a.Nome, a.Media())
}
// ============================================================
// Interface de Armazenamento
// ============================================================
// Armazenador define a interface abstrata de armazenamento
type Armazenador interface {
Salvar(alunos map[string]*Aluno) error
Carregar() (map[string]*Aluno, error)
}
// ------------------------------------------------------------
// Implementacao de Armazenamento em Memoria
// ------------------------------------------------------------
// ArmazenadorMem armazena dados em memoria (perdido quando o programa sai)
type ArmazenadorMem struct{}
func NovoArmazenadorMem() *ArmazenadorMem {
return &ArmazenadorMem{}
}
func (m *ArmazenadorMem) Salvar(alunos map[string]*Aluno) error {
// Armazenamento em memoria nao precisa de persistencia, apenas retorna nil
return nil
}
func (m *ArmazenadorMem) Carregar() (map[string]*Aluno, error) {
return make(map[string]*Aluno), nil
}
// ------------------------------------------------------------
// Implementacao de Armazenamento em Arquivo
// ------------------------------------------------------------
// ArmazenadorArq salva dados em um arquivo em formato JSON
type ArmazenadorArq struct {
Caminho string
}
func NovoArmazenadorArq(caminho string) *ArmazenadorArq {
return &ArmazenadorArq{Caminho: caminho}
}
func (f *ArmazenadorArq) Salvar(alunos map[string]*Aluno) error {
dados, err := json.MarshalIndent(alunos, "", " ")
if err != nil {
return fmt.Errorf("falha na serializacao: %w", err)
}
err = os.WriteFile(f.Caminho, dados, 0644)
if err != nil {
return fmt.Errorf("falha ao escrever arquivo: %w", err)
}
return nil
}
func (f *ArmazenadorArq) Carregar() (map[string]*Aluno, error) {
alunos := make(map[string]*Aluno)
dados, err := os.ReadFile(f.Caminho)
if err != nil {
if os.IsNotExist(err) {
// Arquivo nao existe, retornar mapa vazio
return alunos, nil
}
return nil, fmt.Errorf("falha ao ler arquivo: %w", err)
}
err = json.Unmarshal(dados, &alunos)
if err != nil {
return nil, fmt.Errorf("falha ao analisar dados: %w", err)
}
return alunos, nil
}
// ============================================================
// Camada de Logica de Negocio
// ============================================================
// GerenciadorAlunos gerencia todos os dados dos alunos
type GerenciadorAlunos struct {
alunos map[string]*Aluno
armazenador Armazenador
leitor *bufio.Reader
}
// NovoGerenciadorAlunos cria um novo gerenciador
func NovoGerenciadorAlunos(armazenador Armazenador) (*GerenciadorAlunos, error) {
alunos, err := armazenador.Carregar()
if err != nil {
return nil, fmt.Errorf("falha ao carregar dados: %w", err)
}
return &GerenciadorAlunos{
alunos: alunos,
armazenador: armazenador,
leitor: bufio.NewReader(os.Stdin),
}, nil
}
// AdicionarAluno adiciona um novo aluno
func (g *GerenciadorAlunos) AdicionarAluno(id, nome string) error {
if id == "" || nome == "" {
return fmt.Errorf("ID do aluno e nome nao podem ser vazios")
}
if _, existe := g.alunos[id]; existe {
return fmt.Errorf("ID do aluno %s ja existe", id)
}
g.alunos[id] = &Aluno{ID: id, Nome: nome}
return g.armazenador.Salvar(g.alunos)
}
// AdicionarDisciplina adiciona uma nota de disciplina a um aluno
func (g *GerenciadorAlunos) AdicionarDisciplina(id, nomeDisciplina string, nota float64) error {
aluno, err := g.encontrarAluno(id)
if err != nil {
return err
}
if nota < 0 || nota > 100 {
return fmt.Errorf("nota deve estar entre 0 e 100")
}
aluno.AdicionarDisciplina(nomeDisciplina, nota)
return g.armazenador.Salvar(g.alunos)
}
// ObterAluno consulta informacoes do aluno
func (g *GerenciadorAlunos) ObterAluno(id string) (*Aluno, error) {
return g.encontrarAluno(id)
}
// RemoverAluno remove um aluno
func (g *GerenciadorAlunos) RemoverAluno(id string) error {
if _, existe := g.alunos[id]; !existe {
return fmt.Errorf("ID do aluno %s nao existe", id)
}
delete(g.alunos, id)
return g.armazenador.Salvar(g.alunos)
}
// ListarAlunos retorna todos os alunos ordenados por ID
func (g *GerenciadorAlunos) ListarAlunos() []*Aluno {
lista := make([]*Aluno, 0, len(g.alunos))
for _, a := range g.alunos {
lista = append(lista, a)
}
sort.Slice(lista, func(i, j int) bool {
return lista[i].ID < lista[j].ID
})
return lista
}
// encontrarAluno encontra um aluno por ID (ajudante interno)
func (g *GerenciadorAlunos) encontrarAluno(id string) (*Aluno, error) {
a, existe := g.alunos[id]
if !existe {
return nil, fmt.Errorf("ID do aluno %s nao existe", id)
}
return a, nil
}
// ============================================================
// Camada de Interacao CLI
// ============================================================
// Executar inicia o loop de interacao em linha de comando
func (g *GerenciadorAlunos) Executar() {
for {
g.imprimirMenu()
opcao := g.lerLinha("Escolha uma opcao: ")
switch opcao {
case "1":
g.handleAdicionarAluno()
case "2":
g.handleAdicionarDisciplina()
case "3":
g.handleObterAluno()
case "4":
g.handleListarAlunos()
case "5":
g.handleRemoverAluno()
case "6":
fmt.Println("\nDados salvos. Ate logo!")
return
default:
fmt.Println("\nOpcao invalida, tente novamente")
}
fmt.Println()
}
}
func (g *GerenciadorAlunos) imprimirMenu() {
fmt.Println("+------------------------------+")
fmt.Println("| Gerenciador de Alunos v1.0 |")
fmt.Println("+------------------------------+")
fmt.Println("| 1. Adicionar Aluno |")
fmt.Println("| 2. Adicionar Nota |")
fmt.Println("| 3. Consultar Aluno |")
fmt.Println("| 4. Listar Todos os Alunos |")
fmt.Println("| 5. Remover Aluno |")
fmt.Println("| 6. Sair |")
fmt.Println("+------------------------------+")
}
func (g *GerenciadorAlunos) handleAdicionarAluno() {
id := g.lerLinha("Digite o ID do aluno: ")
nome := g.lerLinha("Digite o nome do aluno: ")
err := g.AdicionarAluno(id, nome)
if err != nil {
fmt.Printf("\nFalha ao adicionar: %v\n", err)
return
}
fmt.Printf("\nAluno %s adicionado com sucesso!\n", nome)
}
func (g *GerenciadorAlunos) handleAdicionarDisciplina() {
id := g.lerLinha("Digite o ID do aluno: ")
nomeDisciplina := g.lerLinha("Digite o nome da disciplina: ")
notaStr := g.lerLinha("Digite a nota (0-100): ")
nota, err := strconv.ParseFloat(notaStr, 64)
if err != nil {
fmt.Printf("\nFormato de nota invalido: %v\n", err)
return
}
err = g.AdicionarDisciplina(id, nomeDisciplina, nota)
if err != nil {
fmt.Printf("\nFalha ao adicionar: %v\n", err)
return
}
fmt.Printf("\nNota adicionada com sucesso!\n")
}
func (g *GerenciadorAlunos) handleObterAluno() {
id := g.lerLinha("Digite o ID do aluno: ")
a, err := g.ObterAluno(id)
if err != nil {
fmt.Printf("\nFalha na consulta: %v\n", err)
return
}
fmt.Println("\n--- Informacoes do Aluno ---")
fmt.Printf("ID: %s\n", a.ID)
fmt.Printf("Nome: %s\n", a.Nome)
if len(a.Disciplinas) == 0 {
fmt.Println("Nenhuma nota ainda")
} else {
fmt.Println("Notas das disciplinas:")
for _, d := range a.Disciplinas {
fmt.Printf(" %s: %.1f\n", d.Nome, d.Nota)
}
fmt.Printf("Media: %.1f\n", a.Media())
}
}
func (g *GerenciadorAlunos) handleListarAlunos() {
lista := g.ListarAlunos()
if len(lista) == 0 {
fmt.Println("\nNenhum registro de aluno")
return
}
fmt.Println("\n--- Todos os Alunos ---")
for _, a := range lista {
contagemDisciplinas := len(a.Disciplinas)
fmt.Printf(" %s Disciplinas: %d Media: %.1f\n", a, contagemDisciplinas, a.Media())
}
fmt.Printf("\nTotal: %d alunos\n", len(lista))
}
func (g *GerenciadorAlunos) handleRemoverAluno() {
id := g.lerLinha("Digite o ID do aluno para remover: ")
err := g.RemoverAluno(id)
if err != nil {
fmt.Printf("\nFalha ao remover: %v\n", err)
return
}
fmt.Printf("\nAluno com ID %s removido\n", id)
}
// lerLinha le uma linha de entrada do usuario e remove espacos em branco
func (g *GerenciadorAlunos) lerLinha(prompt string) string {
fmt.Print(prompt)
linha, _ := g.leitor.ReadString('\n')
return strings.TrimSpace(linha)
}
// ============================================================
// Entrada do Programa
// ============================================================
func main() {
// Usar armazenamento em arquivo (dados salvos em alunos.json)
armazenador := NovoArmazenadorArq("alunos.json")
// Para usar armazenamento em memoria (dados perdidos ao sair), mude para:
// armazenador := NovoArmazenadorMem()
mgr, err := NovoGerenciadorAlunos(armazenador)
if err != nil {
fmt.Fprintf(os.Stderr, "Falha na inicializacao: %v\n", err)
os.Exit(1)
}
fmt.Println("Bem-vindo ao Sistema de Gerenciamento de Alunos!")
mgr.Executar()
}
Analise do Codigo
1. Projeto da Estrutura de Dados
type Aluno struct {
ID string `json:"id"`
Nome string `json:"nome"`
Disciplinas []Disciplina `json:"disciplinas"`
}
Alunocontem um slice deDisciplinas, cada uma contendo nome da disciplina e nota.- Tags
jsonpermitem que a struct seja serializada diretamente para JSON para armazenamento em arquivo.
2. Metodos e Receptores por Ponteiro
func (a *Aluno) AdicionarDisciplina(nome string, nota float64) { ... }
func (a Aluno) Media() float64 { ... }
AdicionarDisciplinamodifica o slice, entao usa receptor por ponteiro*Aluno.Mediaapenas le dados, entao um receptor por valorAlunoe suficiente.AdicionarDisciplinaimplementa logica de "atualizar se nome da disciplina existe" para evitar duplicatas.
3. Abstracao por Interface — Armazenador
type Armazenador interface {
Salvar(alunos map[string]*Aluno) error
Carregar() (map[string]*Aluno, error)
}
GerenciadorAlunosdepende apenas da interfaceArmazenador, nao do metodo de armazenamento especifico.- Duas implementacoes sao fornecidas:
ArmazenadorMem(em memoria) eArmazenadorArq(arquivo JSON). - Quer trocar para um banco de dados? Apenas adicione um novo tipo implementando
Armazenador— zero alteracoes no codigo de negocio.
4. Estrategia de Tratamento de Erros
Este projeto trata erros em cada camada:
| Camada | Estrategia |
|---|---|
| Validacao de dados | Verificar se ID/nome do aluno nao estao vazios, nota entre 0-100 |
| Logica de negocio | Verificar se ID do aluno existe, erro se duplicado |
| Camada de armazenamento | Envolver erros subjacentes com fmt.Errorf("...: %w", err) |
| Camada CLI | Capturar erros e exibir mensagens amigaveis aos usuarios |
return fmt.Errorf("falha na serializacao: %w", err)
O encapsulamento com %w preserva a informacao do erro original, permitindo que chamadores usem errors.Is ou errors.As para verificar tipos de erro.
5. Loop do Menu CLI
for {
g.imprimirMenu()
opcao := g.lerLinha("Escolha uma opcao: ")
switch opcao { ... }
}
O loop principal continuamente imprime o menu, le a entrada e despacha para o handler correspondente. Este padrao de "leitura-despacho" e a estrutura classica para programas em linha de comando.
6. Camada de Armazenamento Extensivel
Trocar metodos de armazenamento requer apenas alterar uma linha em main():
// Modo em memoria
armazenador := NovoArmazenadorMem()
// Modo arquivo
armazenador := NovoArmazenadorArq("alunos.json")
GerenciadorAlunos esta completamente desatento a mudanca subjacente — este e o valor das interfaces.
Executando e Testando
# Compilar e executar
go run main.go
Bem-vindo ao Sistema de Gerenciamento de Alunos!
+------------------------------+
| Gerenciador de Alunos v1.0 |
+------------------------------+
| 1. Adicionar Aluno |
| 2. Adicionar Nota |
| 3. Consultar Aluno |
| 4. Listar Todos os Alunos |
| 5. Remover Aluno |
| 6. Sair |
+------------------------------+
Escolha uma opcao: 1
Digite o ID do aluno: 1001
Digite o nome do aluno: Alice
Aluno Alice adicionado com sucesso!
Quando terminar, o programa gera um arquivo alunos.json no diretorio atual, que e automaticamente carregado na proxima inicializacao.
❓ Perguntas Frequentes
P1: Por que Media() usa receptor por valor enquanto AdicionarDisciplina() usa receptor por ponteiro?
Media() apenas le dados para calcular uma soma, sem modificar Aluno, entao um receptor por valor e mais seguro. AdicionarDisciplina() precisa adicionar elementos ao slice de Disciplinas, requerendo um receptor por ponteiro para modificar os dados originais do chamador. Principio simples: use ponteiro para modificar, use valor para ler.
P2: O que * significa em map[string]*Aluno?
Isso e um mapa com valores de ponteiro. m.alunos[id] retorna *Aluno (um ponteiro para Aluno), nao uma copia de Aluno. Dessa forma, quando modificamos dados do aluno via AdicionarDisciplina, as alteracoes se refletem diretamente no mapa sem tratamento adicional.
P3: Por que Carregar() trata separadamente o caso em que o arquivo nao existe?
Na primeira execucao, alunos.json ainda nao existe, e os.ReadFile retornara um erro. Se apenas reportarmos o erro e sairmos, o usuario nunca conseguirá iniciar o programa. Entao verificamos com os.IsNotExist(err) — um arquivo ausente e normal, e retornamos um mapa vazio.
P4: Como estender para armazenamento em banco de dados?
Apenas implemente a interface Armazenador:
type ArmazenadorMySQL struct {
db *sql.DB
}
func (m *ArmazenadorMySQL) Salvar(alunos map[string]*Aluno) error {
// INSERT/UPDATE no banco de dados
}
func (m *ArmazenadorMySQL) Carregar() (map[string]*Aluno, error) {
// SELECT do banco de dados
}
Depois passe &ArmazenadorMySQL{db: db} em main(), e o codigo de negocio nao precisa de alteracoes.
📖 Resumo
Esta licao conecta o conhecimento principal da Fase 2 atraves de um projeto completo:
- Structs —
AlunoeDisciplinadefinem o modelo de dados; tagsjsonsuportam serializacao. - Metodos — Receptores por valor para operacoes somente leitura; receptores por ponteiro para modificar dados.
- Interfaces — Interface
Armazenadordesacopla logica de negocio da implementacao de armazenamento, demonstrando programacao orientada a interfaces do Go. - Tratamento de erros — Validacao e encapsulamento de erros em cada camada; a camada CLI exibe mensagens amigaveis aos usuarios.
- Organizacao de pacotes — Todo codigo em um pacote, mas com separacao clara de responsabilidades atraves de tipos e metodos.
Este e o padrao fundamental da engenharia Go: modelar com structs, encapsular comportamento com metodos, desacoplar dependencias com interfaces e propagar excecoes com erros.
📝 Exercicios
Exercicio 1: Adicionar Ordenacao por Media
Adicionar uma nova opcao de menu "Classificar por Media" que ordena todos os alunos por media de nota da mais alta para a mais baixa e exibe o resultado. Dica: usar sort.Slice.
Exercicio 2: Adicionar Funcionalidade de Importacao/Exportacao
Implementar dois novos metodos:
ExportarCSV(nomeArquivo string) error— Exportar todos os alunos para um arquivo CSV (ID do aluno, nome, disciplina, nota).ImportarCSV(nomeArquivo string) error— Importar alunos e notas em lote de um arquivo CSV.
Exercicio 3: Adicionar Middleware de Validacao de Dados
Criar uma struct ArmazenadorValidador que envolve qualquer implementacao de Armazenador, validando todos os dados do aluno antes de Salvar (ID do aluno nao vazio, nome nao vazio, nota entre 0-100). Este e o padrao decorador na pratica.



