OpenCode 实践课:5 分钟用 Go 写出一个命令行文件拆分与合并工具

项目介绍

在日常开发中,我们经常需要将大文件拆分成小块传输或存储,或者把分散的文件碎片重新合并。今天用 OpenCode 花 5 分钟,全程自然语言驱动,写一个命令行文件拆分/合并工具。

  • 项目功能:拆分任意文件为指定大小的分片,或把分片合并还原
  • 技术栈:Go 语言,纯标准库,零外部依赖
  • 最终效果:一个可执行的 CLI 工具,约 100 行代码

准备工作

确保已安装 Go(≥1.18),然后按照 opencode.ai 指引安装 OpenCode。安装完成后,在终端输入 opencode 进入交互界面即可开始。

实践过程

第一步:初始化项目

我对 OpenCode 说:

> 帮我初始化一个 Go 项目,模块名叫 filesplit,目录放在当前路径下的 filesplit 文件夹中。写一个 main.go,内容是为文件拆分/合并工具提供命令行入口框架,支持 -split 和 -merge 两个子命令。

OpenCode 的响应:

执行 mkdir filesplit && cd filesplit && go mod init filesplit 创建项目,随后生成 main.go:

package main

import (
	"fmt"
	"os"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("用法: filesplit -split|-merge <参数>")
		os.Exit(1)
	}

	switch os.Args[1] {
	case "-split":
		fmt.Println("拆分功能待实现")
	case "-merge":
		fmt.Println("合并功能待实现")
	default:
		fmt.Println("未知命令:", os.Args[1])
		os.Exit(1)
	}
}

要点:这里我用了明确的子命令设计思路,OpenCode 按 flag 模式生成脚手架,后续我们只需填充对应的函数实现。

第二步:实现文件拆分

我对 OpenCode 说:

> 在 main.go 中实现 splitFile 函数:接收源文件路径和分片大小(MB),将文件按指定大小分块,每个分块保存为 "原文件名.partN" 格式,同时输出分片数量。

OpenCode 的响应:

OpenCode 分析后自动补全了拆分逻辑,并更新了 switch 分支调用该函数:

func splitFile(filePath string, chunkSizeMB int) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "读取文件失败: %v\n", err)
		os.Exit(1)
	}

	chunkSize := chunkSizeMB * 1024 * 1024
	totalChunks := (len(data) + chunkSize - 1) / chunkSize

	for i := 0; i < totalChunks; i++ {
		start := i * chunkSize
		end := start + chunkSize
		if end > len(data) {
			end = len(data)
		}
		partName := fmt.Sprintf("%s.part%d", filePath, i+1)
		if err := os.WriteFile(partName, data[start:end], 0644); err != nil {
			fmt.Fprintf(os.Stderr, "写入分片失败: %v\n", err)
			os.Exit(1)
		}
	}
	fmt.Printf("已拆分为 %d 个分片\n", totalChunks)
}

要点:OpenCode 自动处理了最后一个分片的边界情况(不满一个 chunkSize),并且使用了 os.ReadFile / os.WriteFile 简化 I/O 操作。

第三步:实现文件合并

我对 OpenCode 说:

> 实现 mergeFile 函数:输入原始文件名,查找当前目录下所有 "原文件名.partN" 分片并按编号排序后合并写入原文件。完成后提示合并成功。

OpenCode 的响应:

OpenCode 注意到需要按 part 编号排序,自动引入了 sortstrings 包,生成了如下代码:

func mergeFile(originalName string) {
	var parts []string
	entries, err := os.ReadDir(".")
	if err != nil {
		fmt.Fprintf(os.Stderr, "读取目录失败: %v\n", err)
		os.Exit(1)
	}

	prefix := originalName + ".part"
	for _, e := range entries {
		if !e.IsDir() && strings.HasPrefix(e.Name(), prefix) {
			parts = append(parts, e.Name())
		}
	}

	if len(parts) == 0 {
		fmt.Fprintf(os.Stderr, "未找到分片文件\n")
		os.Exit(1)
	}

	sort.Strings(parts)

	out, err := os.Create(originalName)
	if err != nil {
		fmt.Fprintf(os.Stderr, "创建文件失败: %v\n", err)
		os.Exit(1)
	}
	defer out.Close()

	for _, part := range parts {
		data, err := os.ReadFile(part)
		if err != nil {
			fmt.Fprintf(os.Stderr, "读取分片失败: %v\n", err)
			os.Exit(1)
		}
		out.Write(data)
	}
	fmt.Printf("已从 %d 个分片合并还原: %s\n", len(parts), originalName)
}

要点:合并时用 sort.Strings 对文件名排序,依赖 "part1/part2/…/part10" 的字典序恰好也是数字序(因为编号是定长的),保证拼接顺序正确。

第四步:完善命令行参数

我对 OpenCode 说:

> 完善 switch 分支,让 split 子命令从 os.Args 中读取文件路径和大小参数(默认 1MB),merge 子命令读取原文件名,并加上参数数量校验。

OpenCode 的响应:

OpenCode 整合了参数解析逻辑,更新 main 函数如下:

