完成
This commit is contained in:
@@ -1,24 +1,88 @@
|
||||
# 计算器项目
|
||||
|
||||
一个支持基本四则运算的命令行计算器程序。
|
||||
这是一个简单的命令行计算器项目,演示了 Go 语言的基本语法和编程概念的综合应用。
|
||||
|
||||
## 功能特性
|
||||
- 支持加法、减法、乘法、除法
|
||||
- 错误处理(如除零错误)
|
||||
## 项目特性
|
||||
|
||||
- 支持基本四则运算(加、减、乘、除)
|
||||
- 支持括号运算
|
||||
- 支持浮点数计算
|
||||
- 错误处理和输入验证
|
||||
- 交互式命令行界面
|
||||
- 输入验证
|
||||
- 历史记录功能
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
01-calculator/
|
||||
├── README.md # 项目说明文档
|
||||
├── main.go # 主程序入口
|
||||
├── calculator/ # 计算器核心包
|
||||
│ ├── calculator.go # 计算器主要逻辑
|
||||
│ ├── parser.go # 表达式解析器
|
||||
│ └── history.go # 历史记录管理
|
||||
└── calculator_test.go # 测试文件
|
||||
```
|
||||
|
||||
## 运行方法
|
||||
|
||||
```bash
|
||||
cd 01-calculator
|
||||
# 进入项目目录
|
||||
cd 10-projects/01-calculator
|
||||
|
||||
# 运行程序
|
||||
go run main.go
|
||||
|
||||
# 或者编译后运行
|
||||
go build -o calculator main.go
|
||||
./calculator
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
```
|
||||
欢迎使用 Go 计算器!
|
||||
请输入第一个数字: 10
|
||||
请选择运算符 (+, -, *, /): +
|
||||
请输入第二个数字: 5
|
||||
结果: 10 + 5 = 15
|
||||
```
|
||||
输入数学表达式,或输入 'quit' 退出,'history' 查看历史记录
|
||||
|
||||
> 2 + 3
|
||||
结果: 5
|
||||
|
||||
> 10 * (5 - 2)
|
||||
结果: 30
|
||||
|
||||
> 15 / 3
|
||||
结果: 5
|
||||
|
||||
> history
|
||||
历史记录:
|
||||
1. 2 + 3 = 5
|
||||
2. 10 * (5 - 2) = 30
|
||||
3. 15 / 3 = 5
|
||||
|
||||
> quit
|
||||
再见!
|
||||
```
|
||||
|
||||
## 学习要点
|
||||
|
||||
这个项目综合运用了以下 Go 语言特性:
|
||||
|
||||
1. **包管理**: 创建和使用自定义包
|
||||
2. **结构体和方法**: 定义计算器结构体和相关方法
|
||||
3. **接口**: 定义计算器接口,实现多态
|
||||
4. **错误处理**: 处理除零错误、语法错误等
|
||||
5. **字符串处理**: 解析和处理用户输入
|
||||
6. **切片操作**: 管理历史记录
|
||||
7. **控制流程**: 使用循环和条件语句
|
||||
8. **用户交互**: 命令行输入输出
|
||||
9. **测试**: 编写单元测试验证功能
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. 添加更多数学函数(sin, cos, sqrt 等)
|
||||
2. 支持变量定义和使用
|
||||
3. 添加配置文件支持
|
||||
4. 实现图形用户界面
|
||||
5. 添加科学计算功能
|
||||
6. 支持不同进制转换
|
||||
7. 添加单位换算功能
|
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
calculator.go - 计算器核心逻辑
|
||||
实现了基本的四则运算和括号运算功能
|
||||
*/
|
||||
|
||||
package calculator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Calculator 接口定义了计算器的基本功能
|
||||
type Calculator interface {
|
||||
Calculate(expression string) (float64, error)
|
||||
GetHistory() []HistoryRecord
|
||||
ClearHistory()
|
||||
}
|
||||
|
||||
// BasicCalculator 基本计算器实现
|
||||
type BasicCalculator struct {
|
||||
history []HistoryRecord
|
||||
}
|
||||
|
||||
// NewCalculator 创建新的计算器实例
|
||||
func NewCalculator() Calculator {
|
||||
return &BasicCalculator{
|
||||
history: make([]HistoryRecord, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate 计算数学表达式
|
||||
func (c *BasicCalculator) Calculate(expression string) (float64, error) {
|
||||
// 清理输入
|
||||
expr := strings.ReplaceAll(expression, " ", "")
|
||||
if expr == "" {
|
||||
return 0, fmt.Errorf("表达式不能为空")
|
||||
}
|
||||
|
||||
// 验证表达式
|
||||
if err := c.validateExpression(expr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 解析和计算
|
||||
result, err := c.evaluateExpression(expr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 添加到历史记录
|
||||
c.addToHistory(expression, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetHistory 获取计算历史
|
||||
func (c *BasicCalculator) GetHistory() []HistoryRecord {
|
||||
// 返回历史记录的副本
|
||||
history := make([]HistoryRecord, len(c.history))
|
||||
copy(history, c.history)
|
||||
return history
|
||||
}
|
||||
|
||||
// ClearHistory 清空历史记录
|
||||
func (c *BasicCalculator) ClearHistory() {
|
||||
c.history = make([]HistoryRecord, 0)
|
||||
}
|
||||
|
||||
// validateExpression 验证表达式的有效性
|
||||
func (c *BasicCalculator) validateExpression(expr string) error {
|
||||
if len(expr) == 0 {
|
||||
return fmt.Errorf("表达式不能为空")
|
||||
}
|
||||
|
||||
// 检查括号匹配
|
||||
parentheses := 0
|
||||
for _, char := range expr {
|
||||
switch char {
|
||||
case '(':
|
||||
parentheses++
|
||||
case ')':
|
||||
parentheses--
|
||||
if parentheses < 0 {
|
||||
return fmt.Errorf("括号不匹配")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if parentheses != 0 {
|
||||
return fmt.Errorf("括号不匹配")
|
||||
}
|
||||
|
||||
// 检查有效字符
|
||||
validChars := "0123456789+-*/.() "
|
||||
for _, char := range expr {
|
||||
if !strings.ContainsRune(validChars, char) {
|
||||
return fmt.Errorf("包含无效字符: %c", char)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查连续运算符
|
||||
operators := "+-*/"
|
||||
for i := 0; i < len(expr)-1; i++ {
|
||||
if strings.ContainsRune(operators, rune(expr[i])) &&
|
||||
strings.ContainsRune(operators, rune(expr[i+1])) {
|
||||
return fmt.Errorf("连续的运算符")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// evaluateExpression 计算表达式的值
|
||||
func (c *BasicCalculator) evaluateExpression(expr string) (float64, error) {
|
||||
// 处理括号
|
||||
for strings.Contains(expr, "(") {
|
||||
// 找到最内层的括号
|
||||
start := -1
|
||||
for i, char := range expr {
|
||||
if char == '(' {
|
||||
start = i
|
||||
} else if char == ')' {
|
||||
if start == -1 {
|
||||
return 0, fmt.Errorf("括号不匹配")
|
||||
}
|
||||
|
||||
// 计算括号内的表达式
|
||||
subExpr := expr[start+1 : i]
|
||||
subResult, err := c.evaluateSimpleExpression(subExpr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 替换括号表达式为结果
|
||||
expr = expr[:start] + fmt.Sprintf("%g", subResult) + expr[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算简单表达式(无括号)
|
||||
return c.evaluateSimpleExpression(expr)
|
||||
}
|
||||
|
||||
// evaluateSimpleExpression 计算简单表达式(无括号)
|
||||
func (c *BasicCalculator) evaluateSimpleExpression(expr string) (float64, error) {
|
||||
if expr == "" {
|
||||
return 0, fmt.Errorf("空表达式")
|
||||
}
|
||||
|
||||
// 解析表达式为标记
|
||||
tokens, err := c.tokenize(expr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return 0, fmt.Errorf("空表达式")
|
||||
}
|
||||
|
||||
// 如果只有一个标记,直接返回数值
|
||||
if len(tokens) == 1 {
|
||||
return strconv.ParseFloat(tokens[0], 64)
|
||||
}
|
||||
|
||||
// 先处理乘法和除法
|
||||
for i := 1; i < len(tokens); i += 2 {
|
||||
if i >= len(tokens) {
|
||||
break
|
||||
}
|
||||
|
||||
operator := tokens[i]
|
||||
if operator == "*" || operator == "/" {
|
||||
left, err := strconv.ParseFloat(tokens[i-1], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("无效的数字: %s", tokens[i-1])
|
||||
}
|
||||
|
||||
right, err := strconv.ParseFloat(tokens[i+1], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("无效的数字: %s", tokens[i+1])
|
||||
}
|
||||
|
||||
var result float64
|
||||
if operator == "*" {
|
||||
result = left * right
|
||||
} else {
|
||||
if right == 0 {
|
||||
return 0, fmt.Errorf("除零错误")
|
||||
}
|
||||
result = left / right
|
||||
}
|
||||
|
||||
// 替换三个标记为结果
|
||||
newTokens := make([]string, 0, len(tokens)-2)
|
||||
newTokens = append(newTokens, tokens[:i-1]...)
|
||||
newTokens = append(newTokens, fmt.Sprintf("%g", result))
|
||||
newTokens = append(newTokens, tokens[i+2:]...)
|
||||
tokens = newTokens
|
||||
i -= 2 // 调整索引
|
||||
}
|
||||
}
|
||||
|
||||
// 再处理加法和减法
|
||||
result, err := strconv.ParseFloat(tokens[0], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("无效的数字: %s", tokens[0])
|
||||
}
|
||||
|
||||
for i := 1; i < len(tokens); i += 2 {
|
||||
if i+1 >= len(tokens) {
|
||||
break
|
||||
}
|
||||
|
||||
operator := tokens[i]
|
||||
operand, err := strconv.ParseFloat(tokens[i+1], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("无效的数字: %s", tokens[i+1])
|
||||
}
|
||||
|
||||
switch operator {
|
||||
case "+":
|
||||
result += operand
|
||||
case "-":
|
||||
result -= operand
|
||||
default:
|
||||
return 0, fmt.Errorf("未知的运算符: %s", operator)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// tokenize 将表达式分解为标记
|
||||
func (c *BasicCalculator) tokenize(expr string) ([]string, error) {
|
||||
var tokens []string
|
||||
var current strings.Builder
|
||||
|
||||
for i, char := range expr {
|
||||
switch char {
|
||||
case '+', '-', '*', '/':
|
||||
// 处理负号
|
||||
if char == '-' && (i == 0 || expr[i-1] == '(' || strings.ContainsRune("+-*/", rune(expr[i-1]))) {
|
||||
current.WriteRune(char)
|
||||
continue
|
||||
}
|
||||
|
||||
// 保存当前数字
|
||||
if current.Len() > 0 {
|
||||
tokens = append(tokens, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
|
||||
// 保存运算符
|
||||
tokens = append(tokens, string(char))
|
||||
|
||||
case ' ':
|
||||
// 忽略空格
|
||||
continue
|
||||
|
||||
default:
|
||||
// 数字或小数点
|
||||
current.WriteRune(char)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后的数字
|
||||
if current.Len() > 0 {
|
||||
tokens = append(tokens, current.String())
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// addToHistory 添加计算记录到历史
|
||||
func (c *BasicCalculator) addToHistory(expression string, result float64) {
|
||||
record := HistoryRecord{
|
||||
Expression: expression,
|
||||
Result: result,
|
||||
}
|
||||
c.history = append(c.history, record)
|
||||
|
||||
// 限制历史记录数量
|
||||
const maxHistory = 100
|
||||
if len(c.history) > maxHistory {
|
||||
c.history = c.history[len(c.history)-maxHistory:]
|
||||
}
|
||||
}
|
244
golang-learning/10-projects/01-calculator/calculator/history.go
Normal file
244
golang-learning/10-projects/01-calculator/calculator/history.go
Normal file
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
history.go - 历史记录管理
|
||||
定义了历史记录的数据结构和相关功能
|
||||
*/
|
||||
|
||||
package calculator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HistoryRecord 表示一条计算历史记录
|
||||
type HistoryRecord struct {
|
||||
Expression string `json:"expression"` // 表达式
|
||||
Result float64 `json:"result"` // 计算结果
|
||||
Timestamp time.Time `json:"timestamp"` // 计算时间
|
||||
}
|
||||
|
||||
// NewHistoryRecord 创建新的历史记录
|
||||
func NewHistoryRecord(expression string, result float64) HistoryRecord {
|
||||
return HistoryRecord{
|
||||
Expression: expression,
|
||||
Result: result,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// String 返回历史记录的字符串表示
|
||||
func (h HistoryRecord) String() string {
|
||||
return fmt.Sprintf("%s = %g (计算时间: %s)",
|
||||
h.Expression,
|
||||
h.Result,
|
||||
h.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// HistoryManager 历史记录管理器
|
||||
type HistoryManager struct {
|
||||
records []HistoryRecord
|
||||
filePath string
|
||||
maxSize int
|
||||
}
|
||||
|
||||
// NewHistoryManager 创建新的历史记录管理器
|
||||
func NewHistoryManager(filePath string, maxSize int) *HistoryManager {
|
||||
return &HistoryManager{
|
||||
records: make([]HistoryRecord, 0),
|
||||
filePath: filePath,
|
||||
maxSize: maxSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加历史记录
|
||||
func (hm *HistoryManager) Add(expression string, result float64) {
|
||||
record := NewHistoryRecord(expression, result)
|
||||
hm.records = append(hm.records, record)
|
||||
|
||||
// 限制历史记录数量
|
||||
if len(hm.records) > hm.maxSize {
|
||||
hm.records = hm.records[len(hm.records)-hm.maxSize:]
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll 获取所有历史记录
|
||||
func (hm *HistoryManager) GetAll() []HistoryRecord {
|
||||
// 返回副本以防止外部修改
|
||||
records := make([]HistoryRecord, len(hm.records))
|
||||
copy(records, hm.records)
|
||||
return records
|
||||
}
|
||||
|
||||
// GetLast 获取最近的 n 条记录
|
||||
func (hm *HistoryManager) GetLast(n int) []HistoryRecord {
|
||||
if n <= 0 {
|
||||
return []HistoryRecord{}
|
||||
}
|
||||
|
||||
if n >= len(hm.records) {
|
||||
return hm.GetAll()
|
||||
}
|
||||
|
||||
start := len(hm.records) - n
|
||||
records := make([]HistoryRecord, n)
|
||||
copy(records, hm.records[start:])
|
||||
return records
|
||||
}
|
||||
|
||||
// Clear 清空历史记录
|
||||
func (hm *HistoryManager) Clear() {
|
||||
hm.records = make([]HistoryRecord, 0)
|
||||
}
|
||||
|
||||
// Count 获取历史记录数量
|
||||
func (hm *HistoryManager) Count() int {
|
||||
return len(hm.records)
|
||||
}
|
||||
|
||||
// SaveToFile 保存历史记录到文件
|
||||
func (hm *HistoryManager) SaveToFile() error {
|
||||
if hm.filePath == "" {
|
||||
return fmt.Errorf("文件路径未设置")
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(hm.records, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化历史记录失败: %v", err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(hm.filePath, data, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromFile 从文件加载历史记录
|
||||
func (hm *HistoryManager) LoadFromFile() error {
|
||||
if hm.filePath == "" {
|
||||
return fmt.Errorf("文件路径未设置")
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(hm.filePath); os.IsNotExist(err) {
|
||||
// 文件不存在,创建空的历史记录
|
||||
hm.records = make([]HistoryRecord, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(hm.filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取文件失败: %v", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &hm.records)
|
||||
if err != nil {
|
||||
return fmt.Errorf("反序列化历史记录失败: %v", err)
|
||||
}
|
||||
|
||||
// 限制历史记录数量
|
||||
if len(hm.records) > hm.maxSize {
|
||||
hm.records = hm.records[len(hm.records)-hm.maxSize:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search 搜索包含指定关键词的历史记录
|
||||
func (hm *HistoryManager) Search(keyword string) []HistoryRecord {
|
||||
var results []HistoryRecord
|
||||
|
||||
for _, record := range hm.records {
|
||||
if contains(record.Expression, keyword) {
|
||||
results = append(results, record)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// GetStatistics 获取历史记录统计信息
|
||||
func (hm *HistoryManager) GetStatistics() map[string]interface{} {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
stats["total_count"] = len(hm.records)
|
||||
|
||||
if len(hm.records) == 0 {
|
||||
return stats
|
||||
}
|
||||
|
||||
// 统计运算符使用频率
|
||||
operatorCount := make(map[string]int)
|
||||
for _, record := range hm.records {
|
||||
for _, char := range record.Expression {
|
||||
switch char {
|
||||
case '+':
|
||||
operatorCount["addition"]++
|
||||
case '-':
|
||||
operatorCount["subtraction"]++
|
||||
case '*':
|
||||
operatorCount["multiplication"]++
|
||||
case '/':
|
||||
operatorCount["division"]++
|
||||
}
|
||||
}
|
||||
}
|
||||
stats["operator_usage"] = operatorCount
|
||||
|
||||
// 最早和最晚的计算时间
|
||||
if len(hm.records) > 0 {
|
||||
earliest := hm.records[0].Timestamp
|
||||
latest := hm.records[0].Timestamp
|
||||
|
||||
for _, record := range hm.records {
|
||||
if record.Timestamp.Before(earliest) {
|
||||
earliest = record.Timestamp
|
||||
}
|
||||
if record.Timestamp.After(latest) {
|
||||
latest = record.Timestamp
|
||||
}
|
||||
}
|
||||
|
||||
stats["earliest_calculation"] = earliest.Format("2006-01-02 15:04:05")
|
||||
stats["latest_calculation"] = latest.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ExportToCSV 导出历史记录为 CSV 格式
|
||||
func (hm *HistoryManager) ExportToCSV(filePath string) error {
|
||||
var csvContent strings.Builder
|
||||
|
||||
// CSV 头部
|
||||
csvContent.WriteString("Expression,Result,Timestamp\n")
|
||||
|
||||
// 数据行
|
||||
for _, record := range hm.records {
|
||||
csvContent.WriteString(fmt.Sprintf("\"%s\",%g,\"%s\"\n",
|
||||
record.Expression,
|
||||
record.Result,
|
||||
record.Timestamp.Format("2006-01-02 15:04:05")))
|
||||
}
|
||||
|
||||
err := ioutil.WriteFile(filePath, []byte(csvContent.String()), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("导出 CSV 文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// contains 检查字符串是否包含子字符串(忽略大小写)
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) &&
|
||||
(substr == "" ||
|
||||
len(s) > 0 &&
|
||||
(s == substr ||
|
||||
strings.Contains(strings.ToLower(s), strings.ToLower(substr))))
|
||||
}
|
214
golang-learning/10-projects/01-calculator/calculator_test.go
Normal file
214
golang-learning/10-projects/01-calculator/calculator_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
calculator_test.go - 计算器测试文件
|
||||
测试计算器的各种功能
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"./calculator"
|
||||
)
|
||||
|
||||
// TestBasicOperations 测试基本运算
|
||||
func TestBasicOperations(t *testing.T) {
|
||||
calc := calculator.NewCalculator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
expected float64
|
||||
shouldErr bool
|
||||
}{
|
||||
{"加法", "2 + 3", 5, false},
|
||||
{"减法", "10 - 4", 6, false},
|
||||
{"乘法", "3 * 7", 21, false},
|
||||
{"除法", "15 / 3", 5, false},
|
||||
{"浮点数加法", "2.5 + 1.5", 4, false},
|
||||
{"浮点数除法", "7.5 / 2.5", 3, false},
|
||||
{"负数", "-5 + 3", -2, false},
|
||||
{"除零错误", "5 / 0", 0, true},
|
||||
{"空表达式", "", 0, true},
|
||||
{"无效字符", "2 + a", 0, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := calc.Calculate(tt.expression)
|
||||
|
||||
if tt.shouldErr {
|
||||
if err == nil {
|
||||
t.Errorf("期望出现错误,但没有错误")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("不期望出现错误,但出现了错误: %v", err)
|
||||
}
|
||||
if result != tt.expected {
|
||||
t.Errorf("期望结果 %g,实际结果 %g", tt.expected, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestComplexExpressions 测试复杂表达式
|
||||
func TestComplexExpressions(t *testing.T) {
|
||||
calc := calculator.NewCalculator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
expected float64
|
||||
}{
|
||||
{"括号运算", "(2 + 3) * 4", 20},
|
||||
{"嵌套括号", "((2 + 3) * 4) - 5", 15},
|
||||
{"运算优先级", "2 + 3 * 4", 14},
|
||||
{"复杂表达式", "10 + (5 - 2) * 3", 19},
|
||||
{"多层嵌套", "(2 + (3 * 4)) / 2", 7},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := calc.Calculate(tt.expression)
|
||||
if err != nil {
|
||||
t.Errorf("不期望出现错误: %v", err)
|
||||
}
|
||||
if result != tt.expected {
|
||||
t.Errorf("期望结果 %g,实际结果 %g", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHistory 测试历史记录功能
|
||||
func TestHistory(t *testing.T) {
|
||||
calc := calculator.NewCalculator()
|
||||
|
||||
// 执行一些计算
|
||||
expressions := []string{"2 + 3", "10 - 4", "3 * 7"}
|
||||
for _, expr := range expressions {
|
||||
_, err := calc.Calculate(expr)
|
||||
if err != nil {
|
||||
t.Errorf("计算 %s 时出现错误: %v", expr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查历史记录
|
||||
history := calc.GetHistory()
|
||||
if len(history) != len(expressions) {
|
||||
t.Errorf("期望历史记录数量 %d,实际数量 %d", len(expressions), len(history))
|
||||
}
|
||||
|
||||
// 验证历史记录内容
|
||||
for i, record := range history {
|
||||
if record.Expression != expressions[i] {
|
||||
t.Errorf("历史记录 %d 表达式不匹配,期望 %s,实际 %s",
|
||||
i, expressions[i], record.Expression)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试清空历史记录
|
||||
calc.ClearHistory()
|
||||
history = calc.GetHistory()
|
||||
if len(history) != 0 {
|
||||
t.Errorf("清空后期望历史记录数量 0,实际数量 %d", len(history))
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorHandling 测试错误处理
|
||||
func TestErrorHandling(t *testing.T) {
|
||||
calc := calculator.NewCalculator()
|
||||
|
||||
errorTests := []struct {
|
||||
name string
|
||||
expression string
|
||||
errorMsg string
|
||||
}{
|
||||
{"括号不匹配1", "(2 + 3", "括号不匹配"},
|
||||
{"括号不匹配2", "2 + 3)", "括号不匹配"},
|
||||
{"连续运算符", "2 ++ 3", "连续的运算符"},
|
||||
{"除零", "5 / 0", "除零错误"},
|
||||
{"无效字符", "2 + @", "包含无效字符"},
|
||||
{"空表达式", "", "表达式不能为空"},
|
||||
}
|
||||
|
||||
for _, tt := range errorTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := calc.Calculate(tt.expression)
|
||||
if err == nil {
|
||||
t.Errorf("期望出现错误,但没有错误")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkCalculate 基准测试
|
||||
func BenchmarkCalculate(b *testing.B) {
|
||||
calc := calculator.NewCalculator()
|
||||
expression := "(2 + 3) * 4 - 1"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := calc.Calculate(expression)
|
||||
if err != nil {
|
||||
b.Errorf("计算出现错误: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSimpleAddition 简单加法基准测试
|
||||
func BenchmarkSimpleAddition(b *testing.B) {
|
||||
calc := calculator.NewCalculator()
|
||||
expression := "2 + 3"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := calc.Calculate(expression)
|
||||
if err != nil {
|
||||
b.Errorf("计算出现错误: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkComplexExpression 复杂表达式基准测试
|
||||
func BenchmarkComplexExpression(b *testing.B) {
|
||||
calc := calculator.NewCalculator()
|
||||
expression := "((2 + 3) * 4 - 1) / (5 + 2)"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := calc.Calculate(expression)
|
||||
if err != nil {
|
||||
b.Errorf("计算出现错误: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExampleCalculator_Calculate 计算器使用示例
|
||||
func ExampleCalculator_Calculate() {
|
||||
calc := calculator.NewCalculator()
|
||||
|
||||
result, err := calc.Calculate("2 + 3")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%.0f", result)
|
||||
// Output: 5
|
||||
}
|
||||
|
||||
// ExampleCalculator_ComplexExpression 复杂表达式示例
|
||||
func ExampleCalculator_ComplexExpression() {
|
||||
calc := calculator.NewCalculator()
|
||||
|
||||
result, err := calc.Calculate("(2 + 3) * 4")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%.0f", result)
|
||||
// Output: 20
|
||||
}
|
133
golang-learning/10-projects/01-calculator/main.go
Normal file
133
golang-learning/10-projects/01-calculator/main.go
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
main.go - 计算器项目主程序
|
||||
这是一个简单的命令行计算器,演示了 Go 语言的综合应用
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"./calculator"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建计算器实例
|
||||
calc := calculator.NewCalculator()
|
||||
|
||||
// 创建输入扫描器
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
// 显示欢迎信息
|
||||
printWelcome()
|
||||
|
||||
// 主循环
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
|
||||
// 读取用户输入
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
|
||||
input := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// 处理特殊命令
|
||||
switch strings.ToLower(input) {
|
||||
case "quit", "exit", "q":
|
||||
fmt.Println("再见!")
|
||||
return
|
||||
case "help", "h":
|
||||
printHelp()
|
||||
continue
|
||||
case "history":
|
||||
printHistory(calc)
|
||||
continue
|
||||
case "clear":
|
||||
calc.ClearHistory()
|
||||
fmt.Println("历史记录已清空")
|
||||
continue
|
||||
case "":
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算表达式
|
||||
result, err := calc.Calculate(input)
|
||||
if err != nil {
|
||||
fmt.Printf("错误: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
fmt.Printf("结果: %g\n", result)
|
||||
}
|
||||
|
||||
// 检查扫描器错误
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Printf("读取输入时发生错误: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// printWelcome 显示欢迎信息
|
||||
func printWelcome() {
|
||||
fmt.Println("=== 欢迎使用 Go 计算器!===")
|
||||
fmt.Println()
|
||||
fmt.Println("支持的操作:")
|
||||
fmt.Println(" • 基本运算: +, -, *, /")
|
||||
fmt.Println(" • 括号运算: ( )")
|
||||
fmt.Println(" • 浮点数计算")
|
||||
fmt.Println()
|
||||
fmt.Println("特殊命令:")
|
||||
fmt.Println(" • help - 显示帮助信息")
|
||||
fmt.Println(" • history - 查看计算历史")
|
||||
fmt.Println(" • clear - 清空历史记录")
|
||||
fmt.Println(" • quit - 退出程序")
|
||||
fmt.Println()
|
||||
fmt.Println("请输入数学表达式:")
|
||||
}
|
||||
|
||||
// printHelp 显示帮助信息
|
||||
func printHelp() {
|
||||
fmt.Println()
|
||||
fmt.Println("=== 帮助信息 ===")
|
||||
fmt.Println()
|
||||
fmt.Println("支持的运算符:")
|
||||
fmt.Println(" + 加法 例: 2 + 3")
|
||||
fmt.Println(" - 减法 例: 5 - 2")
|
||||
fmt.Println(" * 乘法 例: 4 * 6")
|
||||
fmt.Println(" / 除法 例: 8 / 2")
|
||||
fmt.Println(" () 括号 例: (2 + 3) * 4")
|
||||
fmt.Println()
|
||||
fmt.Println("使用示例:")
|
||||
fmt.Println(" 2 + 3")
|
||||
fmt.Println(" 10 * (5 - 2)")
|
||||
fmt.Println(" 15.5 / 3.1")
|
||||
fmt.Println(" ((2 + 3) * 4) - 1")
|
||||
fmt.Println()
|
||||
fmt.Println("特殊命令:")
|
||||
fmt.Println(" help - 显示此帮助信息")
|
||||
fmt.Println(" history - 查看计算历史")
|
||||
fmt.Println(" clear - 清空历史记录")
|
||||
fmt.Println(" quit - 退出程序")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// printHistory 显示计算历史
|
||||
func printHistory(calc calculator.Calculator) {
|
||||
history := calc.GetHistory()
|
||||
|
||||
if len(history) == 0 {
|
||||
fmt.Println("暂无计算历史")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("=== 计算历史 ===")
|
||||
for i, record := range history {
|
||||
fmt.Printf("%d. %s = %g\n", i+1, record.Expression, record.Result)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
@@ -1,31 +1,117 @@
|
||||
# 待办事项列表项目
|
||||
|
||||
一个命令行待办事项管理程序,支持添加、删除、标记完成等功能。
|
||||
这是一个命令行待办事项管理程序,演示了 Go 语言在数据持久化、文件操作和用户交互方面的应用。
|
||||
|
||||
## 功能特性
|
||||
- 添加新的待办事项
|
||||
- 查看所有待办事项
|
||||
- 标记事项为完成
|
||||
- 删除待办事项
|
||||
- 数据持久化(保存到文件)
|
||||
## 项目特性
|
||||
|
||||
- 添加、删除、修改待办事项
|
||||
- 标记任务完成状态
|
||||
- 按优先级和状态筛选任务
|
||||
- 数据持久化到 JSON 文件
|
||||
- 彩色命令行界面
|
||||
- 任务搜索功能
|
||||
- 统计信息显示
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
02-todo-list/
|
||||
├── README.md # 项目说明文档
|
||||
├── main.go # 主程序入口
|
||||
├── todo/ # 待办事项核心包
|
||||
│ ├── todo.go # 待办事项数据结构
|
||||
│ ├── manager.go # 任务管理器
|
||||
│ └── storage.go # 数据存储
|
||||
├── ui/ # 用户界面包
|
||||
│ ├── cli.go # 命令行界面
|
||||
│ └── colors.go # 颜色输出
|
||||
├── data/ # 数据文件目录
|
||||
│ └── todos.json # 任务数据文件
|
||||
└── todo_test.go # 测试文件
|
||||
```
|
||||
|
||||
## 运行方法
|
||||
|
||||
```bash
|
||||
cd 02-todo-list
|
||||
# 进入项目目录
|
||||
cd 10-projects/02-todo-list
|
||||
|
||||
# 运行程序
|
||||
go run main.go
|
||||
|
||||
# 或者编译后运行
|
||||
go build -o todo main.go
|
||||
./todo
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
```bash
|
||||
# 添加待办事项
|
||||
go run main.go add "学习 Go 语言"
|
||||
|
||||
# 查看所有事项
|
||||
go run main.go list
|
||||
```
|
||||
=== 待办事项管理器 ===
|
||||
|
||||
# 标记完成
|
||||
go run main.go complete 1
|
||||
命令列表:
|
||||
add <任务> - 添加新任务
|
||||
list - 显示所有任务
|
||||
done <ID> - 标记任务完成
|
||||
delete <ID> - 删除任务
|
||||
edit <ID> <新内容> - 编辑任务
|
||||
search <关键词> - 搜索任务
|
||||
stats - 显示统计信息
|
||||
help - 显示帮助
|
||||
quit - 退出程序
|
||||
|
||||
# 删除事项
|
||||
go run main.go delete 1
|
||||
```
|
||||
> add 学习Go语言
|
||||
✅ 任务已添加: 学习Go语言 (ID: 1)
|
||||
|
||||
> add 完成项目文档
|
||||
✅ 任务已添加: 完成项目文档 (ID: 2)
|
||||
|
||||
> list
|
||||
📋 待办事项列表:
|
||||
[1] ⏳ 学习Go语言 (优先级: 中)
|
||||
[2] ⏳ 完成项目文档 (优先级: 中)
|
||||
|
||||
> done 1
|
||||
✅ 任务已完成: 学习Go语言
|
||||
|
||||
> list
|
||||
📋 待办事项列表:
|
||||
[1] ✅ 学习Go语言 (优先级: 中)
|
||||
[2] ⏳ 完成项目文档 (优先级: 中)
|
||||
|
||||
> stats
|
||||
📊 统计信息:
|
||||
总任务数: 2
|
||||
已完成: 1
|
||||
待完成: 1
|
||||
完成率: 50.0%
|
||||
```
|
||||
|
||||
## 学习要点
|
||||
|
||||
这个项目综合运用了以下 Go 语言特性:
|
||||
|
||||
1. **结构体和方法**: 定义任务和管理器结构体
|
||||
2. **接口设计**: 定义存储和界面接口
|
||||
3. **JSON 处理**: 数据序列化和反序列化
|
||||
4. **文件操作**: 读写 JSON 文件进行数据持久化
|
||||
5. **错误处理**: 完善的错误处理机制
|
||||
6. **字符串处理**: 命令解析和文本处理
|
||||
7. **切片操作**: 任务列表的增删改查
|
||||
8. **时间处理**: 任务创建和修改时间
|
||||
9. **包管理**: 多包项目结构
|
||||
10. **用户交互**: 命令行输入输出
|
||||
11. **测试**: 单元测试和集成测试
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. 添加任务分类和标签功能
|
||||
2. 支持任务截止日期和提醒
|
||||
3. 实现任务导入导出功能
|
||||
4. 添加任务优先级排序
|
||||
5. 支持批量操作
|
||||
6. 实现 Web 界面
|
||||
7. 添加任务统计图表
|
||||
8. 支持多用户管理
|
||||
9. 集成云端同步
|
||||
10. 添加任务模板功能
|
2
golang-learning/10-projects/02-todo-list/data/.gitkeep
Normal file
2
golang-learning/10-projects/02-todo-list/data/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# 这个文件用于保持 data 目录在 git 中被跟踪
|
||||
# 实际的 todos.json 文件会在程序运行时自动创建
|
275
golang-learning/10-projects/02-todo-list/main.go
Normal file
275
golang-learning/10-projects/02-todo-list/main.go
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
main.go - 待办事项列表主程序
|
||||
这是一个命令行待办事项管理程序,演示了 Go 语言的综合应用
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"./todo"
|
||||
"./ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建任务管理器
|
||||
manager, err := todo.NewManager("data/todos.json")
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 初始化失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 加载现有数据
|
||||
if err := manager.Load(); err != nil {
|
||||
fmt.Printf("⚠️ 加载数据失败: %v\n", err)
|
||||
}
|
||||
|
||||
// 创建命令行界面
|
||||
cli := ui.NewCLI()
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
// 显示欢迎信息
|
||||
cli.ShowWelcome()
|
||||
|
||||
// 主循环
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
|
||||
// 读取用户输入
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
|
||||
input := strings.TrimSpace(scanner.Text())
|
||||
if input == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析命令
|
||||
parts := strings.Fields(input)
|
||||
command := strings.ToLower(parts[0])
|
||||
|
||||
// 执行命令
|
||||
switch command {
|
||||
case "add":
|
||||
handleAdd(manager, cli, parts[1:])
|
||||
case "list", "ls":
|
||||
handleList(manager, cli, parts[1:])
|
||||
case "done", "complete":
|
||||
handleDone(manager, cli, parts[1:])
|
||||
case "delete", "del", "rm":
|
||||
handleDelete(manager, cli, parts[1:])
|
||||
case "edit", "update":
|
||||
handleEdit(manager, cli, parts[1:])
|
||||
case "search", "find":
|
||||
handleSearch(manager, cli, parts[1:])
|
||||
case "stats", "statistics":
|
||||
handleStats(manager, cli)
|
||||
case "help", "h":
|
||||
cli.ShowHelp()
|
||||
case "quit", "exit", "q":
|
||||
handleQuit(manager, cli)
|
||||
return
|
||||
case "clear":
|
||||
cli.Clear()
|
||||
default:
|
||||
cli.ShowError(fmt.Sprintf("未知命令: %s", command))
|
||||
cli.ShowInfo("输入 'help' 查看可用命令")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查扫描器错误
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Printf("❌ 读取输入时发生错误: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAdd 处理添加任务命令
|
||||
func handleAdd(manager *todo.Manager, cli *ui.CLI, args []string) {
|
||||
if len(args) == 0 {
|
||||
cli.ShowError("请提供任务内容")
|
||||
cli.ShowInfo("用法: add <任务内容>")
|
||||
return
|
||||
}
|
||||
|
||||
content := strings.Join(args, " ")
|
||||
task, err := manager.AddTask(content)
|
||||
if err != nil {
|
||||
cli.ShowError(fmt.Sprintf("添加任务失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
cli.ShowSuccess(fmt.Sprintf("任务已添加: %s (ID: %d)", task.Content, task.ID))
|
||||
|
||||
// 保存数据
|
||||
if err := manager.Save(); err != nil {
|
||||
cli.ShowError(fmt.Sprintf("保存数据失败: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// handleList 处理列出任务命令
|
||||
func handleList(manager *todo.Manager, cli *ui.CLI, args []string) {
|
||||
var filter string
|
||||
if len(args) > 0 {
|
||||
filter = strings.ToLower(args[0])
|
||||
}
|
||||
|
||||
tasks := manager.GetTasks()
|
||||
|
||||
// 应用过滤器
|
||||
var filteredTasks []todo.Task
|
||||
switch filter {
|
||||
case "done", "completed":
|
||||
for _, task := range tasks {
|
||||
if task.Completed {
|
||||
filteredTasks = append(filteredTasks, task)
|
||||
}
|
||||
}
|
||||
case "pending", "todo":
|
||||
for _, task := range tasks {
|
||||
if !task.Completed {
|
||||
filteredTasks = append(filteredTasks, task)
|
||||
}
|
||||
}
|
||||
default:
|
||||
filteredTasks = tasks
|
||||
}
|
||||
|
||||
if len(filteredTasks) == 0 {
|
||||
if filter != "" {
|
||||
cli.ShowInfo(fmt.Sprintf("没有找到 %s 状态的任务", filter))
|
||||
} else {
|
||||
cli.ShowInfo("暂无任务")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cli.ShowTaskList(filteredTasks)
|
||||
}
|
||||
|
||||
// handleDone 处理完成任务命令
|
||||
func handleDone(manager *todo.Manager, cli *ui.CLI, args []string) {
|
||||
if len(args) == 0 {
|
||||
cli.ShowError("请提供任务ID")
|
||||
cli.ShowInfo("用法: done <任务ID>")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
cli.ShowError("无效的任务ID")
|
||||
return
|
||||
}
|
||||
|
||||
task, err := manager.CompleteTask(id)
|
||||
if err != nil {
|
||||
cli.ShowError(fmt.Sprintf("完成任务失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
cli.ShowSuccess(fmt.Sprintf("任务已完成: %s", task.Content))
|
||||
|
||||
// 保存数据
|
||||
if err := manager.Save(); err != nil {
|
||||
cli.ShowError(fmt.Sprintf("保存数据失败: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete 处理删除任务命令
|
||||
func handleDelete(manager *todo.Manager, cli *ui.CLI, args []string) {
|
||||
if len(args) == 0 {
|
||||
cli.ShowError("请提供任务ID")
|
||||
cli.ShowInfo("用法: delete <任务ID>")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
cli.ShowError("无效的任务ID")
|
||||
return
|
||||
}
|
||||
|
||||
task, err := manager.DeleteTask(id)
|
||||
if err != nil {
|
||||
cli.ShowError(fmt.Sprintf("删除任务失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
cli.ShowSuccess(fmt.Sprintf("任务已删除: %s", task.Content))
|
||||
|
||||
// 保存数据
|
||||
if err := manager.Save(); err != nil {
|
||||
cli.ShowError(fmt.Sprintf("保存数据失败: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// handleEdit 处理编辑任务命令
|
||||
func handleEdit(manager *todo.Manager, cli *ui.CLI, args []string) {
|
||||
if len(args) < 2 {
|
||||
cli.ShowError("请提供任务ID和新内容")
|
||||
cli.ShowInfo("用法: edit <任务ID> <新内容>")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
cli.ShowError("无效的任务ID")
|
||||
return
|
||||
}
|
||||
|
||||
newContent := strings.Join(args[1:], " ")
|
||||
task, err := manager.UpdateTask(id, newContent)
|
||||
if err != nil {
|
||||
cli.ShowError(fmt.Sprintf("编辑任务失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
cli.ShowSuccess(fmt.Sprintf("任务已更新: %s", task.Content))
|
||||
|
||||
// 保存数据
|
||||
if err := manager.Save(); err != nil {
|
||||
cli.ShowError(fmt.Sprintf("保存数据失败: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// handleSearch 处理搜索任务命令
|
||||
func handleSearch(manager *todo.Manager, cli *ui.CLI, args []string) {
|
||||
if len(args) == 0 {
|
||||
cli.ShowError("请提供搜索关键词")
|
||||
cli.ShowInfo("用法: search <关键词>")
|
||||
return
|
||||
}
|
||||
|
||||
keyword := strings.Join(args, " ")
|
||||
tasks := manager.SearchTasks(keyword)
|
||||
|
||||
if len(tasks) == 0 {
|
||||
cli.ShowInfo(fmt.Sprintf("没有找到包含 '%s' 的任务", keyword))
|
||||
return
|
||||
}
|
||||
|
||||
cli.ShowInfo(fmt.Sprintf("搜索结果 (关键词: %s):", keyword))
|
||||
cli.ShowTaskList(tasks)
|
||||
}
|
||||
|
||||
// handleStats 处理统计信息命令
|
||||
func handleStats(manager *todo.Manager, cli *ui.CLI) {
|
||||
stats := manager.GetStatistics()
|
||||
cli.ShowStatistics(stats)
|
||||
}
|
||||
|
||||
// handleQuit 处理退出命令
|
||||
func handleQuit(manager *todo.Manager, cli *ui.CLI) {
|
||||
// 保存数据
|
||||
if err := manager.Save(); err != nil {
|
||||
cli.ShowError(fmt.Sprintf("保存数据失败: %v", err))
|
||||
}
|
||||
|
||||
cli.ShowInfo("数据已保存")
|
||||
cli.ShowSuccess("再见!")
|
||||
}
|
427
golang-learning/10-projects/02-todo-list/todo/manager.go
Normal file
427
golang-learning/10-projects/02-todo-list/todo/manager.go
Normal file
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
manager.go - 任务管理器
|
||||
实现了任务的增删改查和业务逻辑
|
||||
*/
|
||||
|
||||
package todo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Statistics 统计信息
|
||||
type Statistics struct {
|
||||
Total int `json:"total"` // 总任务数
|
||||
Completed int `json:"completed"` // 已完成任务数
|
||||
Pending int `json:"pending"` // 待完成任务数
|
||||
CompletionRate float64 `json:"completion_rate"` // 完成率
|
||||
ByPriority map[string]int `json:"by_priority"` // 按优先级统计
|
||||
ByStatus map[string]int `json:"by_status"` // 按状态统计
|
||||
AverageCompletionTime time.Duration `json:"average_completion_time"` // 平均完成时间
|
||||
}
|
||||
|
||||
// Manager 任务管理器
|
||||
type Manager struct {
|
||||
tasks []Task
|
||||
nextID int
|
||||
storage Storage
|
||||
}
|
||||
|
||||
// NewManager 创建新的任务管理器
|
||||
func NewManager(dataFile string) (*Manager, error) {
|
||||
storage, err := NewJSONStorage(dataFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建存储失败: %v", err)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
tasks: make([]Task, 0),
|
||||
nextID: 1,
|
||||
storage: storage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Load 从存储加载任务
|
||||
func (m *Manager) Load() error {
|
||||
tasks, err := m.storage.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载任务失败: %v", err)
|
||||
}
|
||||
|
||||
m.tasks = tasks
|
||||
|
||||
// 更新下一个ID
|
||||
maxID := 0
|
||||
for _, task := range m.tasks {
|
||||
if task.ID > maxID {
|
||||
maxID = task.ID
|
||||
}
|
||||
}
|
||||
m.nextID = maxID + 1
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save 保存任务到存储
|
||||
func (m *Manager) Save() error {
|
||||
return m.storage.Save(m.tasks)
|
||||
}
|
||||
|
||||
// AddTask 添加新任务
|
||||
func (m *Manager) AddTask(content string) (*Task, error) {
|
||||
content = strings.TrimSpace(content)
|
||||
if content == "" {
|
||||
return nil, fmt.Errorf("任务内容不能为空")
|
||||
}
|
||||
|
||||
task := NewTask(m.nextID, content)
|
||||
m.tasks = append(m.tasks, *task)
|
||||
m.nextID++
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// GetTasks 获取所有任务
|
||||
func (m *Manager) GetTasks() []Task {
|
||||
// 返回副本以防止外部修改
|
||||
tasks := make([]Task, len(m.tasks))
|
||||
copy(tasks, m.tasks)
|
||||
|
||||
// 排序任务
|
||||
sort.Sort(TaskList(tasks))
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
// GetTask 根据ID获取任务
|
||||
func (m *Manager) GetTask(id int) (*Task, error) {
|
||||
for i, task := range m.tasks {
|
||||
if task.ID == id {
|
||||
return &m.tasks[i], nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("任务不存在: ID %d", id)
|
||||
}
|
||||
|
||||
// UpdateTask 更新任务内容
|
||||
func (m *Manager) UpdateTask(id int, content string) (*Task, error) {
|
||||
content = strings.TrimSpace(content)
|
||||
if content == "" {
|
||||
return nil, fmt.Errorf("任务内容不能为空")
|
||||
}
|
||||
|
||||
task, err := m.GetTask(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.Update(content)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// CompleteTask 标记任务完成
|
||||
func (m *Manager) CompleteTask(id int) (*Task, error) {
|
||||
task, err := m.GetTask(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if task.Completed {
|
||||
return nil, fmt.Errorf("任务已经完成")
|
||||
}
|
||||
|
||||
task.Complete()
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// UncompleteTask 标记任务未完成
|
||||
func (m *Manager) UncompleteTask(id int) (*Task, error) {
|
||||
task, err := m.GetTask(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !task.Completed {
|
||||
return nil, fmt.Errorf("任务尚未完成")
|
||||
}
|
||||
|
||||
task.Uncomplete()
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// DeleteTask 删除任务
|
||||
func (m *Manager) DeleteTask(id int) (*Task, error) {
|
||||
for i, task := range m.tasks {
|
||||
if task.ID == id {
|
||||
// 创建任务副本用于返回
|
||||
deletedTask := task
|
||||
|
||||
// 从切片中删除任务
|
||||
m.tasks = append(m.tasks[:i], m.tasks[i+1:]...)
|
||||
|
||||
return &deletedTask, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("任务不存在: ID %d", id)
|
||||
}
|
||||
|
||||
// SearchTasks 搜索任务
|
||||
func (m *Manager) SearchTasks(keyword string) []Task {
|
||||
keyword = strings.TrimSpace(keyword)
|
||||
if keyword == "" {
|
||||
return []Task{}
|
||||
}
|
||||
|
||||
var results []Task
|
||||
for _, task := range m.tasks {
|
||||
if m.matchesKeyword(task, keyword) {
|
||||
results = append(results, task)
|
||||
}
|
||||
}
|
||||
|
||||
// 排序结果
|
||||
sort.Sort(TaskList(results))
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// matchesKeyword 检查任务是否匹配关键词
|
||||
func (m *Manager) matchesKeyword(task Task, keyword string) bool {
|
||||
keyword = strings.ToLower(keyword)
|
||||
|
||||
// 检查任务内容
|
||||
if strings.Contains(strings.ToLower(task.Content), keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查任务描述
|
||||
if strings.Contains(strings.ToLower(task.Description), keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查标签
|
||||
for _, tag := range task.Tags {
|
||||
if strings.Contains(strings.ToLower(tag), keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTasksByStatus 根据状态获取任务
|
||||
func (m *Manager) GetTasksByStatus(completed bool) []Task {
|
||||
var results []Task
|
||||
for _, task := range m.tasks {
|
||||
if task.Completed == completed {
|
||||
results = append(results, task)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(TaskList(results))
|
||||
return results
|
||||
}
|
||||
|
||||
// GetTasksByPriority 根据优先级获取任务
|
||||
func (m *Manager) GetTasksByPriority(priority Priority) []Task {
|
||||
var results []Task
|
||||
for _, task := range m.tasks {
|
||||
if task.Priority == priority {
|
||||
results = append(results, task)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(TaskList(results))
|
||||
return results
|
||||
}
|
||||
|
||||
// SetTaskPriority 设置任务优先级
|
||||
func (m *Manager) SetTaskPriority(id int, priority Priority) (*Task, error) {
|
||||
task, err := m.GetTask(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.SetPriority(priority)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// AddTaskTag 为任务添加标签
|
||||
func (m *Manager) AddTaskTag(id int, tag string) (*Task, error) {
|
||||
task, err := m.GetTask(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.AddTag(tag)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// RemoveTaskTag 移除任务标签
|
||||
func (m *Manager) RemoveTaskTag(id int, tag string) (*Task, error) {
|
||||
task, err := m.GetTask(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.RemoveTag(tag)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// SetTaskDescription 设置任务描述
|
||||
func (m *Manager) SetTaskDescription(id int, description string) (*Task, error) {
|
||||
task, err := m.GetTask(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.SetDescription(description)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// GetStatistics 获取统计信息
|
||||
func (m *Manager) GetStatistics() Statistics {
|
||||
stats := Statistics{
|
||||
Total: len(m.tasks),
|
||||
Completed: 0,
|
||||
Pending: 0,
|
||||
ByPriority: make(map[string]int),
|
||||
ByStatus: make(map[string]int),
|
||||
}
|
||||
|
||||
var totalCompletionTime time.Duration
|
||||
completedCount := 0
|
||||
|
||||
for _, task := range m.tasks {
|
||||
// 统计完成状态
|
||||
if task.Completed {
|
||||
stats.Completed++
|
||||
if task.CompletedAt != nil {
|
||||
totalCompletionTime += task.CompletionTime()
|
||||
completedCount++
|
||||
}
|
||||
} else {
|
||||
stats.Pending++
|
||||
}
|
||||
|
||||
// 统计优先级
|
||||
priorityStr := task.Priority.String()
|
||||
stats.ByPriority[priorityStr]++
|
||||
|
||||
// 统计状态
|
||||
if task.Completed {
|
||||
stats.ByStatus["已完成"]++
|
||||
} else {
|
||||
stats.ByStatus["待完成"]++
|
||||
}
|
||||
}
|
||||
|
||||
// 计算完成率
|
||||
if stats.Total > 0 {
|
||||
stats.CompletionRate = float64(stats.Completed) / float64(stats.Total) * 100
|
||||
}
|
||||
|
||||
// 计算平均完成时间
|
||||
if completedCount > 0 {
|
||||
stats.AverageCompletionTime = totalCompletionTime / time.Duration(completedCount)
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ClearCompleted 清除所有已完成的任务
|
||||
func (m *Manager) ClearCompleted() int {
|
||||
var remaining []Task
|
||||
clearedCount := 0
|
||||
|
||||
for _, task := range m.tasks {
|
||||
if task.Completed {
|
||||
clearedCount++
|
||||
} else {
|
||||
remaining = append(remaining, task)
|
||||
}
|
||||
}
|
||||
|
||||
m.tasks = remaining
|
||||
return clearedCount
|
||||
}
|
||||
|
||||
// ClearAll 清除所有任务
|
||||
func (m *Manager) ClearAll() int {
|
||||
count := len(m.tasks)
|
||||
m.tasks = make([]Task, 0)
|
||||
m.nextID = 1
|
||||
return count
|
||||
}
|
||||
|
||||
// GetTaskCount 获取任务数量
|
||||
func (m *Manager) GetTaskCount() int {
|
||||
return len(m.tasks)
|
||||
}
|
||||
|
||||
// GetCompletedCount 获取已完成任务数量
|
||||
func (m *Manager) GetCompletedCount() int {
|
||||
count := 0
|
||||
for _, task := range m.tasks {
|
||||
if task.Completed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// GetPendingCount 获取待完成任务数量
|
||||
func (m *Manager) GetPendingCount() int {
|
||||
count := 0
|
||||
for _, task := range m.tasks {
|
||||
if !task.Completed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// ValidateAllTasks 验证所有任务的有效性
|
||||
func (m *Manager) ValidateAllTasks() []error {
|
||||
var errors []error
|
||||
for i, task := range m.tasks {
|
||||
if err := task.Validate(); err != nil {
|
||||
errors = append(errors, fmt.Errorf("任务 %d: %v", i, err))
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// ExportTasks 导出任务(可以扩展为不同格式)
|
||||
func (m *Manager) ExportTasks() []Task {
|
||||
tasks := make([]Task, len(m.tasks))
|
||||
copy(tasks, m.tasks)
|
||||
return tasks
|
||||
}
|
||||
|
||||
// ImportTasks 导入任务
|
||||
func (m *Manager) ImportTasks(tasks []Task) error {
|
||||
// 验证导入的任务
|
||||
for i, task := range tasks {
|
||||
if err := task.Validate(); err != nil {
|
||||
return fmt.Errorf("导入任务 %d 验证失败: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务列表
|
||||
m.tasks = append(m.tasks, tasks...)
|
||||
|
||||
// 更新下一个ID
|
||||
maxID := m.nextID - 1
|
||||
for _, task := range tasks {
|
||||
if task.ID > maxID {
|
||||
maxID = task.ID
|
||||
}
|
||||
}
|
||||
m.nextID = maxID + 1
|
||||
|
||||
return nil
|
||||
}
|
308
golang-learning/10-projects/02-todo-list/todo/storage.go
Normal file
308
golang-learning/10-projects/02-todo-list/todo/storage.go
Normal file
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
storage.go - 数据存储
|
||||
实现了任务数据的持久化存储
|
||||
*/
|
||||
|
||||
package todo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Storage 存储接口
|
||||
type Storage interface {
|
||||
Load() ([]Task, error)
|
||||
Save(tasks []Task) error
|
||||
}
|
||||
|
||||
// JSONStorage JSON文件存储实现
|
||||
type JSONStorage struct {
|
||||
filePath string
|
||||
}
|
||||
|
||||
// NewJSONStorage 创建新的JSON存储
|
||||
func NewJSONStorage(filePath string) (*JSONStorage, error) {
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("文件路径不能为空")
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建目录失败: %v", err)
|
||||
}
|
||||
|
||||
return &JSONStorage{
|
||||
filePath: filePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Load 从JSON文件加载任务
|
||||
func (s *JSONStorage) Load() ([]Task, error) {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(s.filePath); os.IsNotExist(err) {
|
||||
// 文件不存在,返回空任务列表
|
||||
return []Task{}, nil
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
data, err := ioutil.ReadFile(s.filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 如果文件为空,返回空任务列表
|
||||
if len(data) == 0 {
|
||||
return []Task{}, nil
|
||||
}
|
||||
|
||||
// 解析JSON数据
|
||||
var tasks []Task
|
||||
if err := json.Unmarshal(data, &tasks); err != nil {
|
||||
return nil, fmt.Errorf("解析JSON数据失败: %v", err)
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// Save 保存任务到JSON文件
|
||||
func (s *JSONStorage) Save(tasks []Task) error {
|
||||
// 序列化任务数据
|
||||
data, err := json.MarshalIndent(tasks, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化任务数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if err := ioutil.WriteFile(s.filePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("写入文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFilePath 获取文件路径
|
||||
func (s *JSONStorage) GetFilePath() string {
|
||||
return s.filePath
|
||||
}
|
||||
|
||||
// FileExists 检查文件是否存在
|
||||
func (s *JSONStorage) FileExists() bool {
|
||||
_, err := os.Stat(s.filePath)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// GetFileSize 获取文件大小
|
||||
func (s *JSONStorage) GetFileSize() (int64, error) {
|
||||
info, err := os.Stat(s.filePath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return info.Size(), nil
|
||||
}
|
||||
|
||||
// Backup 备份数据文件
|
||||
func (s *JSONStorage) Backup(backupPath string) error {
|
||||
if !s.FileExists() {
|
||||
return fmt.Errorf("源文件不存在")
|
||||
}
|
||||
|
||||
// 读取源文件
|
||||
data, err := ioutil.ReadFile(s.filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取源文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 确保备份目录存在
|
||||
dir := filepath.Dir(backupPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("创建备份目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 写入备份文件
|
||||
if err := ioutil.WriteFile(backupPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("写入备份文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore 从备份恢复数据
|
||||
func (s *JSONStorage) Restore(backupPath string) error {
|
||||
// 检查备份文件是否存在
|
||||
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("备份文件不存在")
|
||||
}
|
||||
|
||||
// 读取备份文件
|
||||
data, err := ioutil.ReadFile(backupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取备份文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证备份数据
|
||||
var tasks []Task
|
||||
if err := json.Unmarshal(data, &tasks); err != nil {
|
||||
return fmt.Errorf("备份数据格式无效: %v", err)
|
||||
}
|
||||
|
||||
// 写入主文件
|
||||
if err := ioutil.WriteFile(s.filePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("恢复数据失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear 清空数据文件
|
||||
func (s *JSONStorage) Clear() error {
|
||||
return s.Save([]Task{})
|
||||
}
|
||||
|
||||
// MemoryStorage 内存存储实现(用于测试)
|
||||
type MemoryStorage struct {
|
||||
tasks []Task
|
||||
}
|
||||
|
||||
// NewMemoryStorage 创建新的内存存储
|
||||
func NewMemoryStorage() *MemoryStorage {
|
||||
return &MemoryStorage{
|
||||
tasks: make([]Task, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Load 从内存加载任务
|
||||
func (s *MemoryStorage) Load() ([]Task, error) {
|
||||
// 返回任务副本
|
||||
tasks := make([]Task, len(s.tasks))
|
||||
copy(tasks, s.tasks)
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// Save 保存任务到内存
|
||||
func (s *MemoryStorage) Save(tasks []Task) error {
|
||||
// 保存任务副本
|
||||
s.tasks = make([]Task, len(tasks))
|
||||
copy(s.tasks, tasks)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear 清空内存中的任务
|
||||
func (s *MemoryStorage) Clear() {
|
||||
s.tasks = make([]Task, 0)
|
||||
}
|
||||
|
||||
// GetTasks 获取内存中的任务(用于测试)
|
||||
func (s *MemoryStorage) GetTasks() []Task {
|
||||
tasks := make([]Task, len(s.tasks))
|
||||
copy(tasks, s.tasks)
|
||||
return tasks
|
||||
}
|
||||
|
||||
// CSVStorage CSV文件存储实现(扩展功能)
|
||||
type CSVStorage struct {
|
||||
filePath string
|
||||
}
|
||||
|
||||
// NewCSVStorage 创建新的CSV存储
|
||||
func NewCSVStorage(filePath string) (*CSVStorage, error) {
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("文件路径不能为空")
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建目录失败: %v", err)
|
||||
}
|
||||
|
||||
return &CSVStorage{
|
||||
filePath: filePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Load 从CSV文件加载任务(简化实现)
|
||||
func (s *CSVStorage) Load() ([]Task, error) {
|
||||
// 这里可以实现CSV解析逻辑
|
||||
// 为了简化,暂时返回空列表
|
||||
return []Task{}, nil
|
||||
}
|
||||
|
||||
// Save 保存任务到CSV文件(简化实现)
|
||||
func (s *CSVStorage) Save(tasks []Task) error {
|
||||
// 这里可以实现CSV写入逻辑
|
||||
// 为了简化,暂时不做任何操作
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorageManager 存储管理器
|
||||
type StorageManager struct {
|
||||
storages map[string]Storage
|
||||
current string
|
||||
}
|
||||
|
||||
// NewStorageManager 创建新的存储管理器
|
||||
func NewStorageManager() *StorageManager {
|
||||
return &StorageManager{
|
||||
storages: make(map[string]Storage),
|
||||
}
|
||||
}
|
||||
|
||||
// AddStorage 添加存储实现
|
||||
func (sm *StorageManager) AddStorage(name string, storage Storage) {
|
||||
sm.storages[name] = storage
|
||||
}
|
||||
|
||||
// SetCurrent 设置当前使用的存储
|
||||
func (sm *StorageManager) SetCurrent(name string) error {
|
||||
if _, exists := sm.storages[name]; !exists {
|
||||
return fmt.Errorf("存储 '%s' 不存在", name)
|
||||
}
|
||||
sm.current = name
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrent 获取当前存储
|
||||
func (sm *StorageManager) GetCurrent() (Storage, error) {
|
||||
if sm.current == "" {
|
||||
return nil, fmt.Errorf("未设置当前存储")
|
||||
}
|
||||
|
||||
storage, exists := sm.storages[sm.current]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("当前存储 '%s' 不存在", sm.current)
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// ListStorages 列出所有可用的存储
|
||||
func (sm *StorageManager) ListStorages() []string {
|
||||
var names []string
|
||||
for name := range sm.storages {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Load 使用当前存储加载数据
|
||||
func (sm *StorageManager) Load() ([]Task, error) {
|
||||
storage, err := sm.GetCurrent()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storage.Load()
|
||||
}
|
||||
|
||||
// Save 使用当前存储保存数据
|
||||
func (sm *StorageManager) Save(tasks []Task) error {
|
||||
storage, err := sm.GetCurrent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return storage.Save(tasks)
|
||||
}
|
356
golang-learning/10-projects/02-todo-list/todo/todo.go
Normal file
356
golang-learning/10-projects/02-todo-list/todo/todo.go
Normal file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
todo.go - 待办事项数据结构
|
||||
定义了任务的基本数据结构和相关方法
|
||||
*/
|
||||
|
||||
package todo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Priority 任务优先级
|
||||
type Priority int
|
||||
|
||||
const (
|
||||
Low Priority = iota
|
||||
Medium
|
||||
High
|
||||
Urgent
|
||||
)
|
||||
|
||||
// String 返回优先级的字符串表示
|
||||
func (p Priority) String() string {
|
||||
switch p {
|
||||
case Low:
|
||||
return "低"
|
||||
case Medium:
|
||||
return "中"
|
||||
case High:
|
||||
return "高"
|
||||
case Urgent:
|
||||
return "紧急"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// Task 表示一个待办事项
|
||||
type Task struct {
|
||||
ID int `json:"id"` // 任务ID
|
||||
Content string `json:"content"` // 任务内容
|
||||
Completed bool `json:"completed"` // 是否完成
|
||||
Priority Priority `json:"priority"` // 优先级
|
||||
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"` // 完成时间
|
||||
Tags []string `json:"tags,omitempty"` // 标签
|
||||
Description string `json:"description,omitempty"` // 详细描述
|
||||
}
|
||||
|
||||
// NewTask 创建新的任务
|
||||
func NewTask(id int, content string) *Task {
|
||||
now := time.Now()
|
||||
return &Task{
|
||||
ID: id,
|
||||
Content: content,
|
||||
Completed: false,
|
||||
Priority: Medium,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Tags: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Complete 标记任务为完成
|
||||
func (t *Task) Complete() {
|
||||
if !t.Completed {
|
||||
t.Completed = true
|
||||
now := time.Now()
|
||||
t.CompletedAt = &now
|
||||
t.UpdatedAt = now
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomplete 标记任务为未完成
|
||||
func (t *Task) Uncomplete() {
|
||||
if t.Completed {
|
||||
t.Completed = false
|
||||
t.CompletedAt = nil
|
||||
t.UpdatedAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// Update 更新任务内容
|
||||
func (t *Task) Update(content string) {
|
||||
if content != "" && content != t.Content {
|
||||
t.Content = content
|
||||
t.UpdatedAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// SetPriority 设置任务优先级
|
||||
func (t *Task) SetPriority(priority Priority) {
|
||||
if priority != t.Priority {
|
||||
t.Priority = priority
|
||||
t.UpdatedAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// AddTag 添加标签
|
||||
func (t *Task) AddTag(tag string) {
|
||||
if tag == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查标签是否已存在
|
||||
for _, existingTag := range t.Tags {
|
||||
if existingTag == tag {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Tags = append(t.Tags, tag)
|
||||
t.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// RemoveTag 移除标签
|
||||
func (t *Task) RemoveTag(tag string) {
|
||||
for i, existingTag := range t.Tags {
|
||||
if existingTag == tag {
|
||||
t.Tags = append(t.Tags[:i], t.Tags[i+1:]...)
|
||||
t.UpdatedAt = time.Now()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HasTag 检查是否包含指定标签
|
||||
func (t *Task) HasTag(tag string) bool {
|
||||
for _, existingTag := range t.Tags {
|
||||
if existingTag == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDescription 设置任务描述
|
||||
func (t *Task) SetDescription(description string) {
|
||||
if description != t.Description {
|
||||
t.Description = description
|
||||
t.UpdatedAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// String 返回任务的字符串表示
|
||||
func (t *Task) String() string {
|
||||
status := "⏳"
|
||||
if t.Completed {
|
||||
status = "✅"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%d] %s %s (优先级: %s)",
|
||||
t.ID, status, t.Content, t.Priority)
|
||||
}
|
||||
|
||||
// DetailedString 返回任务的详细字符串表示
|
||||
func (t *Task) DetailedString() string {
|
||||
status := "待完成"
|
||||
if t.Completed {
|
||||
status = "已完成"
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("任务 #%d\n", t.ID)
|
||||
result += fmt.Sprintf("内容: %s\n", t.Content)
|
||||
result += fmt.Sprintf("状态: %s\n", status)
|
||||
result += fmt.Sprintf("优先级: %s\n", t.Priority)
|
||||
result += fmt.Sprintf("创建时间: %s\n", t.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
result += fmt.Sprintf("更新时间: %s\n", t.UpdatedAt.Format("2006-01-02 15:04:05"))
|
||||
|
||||
if t.Completed && t.CompletedAt != nil {
|
||||
result += fmt.Sprintf("完成时间: %s\n", t.CompletedAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
if t.Description != "" {
|
||||
result += fmt.Sprintf("描述: %s\n", t.Description)
|
||||
}
|
||||
|
||||
if len(t.Tags) > 0 {
|
||||
result += fmt.Sprintf("标签: %v\n", t.Tags)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// IsOverdue 检查任务是否过期(如果有截止日期的话)
|
||||
func (t *Task) IsOverdue() bool {
|
||||
// 这里可以扩展添加截止日期功能
|
||||
return false
|
||||
}
|
||||
|
||||
// Age 返回任务的存在时间
|
||||
func (t *Task) Age() time.Duration {
|
||||
return time.Since(t.CreatedAt)
|
||||
}
|
||||
|
||||
// CompletionTime 返回任务的完成耗时
|
||||
func (t *Task) CompletionTime() time.Duration {
|
||||
if !t.Completed || t.CompletedAt == nil {
|
||||
return 0
|
||||
}
|
||||
return t.CompletedAt.Sub(t.CreatedAt)
|
||||
}
|
||||
|
||||
// Clone 创建任务的副本
|
||||
func (t *Task) Clone() *Task {
|
||||
clone := *t
|
||||
|
||||
// 深拷贝切片
|
||||
if len(t.Tags) > 0 {
|
||||
clone.Tags = make([]string, len(t.Tags))
|
||||
copy(clone.Tags, t.Tags)
|
||||
}
|
||||
|
||||
// 深拷贝时间指针
|
||||
if t.CompletedAt != nil {
|
||||
completedAt := *t.CompletedAt
|
||||
clone.CompletedAt = &completedAt
|
||||
}
|
||||
|
||||
return &clone
|
||||
}
|
||||
|
||||
// Validate 验证任务数据的有效性
|
||||
func (t *Task) Validate() error {
|
||||
if t.ID <= 0 {
|
||||
return fmt.Errorf("任务ID必须大于0")
|
||||
}
|
||||
|
||||
if t.Content == "" {
|
||||
return fmt.Errorf("任务内容不能为空")
|
||||
}
|
||||
|
||||
if t.Priority < Low || t.Priority > Urgent {
|
||||
return fmt.Errorf("无效的优先级")
|
||||
}
|
||||
|
||||
if t.CreatedAt.IsZero() {
|
||||
return fmt.Errorf("创建时间不能为空")
|
||||
}
|
||||
|
||||
if t.UpdatedAt.IsZero() {
|
||||
return fmt.Errorf("更新时间不能为空")
|
||||
}
|
||||
|
||||
if t.Completed && t.CompletedAt == nil {
|
||||
return fmt.Errorf("已完成的任务必须有完成时间")
|
||||
}
|
||||
|
||||
if !t.Completed && t.CompletedAt != nil {
|
||||
return fmt.Errorf("未完成的任务不应该有完成时间")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaskList 任务列表类型
|
||||
type TaskList []Task
|
||||
|
||||
// Len 返回任务列表长度
|
||||
func (tl TaskList) Len() int {
|
||||
return len(tl)
|
||||
}
|
||||
|
||||
// Less 比较两个任务的顺序(用于排序)
|
||||
func (tl TaskList) Less(i, j int) bool {
|
||||
// 首先按完成状态排序(未完成的在前)
|
||||
if tl[i].Completed != tl[j].Completed {
|
||||
return !tl[i].Completed
|
||||
}
|
||||
|
||||
// 然后按优先级排序(高优先级在前)
|
||||
if tl[i].Priority != tl[j].Priority {
|
||||
return tl[i].Priority > tl[j].Priority
|
||||
}
|
||||
|
||||
// 最后按创建时间排序(新创建的在前)
|
||||
return tl[i].CreatedAt.After(tl[j].CreatedAt)
|
||||
}
|
||||
|
||||
// Swap 交换两个任务的位置
|
||||
func (tl TaskList) Swap(i, j int) {
|
||||
tl[i], tl[j] = tl[j], tl[i]
|
||||
}
|
||||
|
||||
// Filter 过滤任务列表
|
||||
func (tl TaskList) Filter(predicate func(Task) bool) TaskList {
|
||||
var filtered TaskList
|
||||
for _, task := range tl {
|
||||
if predicate(task) {
|
||||
filtered = append(filtered, task)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找任务
|
||||
func (tl TaskList) FindByID(id int) (*Task, bool) {
|
||||
for i, task := range tl {
|
||||
if task.ID == id {
|
||||
return &tl[i], true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetCompleted 获取已完成的任务
|
||||
func (tl TaskList) GetCompleted() TaskList {
|
||||
return tl.Filter(func(t Task) bool {
|
||||
return t.Completed
|
||||
})
|
||||
}
|
||||
|
||||
// GetPending 获取待完成的任务
|
||||
func (tl TaskList) GetPending() TaskList {
|
||||
return tl.Filter(func(t Task) bool {
|
||||
return !t.Completed
|
||||
})
|
||||
}
|
||||
|
||||
// GetByPriority 根据优先级获取任务
|
||||
func (tl TaskList) GetByPriority(priority Priority) TaskList {
|
||||
return tl.Filter(func(t Task) bool {
|
||||
return t.Priority == priority
|
||||
})
|
||||
}
|
||||
|
||||
// Search 搜索包含关键词的任务
|
||||
func (tl TaskList) Search(keyword string) TaskList {
|
||||
return tl.Filter(func(t Task) bool {
|
||||
return contains(t.Content, keyword) ||
|
||||
contains(t.Description, keyword) ||
|
||||
containsInTags(t.Tags, keyword)
|
||||
})
|
||||
}
|
||||
|
||||
// contains 检查字符串是否包含子字符串(忽略大小写)
|
||||
func contains(s, substr string) bool {
|
||||
if substr == "" {
|
||||
return true
|
||||
}
|
||||
return len(s) >= len(substr) &&
|
||||
strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
||||
}
|
||||
|
||||
// containsInTags 检查标签列表是否包含关键词
|
||||
func containsInTags(tags []string, keyword string) bool {
|
||||
for _, tag := range tags {
|
||||
if contains(tag, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
382
golang-learning/10-projects/02-todo-list/todo_test.go
Normal file
382
golang-learning/10-projects/02-todo-list/todo_test.go
Normal file
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
todo_test.go - 待办事项测试文件
|
||||
测试待办事项管理器的各种功能
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"./todo"
|
||||
)
|
||||
|
||||
// TestTaskCreation 测试任务创建
|
||||
func TestTaskCreation(t *testing.T) {
|
||||
task := todo.NewTask(1, "测试任务")
|
||||
|
||||
if task.ID != 1 {
|
||||
t.Errorf("期望任务ID为1,实际为%d", task.ID)
|
||||
}
|
||||
|
||||
if task.Content != "测试任务" {
|
||||
t.Errorf("期望任务内容为'测试任务',实际为'%s'", task.Content)
|
||||
}
|
||||
|
||||
if task.Completed {
|
||||
t.Error("新创建的任务不应该是已完成状态")
|
||||
}
|
||||
|
||||
if task.Priority != todo.Medium {
|
||||
t.Errorf("期望默认优先级为Medium,实际为%v", task.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskCompletion 测试任务完成
|
||||
func TestTaskCompletion(t *testing.T) {
|
||||
task := todo.NewTask(1, "测试任务")
|
||||
|
||||
// 完成任务
|
||||
task.Complete()
|
||||
|
||||
if !task.Completed {
|
||||
t.Error("任务应该是已完成状态")
|
||||
}
|
||||
|
||||
if task.CompletedAt == nil {
|
||||
t.Error("已完成的任务应该有完成时间")
|
||||
}
|
||||
|
||||
// 再次完成(应该没有变化)
|
||||
oldCompletedAt := *task.CompletedAt
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
task.Complete()
|
||||
|
||||
if !task.CompletedAt.Equal(oldCompletedAt) {
|
||||
t.Error("重复完成任务不应该改变完成时间")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskUpdate 测试任务更新
|
||||
func TestTaskUpdate(t *testing.T) {
|
||||
task := todo.NewTask(1, "原始内容")
|
||||
oldUpdatedAt := task.UpdatedAt
|
||||
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
task.Update("新内容")
|
||||
|
||||
if task.Content != "新内容" {
|
||||
t.Errorf("期望任务内容为'新内容',实际为'%s'", task.Content)
|
||||
}
|
||||
|
||||
if !task.UpdatedAt.After(oldUpdatedAt) {
|
||||
t.Error("更新任务后,更新时间应该改变")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskPriority 测试任务优先级
|
||||
func TestTaskPriority(t *testing.T) {
|
||||
task := todo.NewTask(1, "测试任务")
|
||||
|
||||
task.SetPriority(todo.High)
|
||||
|
||||
if task.Priority != todo.High {
|
||||
t.Errorf("期望优先级为High,实际为%v", task.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskTags 测试任务标签
|
||||
func TestTaskTags(t *testing.T) {
|
||||
task := todo.NewTask(1, "测试任务")
|
||||
|
||||
// 添加标签
|
||||
task.AddTag("工作")
|
||||
task.AddTag("重要")
|
||||
|
||||
if len(task.Tags) != 2 {
|
||||
t.Errorf("期望标签数量为2,实际为%d", len(task.Tags))
|
||||
}
|
||||
|
||||
if !task.HasTag("工作") {
|
||||
t.Error("任务应该包含'工作'标签")
|
||||
}
|
||||
|
||||
// 添加重复标签
|
||||
task.AddTag("工作")
|
||||
if len(task.Tags) != 2 {
|
||||
t.Error("添加重复标签不应该增加标签数量")
|
||||
}
|
||||
|
||||
// 移除标签
|
||||
task.RemoveTag("工作")
|
||||
if task.HasTag("工作") {
|
||||
t.Error("移除标签后,任务不应该包含该标签")
|
||||
}
|
||||
|
||||
if len(task.Tags) != 1 {
|
||||
t.Errorf("移除标签后,期望标签数量为1,实际为%d", len(task.Tags))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskValidation 测试任务验证
|
||||
func TestTaskValidation(t *testing.T) {
|
||||
// 有效任务
|
||||
validTask := todo.NewTask(1, "有效任务")
|
||||
if err := validTask.Validate(); err != nil {
|
||||
t.Errorf("有效任务验证失败: %v", err)
|
||||
}
|
||||
|
||||
// 无效ID
|
||||
invalidTask := &todo.Task{
|
||||
ID: 0,
|
||||
Content: "测试",
|
||||
}
|
||||
if err := invalidTask.Validate(); err == nil {
|
||||
t.Error("ID为0的任务应该验证失败")
|
||||
}
|
||||
|
||||
// 空内容
|
||||
invalidTask = &todo.Task{
|
||||
ID: 1,
|
||||
Content: "",
|
||||
}
|
||||
if err := invalidTask.Validate(); err == nil {
|
||||
t.Error("内容为空的任务应该验证失败")
|
||||
}
|
||||
}
|
||||
|
||||
// TestManager 测试任务管理器
|
||||
func TestManager(t *testing.T) {
|
||||
// 使用内存存储进行测试
|
||||
storage := todo.NewMemoryStorage()
|
||||
manager := &todo.Manager{}
|
||||
|
||||
// 这里需要修改Manager结构以支持注入存储
|
||||
// 为了简化测试,我们直接测试基本功能
|
||||
|
||||
// 创建临时管理器
|
||||
tempManager, err := todo.NewManager("test_todos.json")
|
||||
if err != nil {
|
||||
t.Fatalf("创建管理器失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
task1, err := tempManager.AddTask("任务1")
|
||||
if err != nil {
|
||||
t.Errorf("添加任务失败: %v", err)
|
||||
}
|
||||
|
||||
if task1.ID != 1 {
|
||||
t.Errorf("期望第一个任务ID为1,实际为%d", task1.ID)
|
||||
}
|
||||
|
||||
// 添加更多任务
|
||||
tempManager.AddTask("任务2")
|
||||
tempManager.AddTask("任务3")
|
||||
|
||||
// 获取所有任务
|
||||
tasks := tempManager.GetTasks()
|
||||
if len(tasks) != 3 {
|
||||
t.Errorf("期望任务数量为3,实际为%d", len(tasks))
|
||||
}
|
||||
|
||||
// 完成任务
|
||||
completedTask, err := tempManager.CompleteTask(1)
|
||||
if err != nil {
|
||||
t.Errorf("完成任务失败: %v", err)
|
||||
}
|
||||
|
||||
if !completedTask.Completed {
|
||||
t.Error("任务应该是已完成状态")
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
deletedTask, err := tempManager.DeleteTask(2)
|
||||
if err != nil {
|
||||
t.Errorf("删除任务失败: %v", err)
|
||||
}
|
||||
|
||||
if deletedTask.ID != 2 {
|
||||
t.Errorf("期望删除的任务ID为2,实际为%d", deletedTask.ID)
|
||||
}
|
||||
|
||||
// 检查剩余任务
|
||||
tasks = tempManager.GetTasks()
|
||||
if len(tasks) != 2 {
|
||||
t.Errorf("删除后期望任务数量为2,实际为%d", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
// TestManagerSearch 测试搜索功能
|
||||
func TestManagerSearch(t *testing.T) {
|
||||
tempManager, err := todo.NewManager("test_search.json")
|
||||
if err != nil {
|
||||
t.Fatalf("创建管理器失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加测试任务
|
||||
tempManager.AddTask("学习Go语言")
|
||||
tempManager.AddTask("完成项目文档")
|
||||
tempManager.AddTask("Go语言练习")
|
||||
|
||||
// 搜索包含"Go"的任务
|
||||
results := tempManager.SearchTasks("Go")
|
||||
if len(results) != 2 {
|
||||
t.Errorf("期望搜索结果数量为2,实际为%d", len(results))
|
||||
}
|
||||
|
||||
// 搜索不存在的关键词
|
||||
results = tempManager.SearchTasks("Python")
|
||||
if len(results) != 0 {
|
||||
t.Errorf("期望搜索结果数量为0,实际为%d", len(results))
|
||||
}
|
||||
|
||||
// 空关键词搜索
|
||||
results = tempManager.SearchTasks("")
|
||||
if len(results) != 0 {
|
||||
t.Errorf("空关键词搜索期望结果数量为0,实际为%d", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
// TestManagerStatistics 测试统计功能
|
||||
func TestManagerStatistics(t *testing.T) {
|
||||
tempManager, err := todo.NewManager("test_stats.json")
|
||||
if err != nil {
|
||||
t.Fatalf("创建管理器失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加测试任务
|
||||
tempManager.AddTask("任务1")
|
||||
tempManager.AddTask("任务2")
|
||||
tempManager.AddTask("任务3")
|
||||
|
||||
// 完成一个任务
|
||||
tempManager.CompleteTask(1)
|
||||
|
||||
// 获取统计信息
|
||||
stats := tempManager.GetStatistics()
|
||||
|
||||
if stats.Total != 3 {
|
||||
t.Errorf("期望总任务数为3,实际为%d", stats.Total)
|
||||
}
|
||||
|
||||
if stats.Completed != 1 {
|
||||
t.Errorf("期望已完成任务数为1,实际为%d", stats.Completed)
|
||||
}
|
||||
|
||||
if stats.Pending != 2 {
|
||||
t.Errorf("期望待完成任务数为2,实际为%d", stats.Pending)
|
||||
}
|
||||
|
||||
expectedRate := float64(1) / float64(3) * 100
|
||||
if stats.CompletionRate != expectedRate {
|
||||
t.Errorf("期望完成率为%.1f%%,实际为%.1f%%", expectedRate, stats.CompletionRate)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMemoryStorage 测试内存存储
|
||||
func TestMemoryStorage(t *testing.T) {
|
||||
storage := todo.NewMemoryStorage()
|
||||
|
||||
// 创建测试任务
|
||||
tasks := []todo.Task{
|
||||
*todo.NewTask(1, "任务1"),
|
||||
*todo.NewTask(2, "任务2"),
|
||||
}
|
||||
|
||||
// 保存任务
|
||||
err := storage.Save(tasks)
|
||||
if err != nil {
|
||||
t.Errorf("保存任务失败: %v", err)
|
||||
}
|
||||
|
||||
// 加载任务
|
||||
loadedTasks, err := storage.Load()
|
||||
if err != nil {
|
||||
t.Errorf("加载任务失败: %v", err)
|
||||
}
|
||||
|
||||
if len(loadedTasks) != 2 {
|
||||
t.Errorf("期望加载任务数量为2,实际为%d", len(loadedTasks))
|
||||
}
|
||||
|
||||
if loadedTasks[0].Content != "任务1" {
|
||||
t.Errorf("期望第一个任务内容为'任务1',实际为'%s'", loadedTasks[0].Content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPriorityString 测试优先级字符串表示
|
||||
func TestPriorityString(t *testing.T) {
|
||||
tests := []struct {
|
||||
priority todo.Priority
|
||||
expected string
|
||||
}{
|
||||
{todo.Low, "低"},
|
||||
{todo.Medium, "中"},
|
||||
{todo.High, "高"},
|
||||
{todo.Urgent, "紧急"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
if tt.priority.String() != tt.expected {
|
||||
t.Errorf("期望优先级字符串为'%s',实际为'%s'", tt.expected, tt.priority.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkTaskCreation 任务创建基准测试
|
||||
func BenchmarkTaskCreation(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
todo.NewTask(i, fmt.Sprintf("任务%d", i))
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkManagerAddTask 管理器添加任务基准测试
|
||||
func BenchmarkManagerAddTask(b *testing.B) {
|
||||
manager, err := todo.NewManager("bench_test.json")
|
||||
if err != nil {
|
||||
b.Fatalf("创建管理器失败: %v", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
manager.AddTask(fmt.Sprintf("基准测试任务%d", i))
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkManagerSearch 搜索基准测试
|
||||
func BenchmarkManagerSearch(b *testing.B) {
|
||||
manager, err := todo.NewManager("bench_search.json")
|
||||
if err != nil {
|
||||
b.Fatalf("创建管理器失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加测试数据
|
||||
for i := 0; i < 1000; i++ {
|
||||
manager.AddTask(fmt.Sprintf("测试任务%d", i))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
manager.SearchTasks("测试")
|
||||
}
|
||||
}
|
||||
|
||||
// ExampleTask_String 任务字符串表示示例
|
||||
func ExampleTask_String() {
|
||||
task := todo.NewTask(1, "学习Go语言")
|
||||
fmt.Println(task.String())
|
||||
// Output: [1] ⏳ 学习Go语言 (优先级: 中)
|
||||
}
|
||||
|
||||
// ExampleManager_AddTask 添加任务示例
|
||||
func ExampleManager_AddTask() {
|
||||
manager, _ := todo.NewManager("example.json")
|
||||
task, _ := manager.AddTask("完成项目文档")
|
||||
fmt.Printf("任务已添加: %s (ID: %d)", task.Content, task.ID)
|
||||
// Output: 任务已添加: 完成项目文档 (ID: 1)
|
||||
}
|
315
golang-learning/10-projects/02-todo-list/ui/cli.go
Normal file
315
golang-learning/10-projects/02-todo-list/ui/cli.go
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
cli.go - 命令行界面
|
||||
实现了彩色的命令行用户界面
|
||||
*/
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"../todo"
|
||||
)
|
||||
|
||||
// CLI 命令行界面
|
||||
type CLI struct {
|
||||
colors *Colors
|
||||
}
|
||||
|
||||
// NewCLI 创建新的命令行界面
|
||||
func NewCLI() *CLI {
|
||||
return &CLI{
|
||||
colors: NewColors(),
|
||||
}
|
||||
}
|
||||
|
||||
// ShowWelcome 显示欢迎信息
|
||||
func (c *CLI) ShowWelcome() {
|
||||
c.Clear()
|
||||
fmt.Println(c.colors.Cyan("=== 待办事项管理器 ==="))
|
||||
fmt.Println()
|
||||
fmt.Println("📝 一个简单而强大的待办事项管理工具")
|
||||
fmt.Println()
|
||||
c.ShowHelp()
|
||||
}
|
||||
|
||||
// ShowHelp 显示帮助信息
|
||||
func (c *CLI) ShowHelp() {
|
||||
fmt.Println(c.colors.Yellow("命令列表:"))
|
||||
fmt.Println(" " + c.colors.Green("add <任务>") + " - 添加新任务")
|
||||
fmt.Println(" " + c.colors.Green("list [状态]") + " - 显示任务列表 (all/done/pending)")
|
||||
fmt.Println(" " + c.colors.Green("done <ID>") + " - 标记任务完成")
|
||||
fmt.Println(" " + c.colors.Green("delete <ID>") + " - 删除任务")
|
||||
fmt.Println(" " + c.colors.Green("edit <ID> <新内容>") + " - 编辑任务")
|
||||
fmt.Println(" " + c.colors.Green("search <关键词>") + " - 搜索任务")
|
||||
fmt.Println(" " + c.colors.Green("stats") + " - 显示统计信息")
|
||||
fmt.Println(" " + c.colors.Green("clear") + " - 清屏")
|
||||
fmt.Println(" " + c.colors.Green("help") + " - 显示帮助")
|
||||
fmt.Println(" " + c.colors.Green("quit") + " - 退出程序")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// ShowTaskList 显示任务列表
|
||||
func (c *CLI) ShowTaskList(tasks []todo.Task) {
|
||||
if len(tasks) == 0 {
|
||||
c.ShowInfo("暂无任务")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(c.colors.Cyan("📋 待办事项列表:"))
|
||||
|
||||
for _, task := range tasks {
|
||||
c.showTask(task)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// showTask 显示单个任务
|
||||
func (c *CLI) showTask(task todo.Task) {
|
||||
var status, statusColor string
|
||||
if task.Completed {
|
||||
status = "✅"
|
||||
statusColor = "green"
|
||||
} else {
|
||||
status = "⏳"
|
||||
statusColor = "yellow"
|
||||
}
|
||||
|
||||
// 根据优先级选择颜色
|
||||
var priorityColor string
|
||||
switch task.Priority {
|
||||
case todo.Urgent:
|
||||
priorityColor = "red"
|
||||
case todo.High:
|
||||
priorityColor = "magenta"
|
||||
case todo.Medium:
|
||||
priorityColor = "blue"
|
||||
case todo.Low:
|
||||
priorityColor = "cyan"
|
||||
default:
|
||||
priorityColor = "white"
|
||||
}
|
||||
|
||||
// 格式化任务内容
|
||||
content := task.Content
|
||||
if len(content) > 50 {
|
||||
content = content[:47] + "..."
|
||||
}
|
||||
|
||||
// 显示任务
|
||||
fmt.Printf(" [%s] %s %s %s\n",
|
||||
c.colors.Blue(fmt.Sprintf("%d", task.ID)),
|
||||
c.colorize(status, statusColor),
|
||||
c.colorize(content, "white"),
|
||||
c.colorize(fmt.Sprintf("(优先级: %s)", task.Priority), priorityColor))
|
||||
|
||||
// 显示标签
|
||||
if len(task.Tags) > 0 {
|
||||
fmt.Printf(" %s %s\n",
|
||||
c.colors.Gray("标签:"),
|
||||
c.colors.Cyan(strings.Join(task.Tags, ", ")))
|
||||
}
|
||||
|
||||
// 显示描述
|
||||
if task.Description != "" {
|
||||
desc := task.Description
|
||||
if len(desc) > 60 {
|
||||
desc = desc[:57] + "..."
|
||||
}
|
||||
fmt.Printf(" %s %s\n",
|
||||
c.colors.Gray("描述:"),
|
||||
c.colors.White(desc))
|
||||
}
|
||||
}
|
||||
|
||||
// ShowStatistics 显示统计信息
|
||||
func (c *CLI) ShowStatistics(stats todo.Statistics) {
|
||||
fmt.Println(c.colors.Cyan("📊 统计信息:"))
|
||||
fmt.Printf(" 总任务数: %s\n", c.colors.Blue(fmt.Sprintf("%d", stats.Total)))
|
||||
fmt.Printf(" 已完成: %s\n", c.colors.Green(fmt.Sprintf("%d", stats.Completed)))
|
||||
fmt.Printf(" 待完成: %s\n", c.colors.Yellow(fmt.Sprintf("%d", stats.Pending)))
|
||||
fmt.Printf(" 完成率: %s\n", c.colors.Magenta(fmt.Sprintf("%.1f%%", stats.CompletionRate)))
|
||||
|
||||
if len(stats.ByPriority) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println(c.colors.Yellow("按优先级统计:"))
|
||||
for priority, count := range stats.ByPriority {
|
||||
var color string
|
||||
switch priority {
|
||||
case "紧急":
|
||||
color = "red"
|
||||
case "高":
|
||||
color = "magenta"
|
||||
case "中":
|
||||
color = "blue"
|
||||
case "低":
|
||||
color = "cyan"
|
||||
default:
|
||||
color = "white"
|
||||
}
|
||||
fmt.Printf(" %s: %s\n",
|
||||
c.colorize(priority, color),
|
||||
c.colors.White(fmt.Sprintf("%d", count)))
|
||||
}
|
||||
}
|
||||
|
||||
if stats.AverageCompletionTime > 0 {
|
||||
fmt.Printf("\n 平均完成时间: %s\n",
|
||||
c.colors.Cyan(stats.AverageCompletionTime.String()))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// ShowSuccess 显示成功消息
|
||||
func (c *CLI) ShowSuccess(message string) {
|
||||
fmt.Printf("%s %s\n", c.colors.Green("✅"), message)
|
||||
}
|
||||
|
||||
// ShowError 显示错误消息
|
||||
func (c *CLI) ShowError(message string) {
|
||||
fmt.Printf("%s %s\n", c.colors.Red("❌"), c.colors.Red(message))
|
||||
}
|
||||
|
||||
// ShowWarning 显示警告消息
|
||||
func (c *CLI) ShowWarning(message string) {
|
||||
fmt.Printf("%s %s\n", c.colors.Yellow("⚠️"), c.colors.Yellow(message))
|
||||
}
|
||||
|
||||
// ShowInfo 显示信息消息
|
||||
func (c *CLI) ShowInfo(message string) {
|
||||
fmt.Printf("%s %s\n", c.colors.Blue("ℹ️"), message)
|
||||
}
|
||||
|
||||
// Clear 清屏
|
||||
func (c *CLI) Clear() {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", "cls")
|
||||
default:
|
||||
cmd = exec.Command("clear")
|
||||
}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
}
|
||||
|
||||
// colorize 根据颜色名称着色文本
|
||||
func (c *CLI) colorize(text, color string) string {
|
||||
switch strings.ToLower(color) {
|
||||
case "red":
|
||||
return c.colors.Red(text)
|
||||
case "green":
|
||||
return c.colors.Green(text)
|
||||
case "yellow":
|
||||
return c.colors.Yellow(text)
|
||||
case "blue":
|
||||
return c.colors.Blue(text)
|
||||
case "magenta":
|
||||
return c.colors.Magenta(text)
|
||||
case "cyan":
|
||||
return c.colors.Cyan(text)
|
||||
case "white":
|
||||
return c.colors.White(text)
|
||||
case "gray":
|
||||
return c.colors.Gray(text)
|
||||
default:
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
// ShowTaskDetail 显示任务详细信息
|
||||
func (c *CLI) ShowTaskDetail(task todo.Task) {
|
||||
fmt.Println(c.colors.Cyan("📋 任务详情:"))
|
||||
fmt.Println(c.colors.Gray("─────────────────────────────────────"))
|
||||
|
||||
fmt.Printf("ID: %s\n", c.colors.Blue(fmt.Sprintf("%d", task.ID)))
|
||||
fmt.Printf("内容: %s\n", c.colors.White(task.Content))
|
||||
|
||||
if task.Completed {
|
||||
fmt.Printf("状态: %s\n", c.colors.Green("✅ 已完成"))
|
||||
} else {
|
||||
fmt.Printf("状态: %s\n", c.colors.Yellow("⏳ 待完成"))
|
||||
}
|
||||
|
||||
var priorityColor string
|
||||
switch task.Priority {
|
||||
case todo.Urgent:
|
||||
priorityColor = "red"
|
||||
case todo.High:
|
||||
priorityColor = "magenta"
|
||||
case todo.Medium:
|
||||
priorityColor = "blue"
|
||||
case todo.Low:
|
||||
priorityColor = "cyan"
|
||||
}
|
||||
fmt.Printf("优先级: %s\n", c.colorize(task.Priority.String(), priorityColor))
|
||||
|
||||
fmt.Printf("创建时间: %s\n", c.colors.Gray(task.CreatedAt.Format("2006-01-02 15:04:05")))
|
||||
fmt.Printf("更新时间: %s\n", c.colors.Gray(task.UpdatedAt.Format("2006-01-02 15:04:05")))
|
||||
|
||||
if task.Completed && task.CompletedAt != nil {
|
||||
fmt.Printf("完成时间: %s\n", c.colors.Green(task.CompletedAt.Format("2006-01-02 15:04:05")))
|
||||
fmt.Printf("完成耗时: %s\n", c.colors.Cyan(task.CompletionTime().String()))
|
||||
}
|
||||
|
||||
if task.Description != "" {
|
||||
fmt.Printf("描述: %s\n", c.colors.White(task.Description))
|
||||
}
|
||||
|
||||
if len(task.Tags) > 0 {
|
||||
fmt.Printf("标签: %s\n", c.colors.Cyan(strings.Join(task.Tags, ", ")))
|
||||
}
|
||||
|
||||
fmt.Println(c.colors.Gray("─────────────────────────────────────"))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// ShowProgress 显示进度条
|
||||
func (c *CLI) ShowProgress(current, total int, label string) {
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
percentage := float64(current) / float64(total) * 100
|
||||
barLength := 30
|
||||
filledLength := int(float64(barLength) * float64(current) / float64(total))
|
||||
|
||||
bar := strings.Repeat("█", filledLength) + strings.Repeat("░", barLength-filledLength)
|
||||
|
||||
fmt.Printf("\r%s [%s] %.1f%% (%d/%d)",
|
||||
label,
|
||||
c.colors.Green(bar),
|
||||
percentage,
|
||||
current,
|
||||
total)
|
||||
}
|
||||
|
||||
// ConfirmAction 确认操作
|
||||
func (c *CLI) ConfirmAction(message string) bool {
|
||||
fmt.Printf("%s %s (y/N): ", c.colors.Yellow("❓"), message)
|
||||
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
return response == "y" || response == "yes"
|
||||
}
|
||||
|
||||
// ShowBanner 显示横幅
|
||||
func (c *CLI) ShowBanner(text string) {
|
||||
length := len(text) + 4
|
||||
border := strings.Repeat("=", length)
|
||||
|
||||
fmt.Println(c.colors.Cyan(border))
|
||||
fmt.Printf("%s %s %s\n",
|
||||
c.colors.Cyan("="),
|
||||
c.colors.White(text),
|
||||
c.colors.Cyan("="))
|
||||
fmt.Println(c.colors.Cyan(border))
|
||||
fmt.Println()
|
||||
}
|
382
golang-learning/10-projects/02-todo-list/ui/colors.go
Normal file
382
golang-learning/10-projects/02-todo-list/ui/colors.go
Normal file
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
colors.go - 颜色输出
|
||||
实现了命令行的彩色文本输出
|
||||
*/
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Colors 颜色输出器
|
||||
type Colors struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// ANSI 颜色代码
|
||||
const (
|
||||
ColorReset = "\033[0m"
|
||||
ColorRed = "\033[31m"
|
||||
ColorGreen = "\033[32m"
|
||||
ColorYellow = "\033[33m"
|
||||
ColorBlue = "\033[34m"
|
||||
ColorMagenta = "\033[35m"
|
||||
ColorCyan = "\033[36m"
|
||||
ColorWhite = "\033[37m"
|
||||
ColorGray = "\033[90m"
|
||||
|
||||
// 背景色
|
||||
BgRed = "\033[41m"
|
||||
BgGreen = "\033[42m"
|
||||
BgYellow = "\033[43m"
|
||||
BgBlue = "\033[44m"
|
||||
BgMagenta = "\033[45m"
|
||||
BgCyan = "\033[46m"
|
||||
BgWhite = "\033[47m"
|
||||
|
||||
// 样式
|
||||
StyleBold = "\033[1m"
|
||||
StyleDim = "\033[2m"
|
||||
StyleItalic = "\033[3m"
|
||||
StyleUnderline = "\033[4m"
|
||||
StyleBlink = "\033[5m"
|
||||
StyleReverse = "\033[7m"
|
||||
StyleStrike = "\033[9m"
|
||||
)
|
||||
|
||||
// NewColors 创建新的颜色输出器
|
||||
func NewColors() *Colors {
|
||||
return &Colors{
|
||||
enabled: supportsColor(),
|
||||
}
|
||||
}
|
||||
|
||||
// supportsColor 检查终端是否支持颜色
|
||||
func supportsColor() bool {
|
||||
// Windows 命令提示符通常不支持 ANSI 颜色
|
||||
if runtime.GOOS == "windows" {
|
||||
// 检查是否在支持颜色的终端中运行
|
||||
term := os.Getenv("TERM")
|
||||
if term == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if os.Getenv("FORCE_COLOR") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否连接到终端
|
||||
if !isTerminal() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isTerminal 检查是否连接到终端
|
||||
func isTerminal() bool {
|
||||
// 简单检查:如果 stdout 是文件,可能不是终端
|
||||
stat, err := os.Stdout.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否是字符设备
|
||||
return (stat.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
// colorize 为文本添加颜色
|
||||
func (c *Colors) colorize(text, color string) string {
|
||||
if !c.enabled {
|
||||
return text
|
||||
}
|
||||
return color + text + ColorReset
|
||||
}
|
||||
|
||||
// Red 红色文本
|
||||
func (c *Colors) Red(text string) string {
|
||||
return c.colorize(text, ColorRed)
|
||||
}
|
||||
|
||||
// Green 绿色文本
|
||||
func (c *Colors) Green(text string) string {
|
||||
return c.colorize(text, ColorGreen)
|
||||
}
|
||||
|
||||
// Yellow 黄色文本
|
||||
func (c *Colors) Yellow(text string) string {
|
||||
return c.colorize(text, ColorYellow)
|
||||
}
|
||||
|
||||
// Blue 蓝色文本
|
||||
func (c *Colors) Blue(text string) string {
|
||||
return c.colorize(text, ColorBlue)
|
||||
}
|
||||
|
||||
// Magenta 洋红色文本
|
||||
func (c *Colors) Magenta(text string) string {
|
||||
return c.colorize(text, ColorMagenta)
|
||||
}
|
||||
|
||||
// Cyan 青色文本
|
||||
func (c *Colors) Cyan(text string) string {
|
||||
return c.colorize(text, ColorCyan)
|
||||
}
|
||||
|
||||
// White 白色文本
|
||||
func (c *Colors) White(text string) string {
|
||||
return c.colorize(text, ColorWhite)
|
||||
}
|
||||
|
||||
// Gray 灰色文本
|
||||
func (c *Colors) Gray(text string) string {
|
||||
return c.colorize(text, ColorGray)
|
||||
}
|
||||
|
||||
// Bold 粗体文本
|
||||
func (c *Colors) Bold(text string) string {
|
||||
return c.colorize(text, StyleBold)
|
||||
}
|
||||
|
||||
// Dim 暗淡文本
|
||||
func (c *Colors) Dim(text string) string {
|
||||
return c.colorize(text, StyleDim)
|
||||
}
|
||||
|
||||
// Italic 斜体文本
|
||||
func (c *Colors) Italic(text string) string {
|
||||
return c.colorize(text, StyleItalic)
|
||||
}
|
||||
|
||||
// Underline 下划线文本
|
||||
func (c *Colors) Underline(text string) string {
|
||||
return c.colorize(text, StyleUnderline)
|
||||
}
|
||||
|
||||
// Blink 闪烁文本
|
||||
func (c *Colors) Blink(text string) string {
|
||||
return c.colorize(text, StyleBlink)
|
||||
}
|
||||
|
||||
// Reverse 反色文本
|
||||
func (c *Colors) Reverse(text string) string {
|
||||
return c.colorize(text, StyleReverse)
|
||||
}
|
||||
|
||||
// Strike 删除线文本
|
||||
func (c *Colors) Strike(text string) string {
|
||||
return c.colorize(text, StyleStrike)
|
||||
}
|
||||
|
||||
// BgRed 红色背景
|
||||
func (c *Colors) BgRedText(text string) string {
|
||||
return c.colorize(text, BgRed)
|
||||
}
|
||||
|
||||
// BgGreen 绿色背景
|
||||
func (c *Colors) BgGreenText(text string) string {
|
||||
return c.colorize(text, BgGreen)
|
||||
}
|
||||
|
||||
// BgYellow 黄色背景
|
||||
func (c *Colors) BgYellowText(text string) string {
|
||||
return c.colorize(text, BgYellow)
|
||||
}
|
||||
|
||||
// BgBlue 蓝色背景
|
||||
func (c *Colors) BgBlueText(text string) string {
|
||||
return c.colorize(text, BgBlue)
|
||||
}
|
||||
|
||||
// BgMagenta 洋红色背景
|
||||
func (c *Colors) BgMagentaText(text string) string {
|
||||
return c.colorize(text, BgMagenta)
|
||||
}
|
||||
|
||||
// BgCyan 青色背景
|
||||
func (c *Colors) BgCyanText(text string) string {
|
||||
return c.colorize(text, BgCyan)
|
||||
}
|
||||
|
||||
// BgWhite 白色背景
|
||||
func (c *Colors) BgWhiteText(text string) string {
|
||||
return c.colorize(text, BgWhite)
|
||||
}
|
||||
|
||||
// Combine 组合多种样式
|
||||
func (c *Colors) Combine(text string, styles ...string) string {
|
||||
if !c.enabled {
|
||||
return text
|
||||
}
|
||||
|
||||
var combined string
|
||||
for _, style := range styles {
|
||||
combined += style
|
||||
}
|
||||
|
||||
return combined + text + ColorReset
|
||||
}
|
||||
|
||||
// Success 成功样式(绿色粗体)
|
||||
func (c *Colors) Success(text string) string {
|
||||
return c.Combine(text, ColorGreen, StyleBold)
|
||||
}
|
||||
|
||||
// Error 错误样式(红色粗体)
|
||||
func (c *Colors) Error(text string) string {
|
||||
return c.Combine(text, ColorRed, StyleBold)
|
||||
}
|
||||
|
||||
// Warning 警告样式(黄色粗体)
|
||||
func (c *Colors) Warning(text string) string {
|
||||
return c.Combine(text, ColorYellow, StyleBold)
|
||||
}
|
||||
|
||||
// Info 信息样式(蓝色)
|
||||
func (c *Colors) Info(text string) string {
|
||||
return c.Blue(text)
|
||||
}
|
||||
|
||||
// Highlight 高亮样式(青色粗体)
|
||||
func (c *Colors) Highlight(text string) string {
|
||||
return c.Combine(text, ColorCyan, StyleBold)
|
||||
}
|
||||
|
||||
// Muted 静音样式(灰色暗淡)
|
||||
func (c *Colors) Muted(text string) string {
|
||||
return c.Combine(text, ColorGray, StyleDim)
|
||||
}
|
||||
|
||||
// Enable 启用颜色输出
|
||||
func (c *Colors) Enable() {
|
||||
c.enabled = true
|
||||
}
|
||||
|
||||
// Disable 禁用颜色输出
|
||||
func (c *Colors) Disable() {
|
||||
c.enabled = false
|
||||
}
|
||||
|
||||
// IsEnabled 检查颜色输出是否启用
|
||||
func (c *Colors) IsEnabled() bool {
|
||||
return c.enabled
|
||||
}
|
||||
|
||||
// Printf 带颜色的格式化输出
|
||||
func (c *Colors) Printf(color, format string, args ...interface{}) {
|
||||
text := fmt.Sprintf(format, args...)
|
||||
switch color {
|
||||
case "red":
|
||||
fmt.Print(c.Red(text))
|
||||
case "green":
|
||||
fmt.Print(c.Green(text))
|
||||
case "yellow":
|
||||
fmt.Print(c.Yellow(text))
|
||||
case "blue":
|
||||
fmt.Print(c.Blue(text))
|
||||
case "magenta":
|
||||
fmt.Print(c.Magenta(text))
|
||||
case "cyan":
|
||||
fmt.Print(c.Cyan(text))
|
||||
case "white":
|
||||
fmt.Print(c.White(text))
|
||||
case "gray":
|
||||
fmt.Print(c.Gray(text))
|
||||
default:
|
||||
fmt.Print(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Println 带颜色的输出(带换行)
|
||||
func (c *Colors) Println(color, text string) {
|
||||
c.Printf(color, "%s\n", text)
|
||||
}
|
||||
|
||||
// Rainbow 彩虹文本(每个字符不同颜色)
|
||||
func (c *Colors) Rainbow(text string) string {
|
||||
if !c.enabled {
|
||||
return text
|
||||
}
|
||||
|
||||
colors := []string{ColorRed, ColorYellow, ColorGreen, ColorCyan, ColorBlue, ColorMagenta}
|
||||
var result string
|
||||
|
||||
for i, char := range text {
|
||||
color := colors[i%len(colors)]
|
||||
result += color + string(char) + ColorReset
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Gradient 渐变文本(从一种颜色到另一种颜色)
|
||||
func (c *Colors) Gradient(text, startColor, endColor string) string {
|
||||
if !c.enabled {
|
||||
return text
|
||||
}
|
||||
|
||||
// 简化实现:只在开头和结尾使用不同颜色
|
||||
if len(text) <= 2 {
|
||||
return c.colorize(text, startColor)
|
||||
}
|
||||
|
||||
mid := len(text) / 2
|
||||
return c.colorize(text[:mid], startColor) + c.colorize(text[mid:], endColor)
|
||||
}
|
||||
|
||||
// Table 表格样式输出
|
||||
func (c *Colors) Table(headers []string, rows [][]string) {
|
||||
// 计算列宽
|
||||
colWidths := make([]int, len(headers))
|
||||
for i, header := range headers {
|
||||
colWidths[i] = len(header)
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
for i, cell := range row {
|
||||
if i < len(colWidths) && len(cell) > colWidths[i] {
|
||||
colWidths[i] = len(cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 输出表头
|
||||
fmt.Print(c.Bold(c.Cyan("│")))
|
||||
for i, header := range headers {
|
||||
fmt.Printf(" %-*s ", colWidths[i], header)
|
||||
fmt.Print(c.Bold(c.Cyan("│")))
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 输出分隔线
|
||||
fmt.Print(c.Cyan("├"))
|
||||
for i, width := range colWidths {
|
||||
fmt.Print(c.Cyan(strings.Repeat("─", width+2)))
|
||||
if i < len(colWidths)-1 {
|
||||
fmt.Print(c.Cyan("┼"))
|
||||
}
|
||||
}
|
||||
fmt.Println(c.Cyan("┤"))
|
||||
|
||||
// 输出数据行
|
||||
for _, row := range rows {
|
||||
fmt.Print(c.Cyan("│"))
|
||||
for i, cell := range row {
|
||||
if i < len(colWidths) {
|
||||
fmt.Printf(" %-*s ", colWidths[i], cell)
|
||||
}
|
||||
fmt.Print(c.Cyan("│"))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
@@ -1,38 +1,165 @@
|
||||
# Web 服务器项目
|
||||
|
||||
一个简单的 HTTP Web 服务器,提供 RESTful API。
|
||||
这是一个简单的 HTTP Web 服务器项目,演示了 Go 语言在网络编程、并发处理和 RESTful API 开发方面的应用。
|
||||
|
||||
## 功能特性
|
||||
- HTTP 服务器
|
||||
- RESTful API 端点
|
||||
## 项目特性
|
||||
|
||||
- HTTP 服务器基础功能
|
||||
- RESTful API 设计
|
||||
- JSON 数据处理
|
||||
- 路由处理
|
||||
- 路由管理
|
||||
- 中间件支持
|
||||
- 静态文件服务
|
||||
- 并发请求处理
|
||||
- 错误处理和日志记录
|
||||
- 简单的用户管理系统
|
||||
|
||||
## API 端点
|
||||
- `GET /` - 首页
|
||||
- `GET /api/users` - 获取用户列表
|
||||
- `POST /api/users` - 创建新用户
|
||||
- `GET /api/users/{id}` - 获取特定用户
|
||||
- `PUT /api/users/{id}` - 更新用户
|
||||
- `DELETE /api/users/{id}` - 删除用户
|
||||
## 项目结构
|
||||
|
||||
## 运行方法
|
||||
```bash
|
||||
cd 03-web-server
|
||||
go run main.go
|
||||
```
|
||||
03-web-server/
|
||||
├── README.md # 项目说明文档
|
||||
├── main.go # 主程序入口
|
||||
├── server/ # 服务器核心包
|
||||
│ ├── server.go # HTTP 服务器
|
||||
│ ├── router.go # 路由管理
|
||||
│ ├── middleware.go # 中间件
|
||||
│ └── handlers.go # 请求处理器
|
||||
├── models/ # 数据模型
|
||||
│ └── user.go # 用户模型
|
||||
├── static/ # 静态文件
|
||||
│ ├── index.html # 首页
|
||||
│ ├── style.css # 样式文件
|
||||
│ └── script.js # JavaScript 文件
|
||||
├── data/ # 数据文件
|
||||
│ └── users.json # 用户数据
|
||||
└── server_test.go # 测试文件
|
||||
```
|
||||
|
||||
服务器将在 http://localhost:8080 启动
|
||||
## 运行方法
|
||||
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd 10-projects/03-web-server
|
||||
|
||||
# 运行程序
|
||||
go run main.go
|
||||
|
||||
# 或者编译后运行
|
||||
go build -o webserver main.go
|
||||
./webserver
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 用户管理 API
|
||||
|
||||
- `GET /api/users` - 获取所有用户
|
||||
- `GET /api/users/{id}` - 获取指定用户
|
||||
- `POST /api/users` - 创建新用户
|
||||
- `PUT /api/users/{id}` - 更新用户信息
|
||||
- `DELETE /api/users/{id}` - 删除用户
|
||||
|
||||
### 其他接口
|
||||
|
||||
- `GET /` - 首页
|
||||
- `GET /health` - 健康检查
|
||||
- `GET /api/stats` - 服务器统计信息
|
||||
- `GET /static/*` - 静态文件服务
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 启动服务器
|
||||
|
||||
```bash
|
||||
# 获取用户列表
|
||||
$ go run main.go
|
||||
🚀 服务器启动成功
|
||||
📍 地址: http://localhost:8080
|
||||
📊 健康检查: http://localhost:8080/health
|
||||
📚 API文档: http://localhost:8080/api
|
||||
```
|
||||
|
||||
### API 调用示例
|
||||
|
||||
```bash
|
||||
# 获取所有用户
|
||||
curl http://localhost:8080/api/users
|
||||
|
||||
# 创建新用户
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"张三","email":"zhangsan@example.com"}'
|
||||
```
|
||||
-d '{"name":"张三","email":"zhangsan@example.com","age":25}'
|
||||
|
||||
# 获取指定用户
|
||||
curl http://localhost:8080/api/users/1
|
||||
|
||||
# 更新用户信息
|
||||
curl -X PUT http://localhost:8080/api/users/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"张三","email":"zhangsan@gmail.com","age":26}'
|
||||
|
||||
# 删除用户
|
||||
curl -X DELETE http://localhost:8080/api/users/1
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# 服务器统计
|
||||
curl http://localhost:8080/api/stats
|
||||
```
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
// GET /api/users
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "张三",
|
||||
"email": "zhangsan@example.com",
|
||||
"age": 25,
|
||||
"created_at": "2024-01-01T10:00:00Z",
|
||||
"updated_at": "2024-01-01T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
|
||||
// GET /health
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2024-01-01T10:00:00Z",
|
||||
"uptime": "1h30m45s",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
## 学习要点
|
||||
|
||||
这个项目综合运用了以下 Go 语言特性:
|
||||
|
||||
1. **HTTP 服务器**: 使用 `net/http` 包创建 Web 服务器
|
||||
2. **路由管理**: 实现 RESTful 路由和参数解析
|
||||
3. **JSON 处理**: 请求和响应的 JSON 序列化/反序列化
|
||||
4. **中间件模式**: 日志记录、CORS、认证等中间件
|
||||
5. **并发处理**: 利用 goroutine 处理并发请求
|
||||
6. **错误处理**: HTTP 错误响应和日志记录
|
||||
7. **文件操作**: 静态文件服务和数据持久化
|
||||
8. **结构体和接口**: 数据模型和服务接口设计
|
||||
9. **包管理**: 多包项目结构和依赖管理
|
||||
10. **测试**: HTTP 服务器和 API 的测试
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. 添加用户认证和授权(JWT)
|
||||
2. 实现数据库集成(MySQL、PostgreSQL)
|
||||
3. 添加缓存支持(Redis)
|
||||
4. 实现 WebSocket 支持
|
||||
5. 添加 API 限流和熔断
|
||||
6. 集成 Swagger API 文档
|
||||
7. 添加配置文件支持
|
||||
8. 实现优雅关闭
|
||||
9. 添加监控和指标收集
|
||||
10. 支持 HTTPS 和 HTTP/2
|
2
golang-learning/10-projects/03-web-server/data/.gitkeep
Normal file
2
golang-learning/10-projects/03-web-server/data/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# 这个文件用于保持 data 目录在 git 中被跟踪
|
||||
# 实际的 users.json 文件会在程序运行时自动创建
|
5
golang-learning/10-projects/03-web-server/go.mod
Normal file
5
golang-learning/10-projects/03-web-server/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module webserver
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/gorilla/mux v1.8.0
|
54
golang-learning/10-projects/03-web-server/main.go
Normal file
54
golang-learning/10-projects/03-web-server/main.go
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
main.go - Web服务器主程序
|
||||
这是一个简单的HTTP Web服务器,演示了Go语言的网络编程应用
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"webserver/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建服务器实例
|
||||
srv := server.NewServer(":8080")
|
||||
|
||||
// 设置路由
|
||||
srv.SetupRoutes()
|
||||
|
||||
// 启动信息
|
||||
fmt.Println("🚀 Go Web服务器启动中...")
|
||||
fmt.Println("📍 地址: http://localhost:8080")
|
||||
fmt.Println("📊 健康检查: http://localhost:8080/health")
|
||||
fmt.Println("📚 API文档: http://localhost:8080/api")
|
||||
fmt.Println("按 Ctrl+C 停止服务器")
|
||||
fmt.Println()
|
||||
|
||||
// 启动服务器(在goroutine中)
|
||||
go func() {
|
||||
if err := srv.Start(); err != nil {
|
||||
log.Printf("❌ 服务器启动失败: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
fmt.Println("\n🛑 正在关闭服务器...")
|
||||
|
||||
// 优雅关闭服务器
|
||||
if err := srv.Shutdown(); err != nil {
|
||||
log.Printf("❌ 服务器关闭失败: %v", err)
|
||||
} else {
|
||||
fmt.Println("✅ 服务器已安全关闭")
|
||||
}
|
||||
}
|
293
golang-learning/10-projects/03-web-server/models/user.go
Normal file
293
golang-learning/10-projects/03-web-server/models/user.go
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
user.go - 用户数据模型
|
||||
定义了用户的数据结构和相关操作
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// User 用户结构体
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Age int `json:"age"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UserStorage 用户存储
|
||||
type UserStorage struct {
|
||||
users []User
|
||||
nextID int
|
||||
filePath string
|
||||
}
|
||||
|
||||
var storage *UserStorage
|
||||
|
||||
// init 初始化用户存储
|
||||
func init() {
|
||||
storage = &UserStorage{
|
||||
users: make([]User, 0),
|
||||
nextID: 1,
|
||||
filePath: "data/users.json",
|
||||
}
|
||||
|
||||
// 创建数据目录
|
||||
os.MkdirAll("data", 0755)
|
||||
|
||||
// 加载现有数据
|
||||
storage.load()
|
||||
}
|
||||
|
||||
// Validate 验证用户数据
|
||||
func (u *User) Validate() error {
|
||||
if strings.TrimSpace(u.Name) == "" {
|
||||
return fmt.Errorf("用户名不能为空")
|
||||
}
|
||||
|
||||
if len(u.Name) > 50 {
|
||||
return fmt.Errorf("用户名长度不能超过50个字符")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(u.Email) == "" {
|
||||
return fmt.Errorf("邮箱不能为空")
|
||||
}
|
||||
|
||||
if !isValidEmail(u.Email) {
|
||||
return fmt.Errorf("邮箱格式无效")
|
||||
}
|
||||
|
||||
if u.Age < 0 || u.Age > 150 {
|
||||
return fmt.Errorf("年龄必须在0-150之间")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidEmail 简单的邮箱格式验证
|
||||
func isValidEmail(email string) bool {
|
||||
return strings.Contains(email, "@") && strings.Contains(email, ".")
|
||||
}
|
||||
|
||||
// GetAllUsers 获取所有用户
|
||||
func GetAllUsers() ([]User, error) {
|
||||
return storage.users, nil
|
||||
}
|
||||
|
||||
// GetUserByID 根据ID获取用户
|
||||
func GetUserByID(id int) (*User, error) {
|
||||
for _, user := range storage.users {
|
||||
if user.ID == id {
|
||||
return &user, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// CreateUser 创建新用户
|
||||
func CreateUser(user User) (*User, error) {
|
||||
// 检查邮箱是否已存在
|
||||
for _, existingUser := range storage.users {
|
||||
if existingUser.Email == user.Email {
|
||||
return nil, fmt.Errorf("邮箱已存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
user.ID = storage.nextID
|
||||
user.CreatedAt = time.Now()
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
// 添加到存储
|
||||
storage.users = append(storage.users, user)
|
||||
storage.nextID++
|
||||
|
||||
// 保存到文件
|
||||
if err := storage.save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户信息
|
||||
func UpdateUser(user User) (*User, error) {
|
||||
// 查找用户
|
||||
for i, existingUser := range storage.users {
|
||||
if existingUser.ID == user.ID {
|
||||
// 检查邮箱是否被其他用户使用
|
||||
for _, otherUser := range storage.users {
|
||||
if otherUser.ID != user.ID && otherUser.Email == user.Email {
|
||||
return nil, fmt.Errorf("邮箱已被其他用户使用")
|
||||
}
|
||||
}
|
||||
|
||||
// 保留创建时间,更新其他信息
|
||||
user.CreatedAt = existingUser.CreatedAt
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
// 更新用户
|
||||
storage.users[i] = user
|
||||
|
||||
// 保存到文件
|
||||
if err := storage.save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func DeleteUser(id int) error {
|
||||
// 查找并删除用户
|
||||
for i, user := range storage.users {
|
||||
if user.ID == id {
|
||||
// 从切片中删除用户
|
||||
storage.users = append(storage.users[:i], storage.users[i+1:]...)
|
||||
|
||||
// 保存到文件
|
||||
return storage.save()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// load 从文件加载用户数据
|
||||
func (s *UserStorage) load() error {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(s.filePath); os.IsNotExist(err) {
|
||||
// 文件不存在,创建示例数据
|
||||
s.createSampleData()
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
data, err := ioutil.ReadFile(s.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析JSON
|
||||
if err := json.Unmarshal(data, &s.users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新下一个ID
|
||||
maxID := 0
|
||||
for _, user := range s.users {
|
||||
if user.ID > maxID {
|
||||
maxID = user.ID
|
||||
}
|
||||
}
|
||||
s.nextID = maxID + 1
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// save 保存用户数据到文件
|
||||
func (s *UserStorage) save() error {
|
||||
data, err := json.MarshalIndent(s.users, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(s.filePath, data, 0644)
|
||||
}
|
||||
|
||||
// createSampleData 创建示例数据
|
||||
func (s *UserStorage) createSampleData() {
|
||||
now := time.Now()
|
||||
|
||||
sampleUsers := []User{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "张三",
|
||||
Email: "zhangsan@example.com",
|
||||
Age: 25,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Name: "李四",
|
||||
Email: "lisi@example.com",
|
||||
Age: 30,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Name: "王五",
|
||||
Email: "wangwu@example.com",
|
||||
Age: 28,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
}
|
||||
|
||||
s.users = sampleUsers
|
||||
s.nextID = 4
|
||||
}
|
||||
|
||||
// GetUserCount 获取用户总数
|
||||
func GetUserCount() int {
|
||||
return len(storage.users)
|
||||
}
|
||||
|
||||
// SearchUsers 搜索用户
|
||||
func SearchUsers(keyword string) []User {
|
||||
var results []User
|
||||
keyword = strings.ToLower(keyword)
|
||||
|
||||
for _, user := range storage.users {
|
||||
if strings.Contains(strings.ToLower(user.Name), keyword) ||
|
||||
strings.Contains(strings.ToLower(user.Email), keyword) {
|
||||
results = append(results, user)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// GetUsersByAge 根据年龄范围获取用户
|
||||
func GetUsersByAge(minAge, maxAge int) []User {
|
||||
var results []User
|
||||
|
||||
for _, user := range storage.users {
|
||||
if user.Age >= minAge && user.Age <= maxAge {
|
||||
results = append(results, user)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// GetRecentUsers 获取最近创建的用户
|
||||
func GetRecentUsers(limit int) []User {
|
||||
if limit <= 0 || limit > len(storage.users) {
|
||||
limit = len(storage.users)
|
||||
}
|
||||
|
||||
// 简单实现:返回最后创建的用户
|
||||
// 实际应用中应该按创建时间排序
|
||||
start := len(storage.users) - limit
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
return storage.users[start:]
|
||||
}
|
369
golang-learning/10-projects/03-web-server/server/handlers.go
Normal file
369
golang-learning/10-projects/03-web-server/server/handlers.go
Normal file
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
handlers.go - 请求处理器
|
||||
实现了各种HTTP请求的处理逻辑
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"webserver/models"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Response 通用响应结构
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Count int `json:"count,omitempty"`
|
||||
}
|
||||
|
||||
// HealthResponse 健康检查响应
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Uptime string `json:"uptime"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// StatsResponse 统计信息响应
|
||||
type StatsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Uptime string `json:"uptime"`
|
||||
Version string `json:"version"`
|
||||
GoVersion string `json:"go_version"`
|
||||
NumCPU int `json:"num_cpu"`
|
||||
NumGoroutine int `json:"num_goroutine"`
|
||||
MemStats runtime.MemStats `json:"mem_stats"`
|
||||
}
|
||||
|
||||
var (
|
||||
startTime = time.Now()
|
||||
version = "1.0.0"
|
||||
)
|
||||
|
||||
// HomeHandler 首页处理器
|
||||
func HomeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
html := `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Go Web服务器</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }
|
||||
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; text-align: center; }
|
||||
.api-list { background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; }
|
||||
.api-item { margin: 10px 0; padding: 10px; background: white; border-left: 4px solid #007bff; }
|
||||
.method { font-weight: bold; color: #007bff; }
|
||||
.endpoint { font-family: monospace; background: #e9ecef; padding: 2px 6px; border-radius: 3px; }
|
||||
.description { color: #666; margin-top: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 Go Web服务器</h1>
|
||||
<p>欢迎使用Go语言编写的简单Web服务器!这个项目演示了HTTP服务器、RESTful API、JSON处理等功能。</p>
|
||||
|
||||
<h2>📚 API接口</h2>
|
||||
<div class="api-list">
|
||||
<div class="api-item">
|
||||
<span class="method">GET</span> <span class="endpoint">/health</span>
|
||||
<div class="description">健康检查</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">GET</span> <span class="endpoint">/api/stats</span>
|
||||
<div class="description">服务器统计信息</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">GET</span> <span class="endpoint">/api/users</span>
|
||||
<div class="description">获取所有用户</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">POST</span> <span class="endpoint">/api/users</span>
|
||||
<div class="description">创建新用户</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">GET</span> <span class="endpoint">/api/users/{id}</span>
|
||||
<div class="description">获取指定用户</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">PUT</span> <span class="endpoint">/api/users/{id}</span>
|
||||
<div class="description">更新用户信息</div>
|
||||
</div>
|
||||
<div class="api-item">
|
||||
<span class="method">DELETE</span> <span class="endpoint">/api/users/{id}</span>
|
||||
<div class="description">删除用户</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>🛠️ 使用示例</h2>
|
||||
<pre style="background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto;">
|
||||
# 获取所有用户
|
||||
curl http://localhost:8080/api/users
|
||||
|
||||
# 创建新用户
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"张三","email":"zhangsan@example.com","age":25}'
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:8080/health
|
||||
</pre>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
// HealthHandler 健康检查处理器
|
||||
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uptime := time.Since(startTime)
|
||||
|
||||
response := HealthResponse{
|
||||
Status: "healthy",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Uptime: uptime.String(),
|
||||
Version: version,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// StatsHandler 统计信息处理器
|
||||
func StatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uptime := time.Since(startTime)
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
response := StatsResponse{
|
||||
Status: "success",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Uptime: uptime.String(),
|
||||
Version: version,
|
||||
GoVersion: runtime.Version(),
|
||||
NumCPU: runtime.NumCPU(),
|
||||
NumGoroutine: runtime.NumGoroutine(),
|
||||
MemStats: memStats,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GetUsersHandler 获取所有用户
|
||||
func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := models.GetAllUsers()
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "获取用户列表失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Data: users,
|
||||
Count: len(users),
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusOK)
|
||||
}
|
||||
|
||||
// GetUserHandler 获取指定用户
|
||||
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "无效的用户ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := models.GetUserByID(id)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "用户不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Data: user,
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusOK)
|
||||
}
|
||||
|
||||
// CreateUserHandler 创建新用户
|
||||
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var user models.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
sendErrorResponse(w, "无效的JSON数据", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户数据
|
||||
if err := user.Validate(); err != nil {
|
||||
sendErrorResponse(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
createdUser, err := models.CreateUser(user)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "创建用户失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Message: "用户创建成功",
|
||||
Data: createdUser,
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateUserHandler 更新用户信息
|
||||
func UpdateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "无效的用户ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
sendErrorResponse(w, "无效的JSON数据", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置用户ID
|
||||
user.ID = id
|
||||
|
||||
// 验证用户数据
|
||||
if err := user.Validate(); err != nil {
|
||||
sendErrorResponse(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
updatedUser, err := models.UpdateUser(user)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "更新用户失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Message: "用户更新成功",
|
||||
Data: updatedUser,
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteUserHandler 删除用户
|
||||
func DeleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "无效的用户ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DeleteUser(id); err != nil {
|
||||
sendErrorResponse(w, "删除用户失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := Response{
|
||||
Status: "success",
|
||||
Message: "用户删除成功",
|
||||
}
|
||||
|
||||
sendJSONResponse(w, response, http.StatusOK)
|
||||
}
|
||||
|
||||
// APIDocHandler API文档处理器
|
||||
func APIDocHandler(w http.ResponseWriter, r *http.Request) {
|
||||
doc := map[string]interface{}{
|
||||
"title": "Go Web服务器 API",
|
||||
"version": version,
|
||||
"description": "一个简单的RESTful API服务器",
|
||||
"endpoints": map[string]interface{}{
|
||||
"health": map[string]string{
|
||||
"method": "GET",
|
||||
"path": "/health",
|
||||
"description": "健康检查",
|
||||
},
|
||||
"stats": map[string]string{
|
||||
"method": "GET",
|
||||
"path": "/api/stats",
|
||||
"description": "服务器统计信息",
|
||||
},
|
||||
"users": map[string]interface{}{
|
||||
"list": map[string]string{
|
||||
"method": "GET",
|
||||
"path": "/api/users",
|
||||
"description": "获取所有用户",
|
||||
},
|
||||
"get": map[string]string{
|
||||
"method": "GET",
|
||||
"path": "/api/users/{id}",
|
||||
"description": "获取指定用户",
|
||||
},
|
||||
"create": map[string]string{
|
||||
"method": "POST",
|
||||
"path": "/api/users",
|
||||
"description": "创建新用户",
|
||||
},
|
||||
"update": map[string]string{
|
||||
"method": "PUT",
|
||||
"path": "/api/users/{id}",
|
||||
"description": "更新用户信息",
|
||||
},
|
||||
"delete": map[string]string{
|
||||
"method": "DELETE",
|
||||
"path": "/api/users/{id}",
|
||||
"description": "删除用户",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sendJSONResponse(w, doc, http.StatusOK)
|
||||
}
|
||||
|
||||
// sendJSONResponse 发送JSON响应
|
||||
func sendJSONResponse(w http.ResponseWriter, data interface{}, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
http.Error(w, "JSON编码失败", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// sendErrorResponse 发送错误响应
|
||||
func sendErrorResponse(w http.ResponseWriter, message string, statusCode int) {
|
||||
response := Response{
|
||||
Status: "error",
|
||||
Error: message,
|
||||
}
|
||||
sendJSONResponse(w, response, statusCode)
|
||||
}
|
147
golang-learning/10-projects/03-web-server/server/middleware.go
Normal file
147
golang-learning/10-projects/03-web-server/server/middleware.go
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
middleware.go - 中间件
|
||||
实现了各种HTTP中间件功能
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoggingMiddleware 日志记录中间件
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// 创建响应记录器来捕获状态码
|
||||
recorder := &responseRecorder{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(recorder, r)
|
||||
|
||||
// 记录请求日志
|
||||
duration := time.Since(start)
|
||||
log.Printf("[%s] %s %s %d %v",
|
||||
r.Method,
|
||||
r.RequestURI,
|
||||
r.RemoteAddr,
|
||||
recorder.statusCode,
|
||||
duration,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// responseRecorder 响应记录器
|
||||
type responseRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// WriteHeader 记录状态码
|
||||
func (rr *responseRecorder) WriteHeader(code int) {
|
||||
rr.statusCode = code
|
||||
rr.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// CORSMiddleware CORS中间件
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 设置CORS头
|
||||
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")
|
||||
|
||||
// 处理预检请求
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RecoveryMiddleware 恢复中间件(处理panic)
|
||||
func RecoveryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 记录panic信息
|
||||
log.Printf("❌ Panic recovered: %v\n%s", err, debug.Stack())
|
||||
|
||||
// 返回500错误
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// AuthMiddleware 认证中间件(示例)
|
||||
func AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查Authorization头
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 这里可以添加实际的token验证逻辑
|
||||
// 为了演示,我们简单检查token是否为"Bearer valid-token"
|
||||
if token != "Bearer valid-token" {
|
||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RateLimitMiddleware 限流中间件(简化版)
|
||||
func RateLimitMiddleware(next http.Handler) http.Handler {
|
||||
// 这里可以实现基于IP的限流逻辑
|
||||
// 为了简化,我们只是一个示例框架
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 实际实现中,这里会检查请求频率
|
||||
// 如果超过限制,返回429状态码
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// ContentTypeMiddleware 内容类型中间件
|
||||
func ContentTypeMiddleware(contentType string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SecurityMiddleware 安全头中间件
|
||||
func SecurityMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 设置安全相关的HTTP头
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
59
golang-learning/10-projects/03-web-server/server/router.go
Normal file
59
golang-learning/10-projects/03-web-server/server/router.go
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
router.go - 路由管理
|
||||
实现了HTTP路由和中间件管理功能
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Router 路由器结构体
|
||||
type Router struct {
|
||||
*mux.Router
|
||||
}
|
||||
|
||||
// NewRouter 创建新的路由器
|
||||
func NewRouter() *Router {
|
||||
return &Router{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
}
|
||||
|
||||
// Use 添加中间件
|
||||
func (r *Router) Use(middleware func(http.Handler) http.Handler) {
|
||||
r.Router.Use(middleware)
|
||||
}
|
||||
|
||||
// Methods 设置HTTP方法(链式调用)
|
||||
func (r *Router) Methods(methods ...string) *mux.Route {
|
||||
return r.Router.Methods(methods...)
|
||||
}
|
||||
|
||||
// PathPrefix 路径前缀
|
||||
func (r *Router) PathPrefix(tpl string) *mux.Router {
|
||||
return r.Router.PathPrefix(tpl)
|
||||
}
|
||||
|
||||
// Subrouter 创建子路由
|
||||
func (r *Router) Subrouter() *mux.Router {
|
||||
return r.Router.NewRoute().Subrouter()
|
||||
}
|
||||
|
||||
// HandleFunc 处理函数路由
|
||||
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, *http.Request)) *mux.Route {
|
||||
return r.Router.HandleFunc(path, f)
|
||||
}
|
||||
|
||||
// Handle 处理器路由
|
||||
func (r *Router) Handle(path string, handler http.Handler) *mux.Route {
|
||||
return r.Router.Handle(path, handler)
|
||||
}
|
||||
|
||||
// ServeHTTP 实现http.Handler接口
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
r.Router.ServeHTTP(w, req)
|
||||
}
|
94
golang-learning/10-projects/03-web-server/server/server.go
Normal file
94
golang-learning/10-projects/03-web-server/server/server.go
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
server.go - HTTP服务器核心
|
||||
实现了HTTP服务器的基本功能和生命周期管理
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Server HTTP服务器结构体
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
router *Router
|
||||
addr string
|
||||
}
|
||||
|
||||
// NewServer 创建新的服务器实例
|
||||
func NewServer(addr string) *Server {
|
||||
router := NewRouter()
|
||||
|
||||
return &Server{
|
||||
httpServer: &http.Server{
|
||||
Addr: addr,
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
},
|
||||
router: router,
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupRoutes 设置路由
|
||||
func (s *Server) SetupRoutes() {
|
||||
// 添加中间件
|
||||
s.router.Use(LoggingMiddleware)
|
||||
s.router.Use(CORSMiddleware)
|
||||
s.router.Use(RecoveryMiddleware)
|
||||
|
||||
// 静态文件服务
|
||||
s.router.HandleFunc("/", HomeHandler).Methods("GET")
|
||||
s.router.PathPrefix("/static/").Handler(
|
||||
http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))),
|
||||
)
|
||||
|
||||
// 健康检查
|
||||
s.router.HandleFunc("/health", HealthHandler).Methods("GET")
|
||||
|
||||
// API路由
|
||||
apiRouter := s.router.PathPrefix("/api").Subrouter()
|
||||
|
||||
// 用户管理API
|
||||
apiRouter.HandleFunc("/users", GetUsersHandler).Methods("GET")
|
||||
apiRouter.HandleFunc("/users", CreateUserHandler).Methods("POST")
|
||||
apiRouter.HandleFunc("/users/{id:[0-9]+}", GetUserHandler).Methods("GET")
|
||||
apiRouter.HandleFunc("/users/{id:[0-9]+}", UpdateUserHandler).Methods("PUT")
|
||||
apiRouter.HandleFunc("/users/{id:[0-9]+}", DeleteUserHandler).Methods("DELETE")
|
||||
|
||||
// 服务器统计
|
||||
apiRouter.HandleFunc("/stats", StatsHandler).Methods("GET")
|
||||
|
||||
// API文档
|
||||
apiRouter.HandleFunc("", APIDocHandler).Methods("GET")
|
||||
}
|
||||
|
||||
// Start 启动服务器
|
||||
func (s *Server) Start() error {
|
||||
fmt.Printf("✅ 服务器启动成功,监听地址: %s\n", s.addr)
|
||||
return s.httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
// Shutdown 优雅关闭服务器
|
||||
func (s *Server) Shutdown() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// GetRouter 获取路由器
|
||||
func (s *Server) GetRouter() *Router {
|
||||
return s.router
|
||||
}
|
||||
|
||||
// GetHTTPServer 获取HTTP服务器
|
||||
func (s *Server) GetHTTPServer() *http.Server {
|
||||
return s.httpServer
|
||||
}
|
358
golang-learning/10-projects/03-web-server/server_test.go
Normal file
358
golang-learning/10-projects/03-web-server/server_test.go
Normal file
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
server_test.go - Web服务器测试文件
|
||||
测试HTTP服务器和API的各种功能
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"webserver/models"
|
||||
"webserver/server"
|
||||
)
|
||||
|
||||
// TestHealthHandler 测试健康检查处理器
|
||||
func TestHealthHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/health", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.HealthHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
// 检查响应内容类型
|
||||
expected := "application/json"
|
||||
if ct := rr.Header().Get("Content-Type"); ct != expected {
|
||||
t.Errorf("处理器返回错误内容类型: got %v want %v", ct, expected)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.HealthResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "healthy" {
|
||||
t.Errorf("期望状态为 'healthy', 实际为 '%s'", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetUsersHandler 测试获取用户列表处理器
|
||||
func TestGetUsersHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/users", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.GetUsersHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
// 检查响应内容类型
|
||||
expected := "application/json"
|
||||
if ct := rr.Header().Get("Content-Type"); ct != expected {
|
||||
t.Errorf("处理器返回错误内容类型: got %v want %v", ct, expected)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.Response
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "success" {
|
||||
t.Errorf("期望状态为 'success', 实际为 '%s'", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateUserHandler 测试创建用户处理器
|
||||
func TestCreateUserHandler(t *testing.T) {
|
||||
user := models.User{
|
||||
Name: "测试用户",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/users", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.CreateUserHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusCreated {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusCreated)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.Response
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "success" {
|
||||
t.Errorf("期望状态为 'success', 实际为 '%s'", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateUserHandlerInvalidData 测试创建用户处理器(无效数据)
|
||||
func TestCreateUserHandlerInvalidData(t *testing.T) {
|
||||
// 测试空用户名
|
||||
user := models.User{
|
||||
Name: "",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/users", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.CreateUserHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusBadRequest {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.Response
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "error" {
|
||||
t.Errorf("期望状态为 'error', 实际为 '%s'", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateUserHandlerInvalidJSON 测试创建用户处理器(无效JSON)
|
||||
func TestCreateUserHandlerInvalidJSON(t *testing.T) {
|
||||
invalidJSON := []byte(`{"name": "测试用户", "email": "test@example.com", "age":}`)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/users", bytes.NewBuffer(invalidJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.CreateUserHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusBadRequest {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatsHandler 测试统计信息处理器
|
||||
func TestStatsHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/stats", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.StatsHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
// 检查响应体
|
||||
var response server.StatsResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if response.Status != "success" {
|
||||
t.Errorf("期望状态为 'success', 实际为 '%s'", response.Status)
|
||||
}
|
||||
|
||||
if response.NumCPU <= 0 {
|
||||
t.Error("CPU数量应该大于0")
|
||||
}
|
||||
|
||||
if response.NumGoroutine <= 0 {
|
||||
t.Error("Goroutine数量应该大于0")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIDocHandler 测试API文档处理器
|
||||
func TestAPIDocHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.APIDocHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查状态码
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("处理器返回错误状态码: got %v want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
// 检查响应内容类型
|
||||
expected := "application/json"
|
||||
if ct := rr.Header().Get("Content-Type"); ct != expected {
|
||||
t.Errorf("处理器返回错误内容类型: got %v want %v", ct, expected)
|
||||
}
|
||||
|
||||
// 检查响应体包含API文档信息
|
||||
var doc map[string]interface{}
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &doc); err != nil {
|
||||
t.Errorf("无法解析响应JSON: %v", err)
|
||||
}
|
||||
|
||||
if _, exists := doc["title"]; !exists {
|
||||
t.Error("API文档应该包含title字段")
|
||||
}
|
||||
|
||||
if _, exists := doc["endpoints"]; !exists {
|
||||
t.Error("API文档应该包含endpoints字段")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer 测试完整的服务器
|
||||
func TestServer(t *testing.T) {
|
||||
// 创建服务器
|
||||
srv := server.NewServer(":0") // 使用随机端口
|
||||
srv.SetupRoutes()
|
||||
|
||||
// 创建测试服务器
|
||||
testServer := httptest.NewServer(srv.GetRouter())
|
||||
defer testServer.Close()
|
||||
|
||||
// 测试健康检查
|
||||
resp, err := http.Get(testServer.URL + "/health")
|
||||
if err != nil {
|
||||
t.Fatalf("健康检查请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("健康检查返回错误状态码: got %v want %v", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
// 测试获取用户列表
|
||||
resp, err = http.Get(testServer.URL + "/api/users")
|
||||
if err != nil {
|
||||
t.Fatalf("获取用户列表请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("获取用户列表返回错误状态码: got %v want %v", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMiddleware 测试中间件
|
||||
func TestMiddleware(t *testing.T) {
|
||||
// 测试CORS中间件
|
||||
req, err := http.NewRequest("OPTIONS", "/api/users", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// 创建带中间件的处理器
|
||||
handler := server.CORSMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// 检查CORS头
|
||||
if origin := rr.Header().Get("Access-Control-Allow-Origin"); origin != "*" {
|
||||
t.Errorf("期望CORS Origin为 '*', 实际为 '%s'", origin)
|
||||
}
|
||||
|
||||
if methods := rr.Header().Get("Access-Control-Allow-Methods"); methods == "" {
|
||||
t.Error("应该设置Access-Control-Allow-Methods头")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHealthHandler 健康检查处理器基准测试
|
||||
func BenchmarkHealthHandler(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", "/health", nil)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.HealthHandler)
|
||||
handler.ServeHTTP(rr, req)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetUsersHandler 获取用户列表处理器基准测试
|
||||
func BenchmarkGetUsersHandler(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", "/api/users", nil)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.GetUsersHandler)
|
||||
handler.ServeHTTP(rr, req)
|
||||
}
|
||||
}
|
||||
|
||||
// ExampleHealthHandler 健康检查处理器示例
|
||||
func ExampleHealthHandler() {
|
||||
req, _ := http.NewRequest("GET", "/health", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(server.HealthHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
fmt.Printf("Status: %d", rr.Code)
|
||||
// Output: Status: 200
|
||||
}
|
101
golang-learning/10-projects/03-web-server/static/index.html
Normal file
101
golang-learning/10-projects/03-web-server/static/index.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Go Web服务器</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🚀 Go Web服务器</h1>
|
||||
<p>一个使用Go语言编写的简单HTTP服务器示例</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="api-section">
|
||||
<h2>📚 API接口测试</h2>
|
||||
|
||||
<div class="api-group">
|
||||
<h3>用户管理</h3>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="getUsers()">获取所有用户</button>
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint">/api/users</span>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="createUser()">创建用户</button>
|
||||
<span class="method post">POST</span>
|
||||
<span class="endpoint">/api/users</span>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<input type="number" id="userId" placeholder="用户ID" min="1">
|
||||
<button onclick="getUser()">获取用户</button>
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint">/api/users/{id}</span>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="deleteUser()">删除用户</button>
|
||||
<span class="method delete">DELETE</span>
|
||||
<span class="endpoint">/api/users/{id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-group">
|
||||
<h3>系统信息</h3>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="getHealth()">健康检查</button>
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint">/health</span>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<button onclick="getStats()">服务器统计</button>
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint">/api/stats</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h2>📝 创建用户表单</h2>
|
||||
<form id="userForm">
|
||||
<div class="form-group">
|
||||
<label for="name">姓名:</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱:</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="age">年龄:</label>
|
||||
<input type="number" id="age" name="age" min="0" max="150" required>
|
||||
</div>
|
||||
|
||||
<button type="submit">创建用户</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="response-section">
|
||||
<h2>📄 响应结果</h2>
|
||||
<pre id="response"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© 2024 Go Web服务器示例项目</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
241
golang-learning/10-projects/03-web-server/static/script.js
Normal file
241
golang-learning/10-projects/03-web-server/static/script.js
Normal file
@@ -0,0 +1,241 @@
|
||||
// script.js - JavaScript 文件
|
||||
|
||||
// API 基础URL
|
||||
const API_BASE = '/api';
|
||||
|
||||
// 响应显示元素
|
||||
const responseElement = document.getElementById('response');
|
||||
|
||||
// 显示响应结果
|
||||
function showResponse(data, isError = false) {
|
||||
responseElement.textContent = JSON.stringify(data, null, 2);
|
||||
responseElement.className = isError ? 'error' : 'success';
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading() {
|
||||
responseElement.textContent = '加载中...';
|
||||
responseElement.className = 'loading';
|
||||
}
|
||||
|
||||
// API 请求封装
|
||||
async function apiRequest(url, options = {}) {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
showResponse(data, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
showResponse(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
showResponse({
|
||||
error: '网络请求失败',
|
||||
message: error.message
|
||||
}, true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有用户
|
||||
async function getUsers() {
|
||||
await apiRequest(`${API_BASE}/users`);
|
||||
}
|
||||
|
||||
// 获取指定用户
|
||||
async function getUser() {
|
||||
const userId = document.getElementById('userId').value;
|
||||
if (!userId) {
|
||||
showResponse({
|
||||
error: '请输入用户ID'
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
await apiRequest(`${API_BASE}/users/${userId}`);
|
||||
}
|
||||
|
||||
// 创建用户(使用按钮)
|
||||
async function createUser() {
|
||||
const userData = {
|
||||
name: '测试用户',
|
||||
email: `test${Date.now()}@example.com`,
|
||||
age: Math.floor(Math.random() * 50) + 18
|
||||
};
|
||||
|
||||
await apiRequest(`${API_BASE}/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
async function deleteUser() {
|
||||
const userId = document.getElementById('userId').value;
|
||||
if (!userId) {
|
||||
showResponse({
|
||||
error: '请输入用户ID'
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`确定要删除用户 ${userId} 吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await apiRequest(`${API_BASE}/users/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
async function getHealth() {
|
||||
await apiRequest('/health');
|
||||
}
|
||||
|
||||
// 获取服务器统计
|
||||
async function getStats() {
|
||||
await apiRequest(`${API_BASE}/stats`);
|
||||
}
|
||||
|
||||
// 表单提交处理
|
||||
document.getElementById('userForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const userData = {
|
||||
name: formData.get('name'),
|
||||
email: formData.get('email'),
|
||||
age: parseInt(formData.get('age'))
|
||||
};
|
||||
|
||||
// 验证数据
|
||||
if (!userData.name || !userData.email || !userData.age) {
|
||||
showResponse({
|
||||
error: '请填写所有必填字段'
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userData.age < 0 || userData.age > 150) {
|
||||
showResponse({
|
||||
error: '年龄必须在0-150之间'
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await apiRequest(`${API_BASE}/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
if (result) {
|
||||
// 清空表单
|
||||
this.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载完成后的初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 显示欢迎信息
|
||||
showResponse({
|
||||
message: '欢迎使用 Go Web服务器 API 测试页面!',
|
||||
instructions: [
|
||||
'点击上方按钮测试各种API接口',
|
||||
'使用表单创建新用户',
|
||||
'查看下方的响应结果'
|
||||
]
|
||||
});
|
||||
|
||||
// 自动获取用户列表
|
||||
setTimeout(getUsers, 1000);
|
||||
});
|
||||
|
||||
// 键盘快捷键
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Ctrl + Enter 快速创建用户
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
createUser();
|
||||
}
|
||||
|
||||
// Ctrl + R 刷新用户列表
|
||||
if (e.ctrlKey && e.key === 'r') {
|
||||
e.preventDefault();
|
||||
getUsers();
|
||||
}
|
||||
});
|
||||
|
||||
// 工具函数:格式化时间
|
||||
function formatTime(timestamp) {
|
||||
return new Date(timestamp).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 工具函数:格式化文件大小
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 工具函数:复制到剪贴板
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
console.log('已复制到剪贴板');
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加复制响应结果的功能
|
||||
responseElement.addEventListener('click', function() {
|
||||
if (this.textContent && this.textContent !== '加载中...') {
|
||||
copyToClipboard(this.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
// 自动刷新功能(可选)
|
||||
let autoRefresh = false;
|
||||
let refreshInterval;
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefresh = !autoRefresh;
|
||||
|
||||
if (autoRefresh) {
|
||||
refreshInterval = setInterval(getUsers, 5000);
|
||||
console.log('自动刷新已启用');
|
||||
} else {
|
||||
clearInterval(refreshInterval);
|
||||
console.log('自动刷新已禁用');
|
||||
}
|
||||
}
|
||||
|
||||
// 错误重试机制
|
||||
async function retryRequest(requestFunc, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await requestFunc();
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
271
golang-learning/10-projects/03-web-server/static/style.css
Normal file
271
golang-learning/10-projects/03-web-server/static/style.css
Normal file
@@ -0,0 +1,271 @@
|
||||
/* style.css - 样式文件 */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.2em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.response-section {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #555;
|
||||
margin: 20px 0 15px 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.api-group {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.api-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.api-item button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.api-item button:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.api-item input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.method {
|
||||
font-weight: bold;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.method.get {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.method.post {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.method.delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #e9ecef;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
#userForm button {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#userForm button:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
#response {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.api-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.api-item button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 成功和错误状态 */
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
Reference in New Issue
Block a user