go语言项目--实例化(图书管理)--v1

v1: 图书录入器 --- 从零开始

一、版本概述

v1 是整个项目的起点,实现了一个最简单的命令行图书录入工具。用户通过交互式命令行输入图书信息(书名、作者、价格),程序将数据保存到文本文件中。

核心学习目标

  • Go 基础语法:变量、函数、控制流
  • 文件 I/O 操作:os.OpenFile、bufio.Scanner/Writer
  • 字符串处理:strings.Split、strconv.ParseFloat
  • 表驱动测试(table-driven tests)

项目结构

bash 复制代码
v1/
├── main.go          # 主程序(所有逻辑在一个文件中)
├── main_test.go     # 单元测试
└── books.txt        # 数据存储文件

二、完整代码

main.go

go 复制代码
package main
 
import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)
 
const (
	dataFile = "books.txt"
	exitCmd  = "quit"
)
 
func main() {
	printWelcome()
 
	file := createOrOpenFile(dataFile)
	defer file.Close()
 
	scanner := bufio.NewScanner(os.Stdin)
	writer := bufio.NewWriter(file)
	defer writer.Flush()
 
	for {
		fmt.Print("\n请输入图书信息(书名,作者,价格)或输入quit退出: ")
		if !scanner.Scan() {
			break
		}
		input := strings.TrimSpace(scanner.Text())
		if input == exitCmd {
			fmt.Println("再见!")
			break
		}
 
		parts := parseInputByComma(input)
		if len(parts) != 3 {
			fmt.Println("❌ 格式错误,请使用: 书名,作者,价格")
			continue
		}
 
		title := strings.TrimSpace(parts[0])
		author := strings.TrimSpace(parts[1])
		priceStr := strings.TrimSpace(parts[2])
 
		if err := validateTitle(title); err != nil {
			fmt.Printf("❌ %v\n", err)
			continue
		}
		if err := validateAuthor(author); err != nil {
			fmt.Printf("❌ %v\n", err)
			continue
		}
		price, err := parseAndValidatePrice(priceStr)
		if err != nil {
			fmt.Printf("❌ %v\n", err)
			continue
		}
 
		line := formatBookLine(title, author, price)
		writeLine(writer, line)
		fmt.Printf("✅ 已录入: %s\n", line)
	}
}
 
func printWelcome() {
	fmt.Println("========================================")
	fmt.Println("  📚 图书录入器 v1.0")
	fmt.Println("========================================")
	fmt.Println("  输入格式: 书名,作者,价格")
	fmt.Println("  示例: Go语言编程,许式伟,79.5")
	fmt.Println("  输入 quit 退出")
	fmt.Println("========================================")
}
 
func createOrOpenFile(filename string) *os.File {
	file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		fmt.Printf("❌ 无法打开文件: %v\n", err)
		os.Exit(1)
	}
	return file
}
 
func parseInputByComma(input string) []string {
	return strings.Split(input, ",")
}
 
func validateTitle(title string) error {
	if title == "" {
		return fmt.Errorf("书名不能为空")
	}
	return nil
}
 
func validateAuthor(author string) error {
	if author == "" {
		return fmt.Errorf("作者不能为空")
	}
	return nil
}
 
func parseAndValidatePrice(priceStr string) (float64, error) {
	price, err := strconv.ParseFloat(priceStr, 64)
	if err != nil {
		return 0, fmt.Errorf("价格格式错误: %s", priceStr)
	}
	if price < 0 {
		return 0, fmt.Errorf("价格不能为负数")
	}
	return price, nil
}
 
func formatBookLine(title, author string, price float64) string {
	return fmt.Sprintf("%s|%s|%.2f", title, author, price)
}
 
func writeLine(writer *bufio.Writer, line string) {
	fmt.Fprintln(writer, line)
}

main_test.go

go 复制代码
package main
 
import (
	"testing"
)
 
func TestValidateTitle(t *testing.T) {
	tests := []struct {
		name    string
		title   string
		wantErr bool
	}{
		{"有效标题", "Go语言编程", false},
		{"空标题", "", true},
		{"空格标题", "  ", false},
		{"长标题", "这是一本非常非常长的书名用于测试", false},
		{"特殊字符", "C++ Primer(第5版)", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateTitle(tt.title)
			if (err != nil) != tt.wantErr {
				t.Errorf("validateTitle(%q) error = %v, wantErr %v", tt.title, err, tt.wantErr)
			}
		})
	}
}
 
func TestValidateAuthor(t *testing.T) {
	tests := []struct {
		name    string
		author  string
		wantErr bool
	}{
		{"有效作者", "许式伟", false},
		{"空作者", "", true},
		{"英文名", "Alan Donovan", false},
		{"带标点", "Kernighan, B.W.", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateAuthor(tt.author)
			if (err != nil) != tt.wantErr {
				t.Errorf("validateAuthor(%q) error = %v, wantErr %v", tt.author, err, tt.wantErr)
			}
		})
	}
}
 
