404 Not Found

404 Not Found


nginx

实战:学生管理系统

实战:学生管理系统

想象你是一位班主任,手边有一个笔记本:每一页记录着一位学生的姓名、学号,以及各科成绩。当新同学转来,你翻到空白页登记;考试结束后,你找到对应页面填写成绩;期末时,你按总分排名打印成绩单。整个过程就是在做"增删改查"。

今天我们把这个笔记本搬进电脑——用 Go 语言从零搭建一个命令行学生管理系统,把前面学到的结构体、方法、接口和错误处理全部串起来。


项目需求

功能 说明
添加学生 输入姓名和学号,创建新记录
添加课程成绩 为指定学生录入某门课的成绩
查询学生 按学号查找,显示学生信息和全部成绩
列出所有学生 展示所有已录入学生及其平均分
删除学生 按学号移除一位学生
退出程序 保存数据并退出

系统设计

┌─────────────────────────────────────────────┐
│                  CLI 菜单                    │
├─────────────────────────────────────────────┤
│              StudentManager                  │
│  (持有 Storer 接口,执行业务逻辑)              │
├─────────────────────────────────────────────┤
│              Storer 接口                      │
│  Save / Load —— 可替换存储后端                │
├──────────────────┬──────────────────────────┤
│   MemoryStore    │   FileStore (可选扩展)    │
│   (内存 map)     │   (JSON 文件)             │
└──────────────────┴──────────────────────────┘

核心类型:


完整代码

创建文件 main.go,将以下代码完整复制:

GO
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
)

// ============================================================
// 数据结构定义
// ============================================================

// Course 表示一门课程的成绩
type Course struct {
	Name  string  `json:"name"`
	Score float64 `json:"score"`
}

// Student 表示一名学生
type Student struct {
	ID      string   `json:"id"`
	Name    string   `json:"name"`
	Courses []Course `json:"courses"`
}

// Average 计算学生的平均分
func (s Student) Average() float64 {
	if len(s.Courses) == 0 {
		return 0
	}
	total := 0.0
	for _, c := range s.Courses {
		total += c.Score
	}
	return total / float64(len(s.Courses))
}

// AddCourse 为学生添加一门课程成绩
func (s *Student) AddCourse(name string, score float64) {
	// 如果课程已存在,则更新成绩
	for i, c := range s.Courses {
		if c.Name == name {
			s.Courses[i].Score = score
			return
		}
	}
	s.Courses = append(s.Courses, Course{Name: name, Score: score})
}

// String 实现 fmt.Stringer 接口,方便打印
func (s Student) String() string {
	return fmt.Sprintf("[%s] %s (平均分: %.1f)", s.ID, s.Name, s.Average())
}

// ============================================================
// 存储接口
// ============================================================

// Storer 定义存储操作的抽象接口
type Storer interface {
	Save(students map[string]*Student) error
	Load() (map[string]*Student, error)
}

// ------------------------------------------------------------
// 内存存储实现
// ------------------------------------------------------------

// MemoryStore 将数据保存在内存中(程序退出后丢失)
type MemoryStore struct{}

func NewMemoryStore() *MemoryStore {
	return &MemoryStore{}
}

func (m *MemoryStore) Save(students map[string]*Student) error {
	// 内存存储不需要持久化,直接返回 nil
	return nil
}

func (m *MemoryStore) Load() (map[string]*Student, error) {
	return make(map[string]*Student), nil
}

// ------------------------------------------------------------
// 文件存储实现
// ------------------------------------------------------------

// FileStore 将数据以 JSON 格式保存到文件
type FileStore struct {
	Path string
}

func NewFileStore(path string) *FileStore {
	return &FileStore{Path: path}
}

func (f *FileStore) Save(students map[string]*Student) error {
	data, err := json.MarshalIndent(students, "", "  ")
	if err != nil {
		return fmt.Errorf("序列化失败: %w", err)
	}
	err = os.WriteFile(f.Path, data, 0644)
	if err != nil {
		return fmt.Errorf("写入文件失败: %w", err)
	}
	return nil
}

