大家好,我是极客老墨。

学了这么多语法,是时候写个完整项目了。光看代码片段,很难理解 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 Intel
  • GOOS=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 个关键点:

  1. 项目初始化:使用 go mod init 创建模块,go.mod 是项目起点
  2. 目录结构:cmd/ 存放入口,pkg/ 存放库代码,internal/ 存放私有代码
  3. 包管理:首字母大写导出,小写私有,使用 go get 添加依赖
  4. 测试和构建:go test 运行测试,go build 编译,支持交叉编译
  5. 项目发布:编写 README,使用 Git 管理,打标签发布版本

实战建议

  • 从简单结构开始,需要时再复杂化
  • 编写测试,确保代码质量
  • 使用 Makefile 简化构建流程
  • 编写清晰的 README 文档
  • 遵循 Go 社区的命名和结构约定

从零到一构建项目,是学习 Go 最好的方式。

练习题

  1. 扩展计算器,添加幂运算和开方运算
  2. 实现交互式模式,可以连续输入多个计算
  3. 添加配置文件支持,可以自定义输出格式和颜色
  4. 实现计算历史的保存和加载功能(保存到文件)
  5. 添加单位转换功能(如长度、重量、温度)
  6. 使用 cobra 库重构命令行参数解析,支持子命令

极客老墨,继续折腾!

如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。

完整示例代码在 go-tutorial-code/15-project-example


相关阅读