大家好,我是极客老墨。
学了这么多语法,是时候写个完整项目了。光看代码片段,很难理解 Go 项目是怎么组织的。这篇我们从零开始,写一个命令行工具,看看真实项目的结构和开发流程。
项目目标
我们要做一个简单的命令行计算器,支持基本的数学运算。
功能需求
- 支持加减乘除运算
- 命令行参数输入
- 彩色输出结果
- 错误处理
技术要点
- Go Modules 依赖管理
- 标准项目结构
- 包的导入和使用
- 第三方库集成
- 交叉编译
初始化项目
第一步是创建项目目录并初始化模块。
创建项目
1cd go-tutorial-code
2# 创建项目目录
3mkdir 15-project-example
4cd 15-project-example
5
6# 初始化 Go Module
7go mod init github.com/hankmor/calc
这里老墨为了教程的需要,把代码放到了 go-tutorial-code, 并且模块名称没有与文件夹名称一致。
生成的 go.mod:
1module github.com/hankmor/calc
2
3go 1.24
要点:
- 模块名通常是代码仓库地址
- go.mod 是项目的起点
- 初始化后就可以开始写代码了
项目结构
Go 项目有约定俗成的目录结构。
标准布局
15-project-example/
├── go.mod # 模块定义
├── go.sum # 依赖校验
├── cmd/ # 命令行工具目录
│ └── calc/
│ └── main.go
├── pkg/ # 可导出的库代码
│ └── calculator/
│ ├── calculator.go
│ └── calculator_test.go
├── internal/ # 私有代码(不可被外部导入)
│ └── utils/
│ └── helper.go
├── README.md # 项目说明
└── Makefile # 构建脚本(可选)
目录说明:
cmd/:存放可执行程序的入口pkg/:可以被外部项目导入的库代码internal/:私有代码,只能在本项目内使用
简化版结构
对于简单项目,可以使用更简单的结构:
go-calc/
├── go.mod
├── main.go # 程序主入口
└── calculator/
├── calculator.go
└── calculator_test.go
编写核心逻辑
先实现计算器的核心功能。
创建 calculator 包
1// calculator/calculator.go
2package calculator
3
4import (
5 "fmt"
6)
7
8// Calculator 计算器结构
9type Calculator struct {
10 history []string
11}
12
13// New 创建计算器实例
14func New() *Calculator {
15 return &Calculator{
16 history: []string{},
17 }
18}
19
20// Add 加法
21func (c *Calculator) Add(a, b float64) float64 {
22 result := a + b
23 c.addHistory(fmt.Sprintf("%.2f + %.2f = %.2f", a, b, result))
24 return result
25}
26
27// Subtract 减法
28func (c *Calculator) Subtract(a, b float64) float64 {
29 result := a - b
30 c.addHistory(fmt.Sprintf("%.2f - %.2f = %.2f", a, b, result))
31 return result
32}
33
34// Multiply 乘法
35func (c *Calculator) Multiply(a, b float64) float64 {
36 result := a * b
37 c.addHistory(fmt.Sprintf("%.2f * %.2f = %.2f", a, b, result))
38 return result
39}
40
41// Divide 除法
42func (c *Calculator) Divide(a, b float64) (float64, error) {
43 if b == 0 {
44 return 0, fmt.Errorf("division by zero")
45 }
46 result := a / b
47 c.addHistory(fmt.Sprintf("%.2f / %.2f = %.2f", a, b, result))
48 return result, nil
49}
50
51// History 获取历史记录
52func (c *Calculator) History() []string {
53 return c.history
54}
55
56// addHistory 添加历史记录(私有方法)
57func (c *Calculator) addHistory(record string) {
58 c.history = append(c.history, record)
59}
要点:
- 首字母大写的函数可以被外部调用
- 首字母小写的函数是私有的
- 使用结构体封装状态
- 错误处理返回 error
编写测试
1// calculator/calculator_test.go
2package calculator
3
4import "testing"
5
6func TestAdd(t *testing.T) {
7 calc := New()
8 result := calc.Add(10, 20)
9
10 if result != 30 {
11 t.Errorf("Add(10, 20) = %f; want 30", result)
12 }
13}
14
15func TestDivide(t *testing.T) {
16 calc := New()
17
18 // 正常情况
19 result, err := calc.Divide(10, 2)
20 if err != nil {
21 t.Errorf("Divide(10, 2) returned error: %v", err)
22 }
23 if result != 5 {
24 t.Errorf("Divide(10, 2) = %f; want 5", result)
25 }
26
27 // 除零错误
28 _, err = calc.Divide(10, 0)
29 if err == nil {
30 t.Error("Divide(10, 0) should return error")
31 }
32}
33
34func TestHistory(t *testing.T) {
35 calc := New()
36 calc.Add(1, 2)
37 calc.Subtract(5, 3)
38
39 history := calc.History()
40 if len(history) != 2 {
41 t.Errorf("History length = %d; want 2", len(history))
42 }
43}
运行测试:
1go test ./calculator
编写命令行入口
实现命令行交互逻辑。
基础版本
1// main.go
2package main
3
4import (
5 "fmt"
6 "os"
7 "strconv"
8
9 "github.com/hankmor/calc/pkg/calculator"
10)
11
12func main() {
13 // 注意: * 号在终端中表示通配符,所以直接传入 a * b 参数导致无法识别,需要使用 a '*' b, 或者转义 a \* b
14 if len(os.Args) < 4 {
15 fmt.Println("Usage: calc <num1> <operator> <num2>")
16 fmt.Println("Operators: +, -, *, /")
17 fmt.Println("Note: Use quotes for * operator: calc 5 '*' 3")
18 os.Exit(1)
19 }
20
21 // 解析参数
22 a, err := strconv.ParseFloat(os.Args[1], 64)
23 if err != nil {
24 fmt.Println("Error: invalid first number")
25 os.Exit(1)
26 }
27
28 operator := os.Args[2]
29
30 // 检测可能的 shell 通配符展开问题
31 if len(os.Args) > 4 {
32 fmt.Println("Error: too many arguments")
33 fmt.Println("Hint: If using *, wrap it in quotes: calc 5 '*' 3")
34 os.Exit(1)
35 }
36
37 b, err := strconv.ParseFloat(os.Args[3], 64)
38 if err != nil {
39 fmt.Println("Error: invalid second number")
40 os.Exit(1)
41 }
42
43 // 创建计算器
44 calc := calculator.New()
45
46 // 执行运算
47 var result float64
48 switch operator {
49 case "+":
50 result = calc.Add(a, b)
51 case "-":
52 result = calc.Subtract(a, b)
53 case "*":
54 result = calc.Multiply(a, b)
55 case "/":
56 r, err := calc.Divide(a, b)
57 if err != nil {
58 fmt.Printf("Error: %v\n", err)
59 os.Exit(1)
60 }
61 result = r
62 default:
63 fmt.Println("Error: invalid operator")
64 os.Exit(1)
65 }
66
67 // 输出结果
68 fmt.Printf("Result: %.2f\n", result)
69}
特别注意终端中关于 * 的使用问题,代码中有详细注释。
添加第三方库
使用第三方库让输出更美观。
添加 color 库
1# 添加彩色输出库
2go get github.com/fatih/color
go.mod 变化:
1module github.com/hankmor/calc
2
3go 1.24
4
5require (
6 github.com/fatih/color v1.18.0 // indirect
7 github.com/mattn/go-colorable v0.1.13 // indirect
8 github.com/mattn/go-isatty v0.0.20 // indirect
9 golang.org/x/sys v0.25.0 // indirect
10)
使用 color 库
1// main.go
2package main
3
4import (
5 "fmt"
6 "os"
7 "strconv"
8
9 "github.com/fatih/color"
10 "github.com/username/go-calc/calculator"
11)
12
13func main() {
14 if len(os.Args) < 4 {
15 color.Yellow("Usage: calc <num1> <operator> <num2>")
16 color.Yellow("Operators: +, -, *, /")
17 os.Exit(1)
18 }
19
20 // 解析参数
21 a, err := strconv.ParseFloat(os.Args[1], 64)
22 if err != nil {
23 color.Red("Error: invalid first number")
24 os.Exit(1)
25 }
26
27 operator := os.Args[2]
28
29 b, err := strconv.ParseFloat(os.Args[3], 64)
30 if err != nil {
31 color.Red("Error: invalid second number")
32 os.Exit(1)
33 }
34
35 // 创建计算器
36 calc := calculator.New()
37
38 // 执行运算
39 var result float64
40 switch operator {
41 case "+":
42 result = calc.Add(a, b)
43 case "-":
44 result = calc.Subtract(a, b)
45 case "*":
46 result = calc.Multiply(a, b)
47 case "/":
48 r, err := calc.Divide(a, b)
49 if err != nil {
50 color.Red("Error: %v", err)
51 os.Exit(1)
52 }
53 result = r
54 default:
55 color.Red("Error: invalid operator")
56 os.Exit(1)
57 }
58
59 // 彩色输出结果
60 color.Green("Result: %.2f", result)
61
62 // 显示历史记录
63 if len(calc.History()) > 0 {
64 color.Cyan("\nHistory:")
65 for _, record := range calc.History() {
66 fmt.Println(" " + record)
67 }
68 }
69}
构建和运行
编译和运行项目。
开发阶段运行
1# 直接运行
2go run cmd/calc/main.go 10 + 20
3# Result: 30.00
4
5go run cmd/calc/main.go 100 / 5
6# Result: 20.00
7
8go run cmd/calc/main.go 10 / 0
9# Error: division by zero
10
11go run cmd/calc/main.go 100 '*' 5
12# Result: 500.00
编译构建
1# 编译生成可执行文件
2go build -o calc
3
4# 如果是在 cmd/calc 目录
5go build -o calc ./cmd/calc
6
7# 运行
8./calc 50 '*' 2
9# Result: 100.00
指定输出目录
1# 编译到 bin 目录
2go build -o bin/calc
3
4# 如果是在 cmd/calc 目录
5go build -o bin/calc ./cmd/calc
6
7# 运行
8./bin/calc 15 - 5
9# Result: 10.00
交叉编译
Go 支持交叉编译,可以在一个平台编译出其他平台的可执行文件。
编译 Linux 版本
1# 在 Mac/Windows 上编译 Linux 版本
2GOOS=linux GOARCH=amd64 go build -o calc-linux
3
4# 编译 ARM 架构
5GOOS=linux GOARCH=arm64 go build -o calc-linux-arm64
编译 Windows 版本
1# 在 Mac/Linux 上编译 Windows 版本
2GOOS=windows GOARCH=amd64 go build -o calc.exe
编译 macOS 版本
1# 在 Linux/Windows 上编译 macOS 版本
2GOOS=darwin GOARCH=amd64 go build -o calc-mac
常用平台:
GOOS=linux GOARCH=amd64:Linux 64位GOOS=windows GOARCH=amd64:Windows 64位GOOS=darwin GOARCH=amd64:macOS IntelGOOS=darwin GOARCH=arm64:macOS Apple Silicon
添加 Makefile
使用 Makefile 简化构建流程。
创建 Makefile
1# Makefile
2.PHONY: build run test clean install
3
4# 默认目标
5all: build
6
7# 构建
8build:
9 go build -o bin/calc ./cmd/calc
10
11# 运行
12# 使用方式: make run ARGS="5 + 3"
13# 或者: make run ARGS="5 '*' 3"
14run:
15 go run cmd/calc/main.go $(ARGS)
16
17# 运行示例
18run-example:
19 @echo "Addition example:"
20 @go run cmd/calc/main.go 5 + 3
21 @echo "\nSubtraction example:"
22 @go run cmd/calc/main.go 10 - 2
23 @echo "\nMultiplication example:"
24 @go run cmd/calc/main.go 5 '*' 3
25 @echo "\nDivision example:"
26 @go run cmd/calc/main.go 10 / 2
27
28# 测试
29test:
30 go test -v ./...
31
32# 清理
33clean:
34 rm -rf bin/
35 go clean
36
37# 安装到 GOPATH
38install:
39 go install
40
41# 交叉编译
42build-all:
43 GOOS=linux GOARCH=amd64 go build -o bin/calc-linux ./cmd/calc
44 GOOS=windows GOARCH=amd64 go build -o bin/calc.exe ./cmd/calc
45 GOOS=darwin GOARCH=amd64 go build -o bin/calc-mac ./cmd/calc
46
47# 格式化代码
48fmt:
49 go fmt ./...
50
51# 代码检查
52lint:
53 golangci-lint run
使用 Makefile
1# 构建
2make build
3
4# 运行测试
5make test
6
7# 清理
8make clean
9
10# 交叉编译所有平台
11make build-all
添加 README
编写项目文档。
README.md
1# Go Calculator
2
3A simple command-line calculator written in Go.
4
5## Features
6
7- Basic arithmetic operations (+, -, \*, /)
8- Colorful output
9- Operation history
10- Cross-platform support
11
12## Installation
13
14```bash
15go install github.com/username/go-calc@latest
16```
Usage
1# Addition
2calc 10 + 20
3
4# Subtraction
5calc 50 - 15
6
7# Multiplication
8calc 5 '*' 8
9
10# Division
11calc 100 / 4
老墨总结
Go 项目实战的 5 个关键点:
- 项目初始化:使用 go mod init 创建模块,go.mod 是项目起点
- 目录结构:cmd/ 存放入口,pkg/ 存放库代码,internal/ 存放私有代码
- 包管理:首字母大写导出,小写私有,使用 go get 添加依赖
- 测试和构建:go test 运行测试,go build 编译,支持交叉编译
- 项目发布:编写 README,使用 Git 管理,打标签发布版本
实战建议:
- 从简单结构开始,需要时再复杂化
- 编写测试,确保代码质量
- 使用 Makefile 简化构建流程
- 编写清晰的 README 文档
- 遵循 Go 社区的命名和结构约定
从零到一构建项目,是学习 Go 最好的方式。
练习题
- 扩展计算器,添加幂运算和开方运算
- 实现交互式模式,可以连续输入多个计算
- 添加配置文件支持,可以自定义输出格式和颜色
- 实现计算历史的保存和加载功能(保存到文件)
- 添加单位转换功能(如长度、重量、温度)
- 使用 cobra 库重构命令行参数解析,支持子命令
极客老墨,继续折腾!
如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。
完整示例代码在 go-tutorial-code/15-project-example。