func TestParseAndValidatePrice(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		want    float64
		wantErr bool
	}{
		{"正常价格", "79.5", 79.5, false},
		{"整数价格", "100", 100.0, false},
		{"零价格", "0", 0.0, false},
		{"负价格", "-10", 0, true},
		{"非数字", "abc", 0, true},
		{"空字符串", "", 0, true},
		{"科学计数法", "1e5", 100000, false},
		{"超高精度", "79.555", 79.555, false},
		{"极端大数", "9999999999", 9999999999, false},
		{"带空格", " 79.5 ", 79.5, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseAndValidatePrice(tt.input)
			if (err != nil) != tt.wantErr {
				t.Errorf("parseAndValidatePrice(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
			}
			if !tt.wantErr && got != tt.want {
				t.Errorf("parseAndValidatePrice(%q) = %v, want %v", tt.input, got, tt.want)
			}
		})
	}
}
 
func TestParseInputByComma(t *testing.T) {
	tests := []struct {
		name  string
		input string
		want  int
	}{
		{"标准输入", "Go,作者,79.5", 3},
		{"缺少字段", "Go,作者", 2},
		{"多余逗号", "Go,作者,79.5,额外", 4},
		{"无逗号", "Go语言编程", 1},
		{"空字符串", "", 1},
		{"只有逗号", ",,", 3},
		{"中文逗号混合", "Go,作者,79.5", 1},
		{"带空格", "Go , 作者 , 79.5", 3},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parts := parseInputByComma(tt.input)
			if len(parts) != tt.want {
				t.Errorf("parseInputByComma(%q) = %d parts, want %d", tt.input, len(parts), tt.want)
			}
		})
	}
}
 
func TestFormatBookLine(t *testing.T) {
	tests := []struct {
		name   string
		title  string
		author string
		price  float64
		want   string
	}{
		{"正常格式", "Go", "许式伟", 79.5, "Go|许式伟|79.50"},
		{"整数价格", "Go", "许式伟", 100.0, "Go|许式伟|100.00"},
		{"零价格", "Go", "许式伟", 0.0, "Go|许式伟|0.00"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := formatBookLine(tt.title, tt.author, tt.price)
			if got != tt.want {
				t.Errorf("formatBookLine() = %q, want %q", got, tt.want)
			}
		})
	}
}
 
func TestConstants(t *testing.T) {
	if dataFile != "books.txt" {
		t.Errorf("dataFile = %q, want books.txt", dataFile)
	}
	if exitCmd != "quit" {
		t.Errorf("exitCmd = %q, want quit", exitCmd)
	}
}

三、代码解读

3.1 程序流程

  1. 打印欢迎信息printWelcome() 输出使用说明
  2. 打开数据文件createOrOpenFile() 以追加模式打开 books.txt
  3. 循环读取输入bufio.Scanner 逐行读取标准输入
  4. 解析与校验 → 依次校验书名、作者、价格
  5. 格式化与写入 → 格式化为 书名|作者|价格 并写入文件
  6. 退出 → 输入 quit 退出循环

3.2 关键设计点

  • 文件 I/O :使用 os.O_APPEND|os.O_CREATE|os.O_WRONLY 确保文件不存在时创建、存在时追加
  • 缓冲写入bufio.Writer 提高写入性能,defer writer.Flush() 确保退出时刷新
  • 输入校验:分层校验(格式 → 书名 → 作者 → 价格),任何一步失败都给出明确提示
  • 常量提取dataFileexitCmd 定义为常量,便于维护

3.3 测试设计

  • 表驱动测试 :每个校验函数使用 []struct{name, input, want, wantErr} 表格,覆盖正常/异常/边界场景
  • 独立可测试validateTitleparseAndValidatePrice 等纯函数无副作用,天然可测试

四、设计思想

思想 体现
单一职责 每个函数只做一件事:校验、解析、格式化、写入
防御式编程 每一步输入都做校验,错误信息具体明确
可测试性 业务逻辑拆分为纯函数,便于单元测试
常量提取 魔法值提取为命名常量,语义清晰

五、为什么需要 v2?

v1 存在明显的局限性:

  1. 数据无法查询和修改:只能追加写入,无法查看已录入的图书、无法修改或删除
  2. 数据结构缺失 :没有 Book 结构体,数据以原始字符串形式存储
  3. 没有分层架构:所有逻辑堆积在 main.go 中,随着功能增加会变得无法维护
  4. 没有持久化检索:文本文件只能顺序读取,不支持按 ID 查询

v2 的改进方向:引入结构体和分层架构(Model-Repository-Handler),实现完整的 CRUD 操作和 JSON 持久化。

相关推荐
MeixianAgent1 小时前
Python 回测数据入口怎么验?历史 K 线入库前先做 5 个检查
后端·python
9i编程1 小时前
SpringBoot 测试环境免发短信验证码方案,节省测试短信成本
后端
Ai拆代码的曹操1 小时前
把线程 Dump 读薄:从 BLOCKED/WAITING/RUNNABLE 到问题定位的完整方法论
后端
雪隐2 小时前
个人电脑玩AI-09让5060 Ti给你打工——让 AI 读懂你的资料
人工智能·后端
小满zs2 小时前
Go语言第一章(入门)
后端·go
用户6757049885023 小时前
Kafka 太重?试试 NSQ:一个优雅到极致的消息队列
后端·go
铁皮饭盒3 小时前
S3已成为文件存储标准,阿里/腾讯/华为云都支持,Bun率先原生支持
前端·javascript·后端
洛卡卡了3 小时前
Claude Code Hook,当 CLAUDE.md 规则不生效时,我们还需要强制拦截机制
后端·agent·claude
用户6757049885023 小时前
RabbitMQ 太重,Kafka 太复杂?Go 开发者:Asynq分布式任务队列就刚刚好
后端·go