func (f *FileStore) Load() (map[string]*Student, error) {
	students := make(map[string]*Student)

	data, err := os.ReadFile(f.Path)
	if err != nil {
		if os.IsNotExist(err) {
			// 文件不存在,返回空 map
			return students, nil
		}
		return nil, fmt.Errorf("读取文件失败: %w", err)
	}

	err = json.Unmarshal(data, &students)
	if err != nil {
		return nil, fmt.Errorf("解析数据失败: %w", err)
	}
	return students, nil
}

// ============================================================
// 业务逻辑层
// ============================================================

// StudentManager 管理所有学生数据
type StudentManager struct {
	students map[string]*Student
	store    Storer
	reader  *bufio.Reader
}

// NewStudentManager 创建一个新的管理器
func NewStudentManager(store Storer) (*StudentManager, error) {
	students, err := store.Load()
	if err != nil {
		return nil, fmt.Errorf("加载数据失败: %w", err)
	}
	return &StudentManager{
		students: students,
		store:    store,
		reader:   bufio.NewReader(os.Stdin),
	}, nil
}

// AddStudent 添加一名新学生
func (m *StudentManager) AddStudent(id, name string) error {
	if id == "" || name == "" {
		return fmt.Errorf("学号和姓名不能为空")
	}
	if _, exists := m.students[id]; exists {
		return fmt.Errorf("学号 %s 已存在", id)
	}
	m.students[id] = &Student{ID: id, Name: name}
	return m.store.Save(m.students)
}

// AddCourse 为学生添加课程成绩
func (m *StudentManager) AddCourse(id, courseName string, score float64) error {
	student, err := m.findStudent(id)
	if err != nil {
		return err
	}
	if score < 0 || score > 100 {
		return fmt.Errorf("成绩必须在 0-100 之间")
	}
	student.AddCourse(courseName, score)
	return m.store.Save(m.students)
}

// GetStudent 查询学生信息
func (m *StudentManager) GetStudent(id string) (*Student, error) {
	return m.findStudent(id)
}

// DeleteStudent 删除一名学生
func (m *StudentManager) DeleteStudent(id string) error {
	if _, exists := m.students[id]; !exists {
		return fmt.Errorf("学号 %s 不存在", id)
	}
	delete(m.students, id)
	return m.store.Save(m.students)
}

// ListStudents 返回按学号排序的所有学生切片
func (m *StudentManager) ListStudents() []*Student {
	list := make([]*Student, 0, len(m.students))
	for _, s := range m.students {
		list = append(list, s)
	}
	sort.Slice(list, func(i, j int) bool {
		return list[i].ID < list[j].ID
	})
	return list
}

// findStudent 根据学号查找学生(内部辅助方法)
func (m *StudentManager) findStudent(id string) (*Student, error) {
	s, exists := m.students[id]
	if !exists {
		return nil, fmt.Errorf("学号 %s 不存在", id)
	}
	return s, nil
}

// ============================================================
// CLI 交互层
// ============================================================

// Run 启动命令行交互循环
func (m *StudentManager) Run() {
	for {
		m.printMenu()
		choice := m.readLine("请输入选项: ")

		switch choice {
		case "1":
			m.handleAddStudent()
		case "2":
			m.handleAddCourse()
		case "3":
			m.handleGetStudent()
		case "4":
			m.handleListStudents()
		case "5":
			m.handleDeleteStudent()
		case "6":
			fmt.Println("\n数据已保存,再见!")
			return
		default:
			fmt.Println("\n⚠ 无效选项,请重新输入")
		}
		fmt.Println()
	}
}