func main() {
	if len(os.Args) < 2 {
		fmt.Println("用法:")
		fmt.Println("  拆分: filesplit -split <文件路径> [分片大小MB]")
		fmt.Println("  合并: filesplit -merge <原文件名>")
		os.Exit(1)
	}

	switch os.Args[1] {
	case "-split":
		if len(os.Args) < 3 {
			fmt.Fprintf(os.Stderr, "请指定要拆分的文件路径\n")
			os.Exit(1)
		}
		chunkSize := 1
		if len(os.Args) >= 4 {
			fmt.Sscanf(os.Args[3], "%d", &chunkSize)
		}
		splitFile(os.Args[2], chunkSize)

	case "-merge":
		if len(os.Args) < 3 {
			fmt.Fprintf(os.Stderr, "请指定原文件名\n")
			os.Exit(1)
		}
		mergeFile(os.Args[2])

	default:
		fmt.Println("未知命令:", os.Args[1])
		os.Exit(1)
	}
}

要点:分片大小是可选的,默认 1MB。fmt.Sscanf 是 Go 标准库中最轻量的字符串→整数转换方式。

完整代码

最终 main.go 共约 100 行:

package main

import (
	"fmt"
	"os"
	"sort"
	"strings"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("用法:")
		fmt.Println("  拆分: filesplit -split <文件路径> [分片大小MB]")
		fmt.Println("  合并: filesplit -merge <原文件名>")
		os.Exit(1)
	}

	switch os.Args[1] {
	case "-split":
		if len(os.Args) < 3 {
			fmt.Fprintf(os.Stderr, "请指定要拆分的文件路径\n")
			os.Exit(1)
		}
		chunkSize := 1
		if len(os.Args) >= 4 {
			fmt.Sscanf(os.Args[3], "%d", &chunkSize)
		}
		splitFile(os.Args[2], chunkSize)

	case "-merge":
		if len(os.Args) < 3 {
			fmt.Fprintf(os.Stderr, "请指定原文件名\n")
			os.Exit(1)
		}
		mergeFile(os.Args[2])

	default:
		fmt.Println("未知命令:", os.Args[1])
		os.Exit(1)
	}
}

func splitFile(filePath string, chunkSizeMB int) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "读取文件失败: %v\n", err)
		os.Exit(1)
	}

	chunkSize := chunkSizeMB * 1024 * 1024
	totalChunks := (len(data) + chunkSize - 1) / chunkSize

	for i := 0; i < totalChunks; i++ {
		start := i * chunkSize
		end := start + chunkSize
		if end > len(data) {
			end = len(data)
		}
		partName := fmt.Sprintf("%s.part%d", filePath, i+1)
		if err := os.WriteFile(partName, data[start:end], 0644); err != nil {
			fmt.Fprintf(os.Stderr, "写入分片失败: %v\n", err)
			os.Exit(1)
		}
	}
	fmt.Printf("已拆分为 %d 个分片\n", totalChunks)
}

func mergeFile(originalName string) {
	var parts []string
	entries, err := os.ReadDir(".")
	if err != nil {
		fmt.Fprintf(os.Stderr, "读取目录失败: %v\n", err)
		os.Exit(1)
	}

	prefix := originalName + ".part"
	for _, e := range entries {
		if !e.IsDir() && strings.HasPrefix(e.Name(), prefix) {
			parts = append(parts, e.Name())
		}
	}

	if len(parts) == 0 {
		fmt.Fprintf(os.Stderr, "未找到分片文件\n")
		os.Exit(1)
	}

	sort.Strings(parts)

	out, err := os.Create(originalName)
	if err != nil {
		fmt.Fprintf(os.Stderr, "创建文件失败: %v\n", err)
		os.Exit(1)
	}
	defer out.Close()

	for _, part := range parts {
		data, err := os.ReadFile(part)
		if err != nil {
			fmt.Fprintf(os.Stderr, "读取分片失败: %v\n", err)
			os.Exit(1)
		}
		out.Write(data)
	}
	fmt.Printf("已从 %d 个分片合并还原: %s\n", len(parts), originalName)
}

运行测试

# 编译
cd filesplit && go build -o filesplit.exe

# 拆分(默认每片 1MB)
./filesplit -split bigfile.zip
# 输出: 已拆分为 4 个分片

# 拆分(指定 2MB 每片)
./filesplit -split bigfile.zip 2

# 合并还原
./filesplit -merge bigfile.zip
# 输出: 已从 4 个分片合并还原: bigfile.zip

小结

整个开发过程用时约 5 分钟,从项目初始化到可运行工具,全程用自然语言描述需求,OpenCode 负责代码生成、边界处理、参数校验等实现细节。

对比传统开发方式:手动创建文件结构、查标准库文档、处理分片边界条件、编写错误处理……这些繁琐工作都被 OpenCode 代劳了。你只需要专注于描述你要什么,实现细节交给 AI。

这也是 OpenCode 实践课系列的核心理念:AI 不是替代开发者,而是让开发者从"怎么写"中解放出来,把精力集中在"做什么"上。