func (m *StudentManager) printMenu() {
	fmt.Println("╔══════════════════════════════╗")
	fmt.Println("║      学生管理系统 v1.0       ║")
	fmt.Println("╠══════════════════════════════╣")
	fmt.Println("║  1. 添加学生                 ║")
	fmt.Println("║  2. 添加课程成绩             ║")
	fmt.Println("║  3. 查询学生                 ║")
	fmt.Println("║  4. 列出所有学生             ║")
	fmt.Println("║  5. 删除学生                 ║")
	fmt.Println("║  6. 退出                     ║")
	fmt.Println("╚══════════════════════════════╝")
}

func (m *StudentManager) handleAddStudent() {
	id := m.readLine("请输入学号: ")
	name := m.readLine("请输入姓名: ")
	err := m.AddStudent(id, name)
	if err != nil {
		fmt.Printf("\n❌ 添加失败: %v\n", err)
		return
	}
	fmt.Printf("\n✅ 学生 %s 添加成功!\n", name)
}

func (m *StudentManager) handleAddCourse() {
	id := m.readLine("请输入学号: ")
	courseName := m.readLine("请输入课程名称: ")
	scoreStr := m.readLine("请输入成绩 (0-100): ")

	score, err := strconv.ParseFloat(scoreStr, 64)
	if err != nil {
		fmt.Printf("\n❌ 成绩格式错误: %v\n", err)
		return
	}

	err = m.AddCourse(id, courseName, score)
	if err != nil {
		fmt.Printf("\n❌ 添加失败: %v\n", err)
		return
	}
	fmt.Printf("\n✅ 成绩添加成功!\n")
}

func (m *StudentManager) handleGetStudent() {
	id := m.readLine("请输入学号: ")
	s, err := m.GetStudent(id)
	if err != nil {
		fmt.Printf("\n❌ 查询失败: %v\n", err)
		return
	}
	fmt.Println("\n─── 学生信息 ───")
	fmt.Printf("学号: %s\n", s.ID)
	fmt.Printf("姓名: %s\n", s.Name)
	if len(s.Courses) == 0 {
		fmt.Println("暂无课程成绩")
	} else {
		fmt.Println("课程成绩:")
		for _, c := range s.Courses {
			fmt.Printf("  %s: %.1f\n", c.Name, c.Score)
		}
		fmt.Printf("平均分: %.1f\n", s.Average())
	}
}

func (m *StudentManager) handleListStudents() {
	list := m.ListStudents()
	if len(list) == 0 {
		fmt.Println("\n暂无学生记录")
		return
	}
	fmt.Println("\n─── 所有学生 ───")
	for _, s := range list {
		courseCount := len(s.Courses)
		fmt.Printf("  %s  课程数: %d  平均分: %.1f\n", s, courseCount, s.Average())
	}
	fmt.Printf("\n共 %d 名学生\n", len(list))
}

func (m *StudentManager) handleDeleteStudent() {
	id := m.readLine("请输入要删除的学号: ")
	err := m.DeleteStudent(id)
	if err != nil {
		fmt.Printf("\n❌ 删除失败: %v\n", err)
		return
	}
	fmt.Printf("\n✅ 学号 %s 已删除\n", id)
}

// readLine 读取一行用户输入并去除首尾空格
func (m *StudentManager) readLine(prompt string) string {
	fmt.Print(prompt)
	line, _ := m.reader.ReadString('\n')
	return strings.TrimSpace(line)
}

// ============================================================
// 程序入口
// ============================================================

func main() {
	// 使用文件存储(数据保存到 students.json)
	store := NewFileStore("students.json")

	// 如果想用纯内存存储(退出后数据丢失),改为:
	// store := NewMemoryStore()

	mgr, err := NewStudentManager(store)
	if err != nil {
		fmt.Fprintf(os.Stderr, "初始化失败: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("欢迎使用学生管理系统!")
	mgr.Run()
}

代码解析

1. 数据结构设计

GO
type Student struct {
    ID      string   `json:"id"`
    Name    string   `json:"name"`
    Courses []Course `json:"courses"`
}

2. 方法与指针接收者

GO
func (s *Student) AddCourse(name string, score float64) { ... }
func (s Student) Average() float64 { ... }

3. 接口抽象 — Storer

GO
type Storer interface {
    Save(students map[string]*Student) error
    Load() (map[string]*Student, error)
}

4. 错误处理策略

本项目在每一层都进行错误处理:

层级 策略
数据验证 检查学号/姓名非空、成绩范围 0-100
业务逻辑 检查学号是否存在,存在则报错
存储层 fmt.Errorf("...: %w", err) 包装底层错误
CLI 层 捕获错误,向用户显示友好的中文提示
GO
return fmt.Errorf("序列化失败: %w", err)

%w 包装错误保留了原始信息,调用者可以用 errors.Iserrors.As 判断错误类型。

5. CLI 菜单循环

GO
for {
    m.printMenu()
    choice := m.readLine("请输入选项: ")
    switch choice { ... }
}

主循环不断打印菜单、读取输入、分发到对应处理函数。这种"读取-分发"模式是命令行程序的经典结构。

6. 可扩展的存储层

切换存储方式只需修改 main() 中的一行:

GO
// 内存模式
store := NewMemoryStore()

// 文件模式
store := NewFileStore("students.json")

StudentManager 完全不感知底层变化——这正是接口的价值。


运行与测试

BASH
# 编译并运行
go run main.go
欢迎使用学生管理系统!
╔══════════════════════════════╗
║      学生管理系统 v1.0       ║
╠══════════════════════════════╣
║  1. 添加学生                 ║
║  2. 添加课程成绩             ║
║  3. 查询学生                 ║
║  4. 列出所有学生             ║
║  5. 删除学生                 ║
║  6. 退出                     ║
╚══════════════════════════════╝
请输入选项: 1
请输入学号: 1001
请输入姓名: 张三

✅ 学生 张三 添加成功!

操作结束后,程序会在当前目录生成 students.json 文件,下次启动时自动加载。


❓ 常见问题

Q1: 为什么 Average() 用值接收者,AddCourse() 用指针接收者?

Average() 只读取数据计算总和,不修改 Student,用值接收者更安全。AddCourse() 需要向 Courses 切片追加元素,必须用指针接收者才能修改调用者的原始数据。简单原则:要修改就用指针,只读取就用值

Q2: map[string]*Student 中的 * 是什么意思?

这是一个值为指针的 map。m.students[id] 返回的是 *Student(指向 Student 的指针),而不是 Student 的副本。这样我们通过 AddCourse 修改学生数据时,改动会直接反映在 map 中,无需额外处理。

Q3: 为什么 Load() 中要单独处理文件不存在的情况?

首次运行程序时 students.json 还不存在,os.ReadFile 会返回错误。如果直接报错退出,用户就永远无法启动程序。因此用 os.IsNotExist(err) 判断——文件不存在是正常情况,返回空 map 即可。

Q4: 如何扩展为数据库存储?

只需实现 Storer 接口:

GO
type MySQLStore struct {
    db *sql.DB
}

func (m *MySQLStore) Save(students map[string]*Student) error {
    // INSERT/UPDATE 到数据库
}

func (m *MySQLStore) Load() (map[string]*Student, error) {
    // 从数据库 SELECT
}

然后在 main() 中传入 &MySQLStore{db: db},业务代码无需任何修改。


📖 小节

本课通过一个完整项目,将 Phase 2 的核心知识串联起来:

这就是 Go 工程实践的基本模式:用结构体建模,用方法封装行为,用接口解耦依赖,用 error 传递异常。


📝 作业

作业 1:添加按平均分排序功能

新增菜单选项"按平均分排名",将所有学生按平均分从高到低排序后输出。提示:使用 sort.Slice

作业 2:添加导入/导出功能

实现两个新方法:

作业 3:添加数据验证中间层

创建一个 ValidatingStore 结构体,它包装任意 Storer 实现,在 Save 之前验证所有学生数据(学号非空、姓名非空、成绩在 0-100 之间)。这就是装饰器模式在 Go 中的实践。


下一课:Goroutine 与并发 →
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