> Solo Workspace 的 3 个关键技术决策。
---
上篇讲了为什么要写 Solo Workspace——被碎片化工具折磨够了。这篇不讲故事,讲技术选型。
写 CLI 工具之前,先要回答三个问题:
1. **用什么语言?** — Go、Python、Node、Rust?
2. **架构怎么搭?** — 怎么让未来加功能不加复杂度?
3. **配置怎么管?** — 用户数据存在哪、怎么找?
每个问题都有不止一个答案。我选的未必是最优的,但一定是最适合我自己的。
---
## 一、语言选型:为什么是 Go
决定写 CLI 之后,我列了一个语言候选清单:
| 语言 | Python | Node | Go | Rust |
|------|--------|------|----|------|
| 单二进制 | ❌ | ❌ | ✅ | ✅ |
| 编译速度 | ✅ | ❌ | ✅ | ❌ |
| 开发效率 | ✅ | ✅ | ✅ | ❌ |
| 跨平台 | ❌ | ✅ | ✅ | ✅ |
| 生态成熟度 | ✅ | ✅ | ✅ | ❌ |
### Python 先出局
Python 我最熟,但分发太痛苦了。你要用户先装 Python、再 pip install、再面对各种版本冲突。更别说虚拟环境——一个 CLI 工具要用户搞 venv,体验太差了。
### Node 也出局
Node 比 Python 好一点,npx 能直接跑。但 `node_modules` 的体积让人崩溃,而且跨平台总有各种奇怪的问题。
### Rust 很心动,但排除了
Rust 的性能和安全性没话说,但我不是 Rust 开发者。为了写一个 CLI 从头学 Rust,开发周期至少翻三倍。我不想在第一个项目上同时挑战「写工具」和「学语言」两件事。
### Go 胜出
Go 的优势恰好踩中了我所有的需求点:
- **单二进制**:`go build` 出来一个文件,扔到 `~/bin/` 就能用
- **交叉编译**:`GOOS=windows GOARCH=amd64 go build` 一行命令打出 Windows 版本
- **零依赖运行**:编译出来的二进制不依赖任何运行时
- **开发效率**:goroutine、channel、标准库,写 CLI 足够用
- **Cobra**:Go 生态里最成熟的 CLI 框架,不用自己造轮子
最后还有一个私心:**我就想写 Go**。兴趣驱动的事,才容易坚持下去。
---
## 二、插件架构:像乐高一样拼
确定了语言,接下来想一个问题:**我肯定还会加新功能,怎么保证加了不把代码搞乱?**
常见的做法是写一个大 `cmd/` 目录,每个子命令一个文件。但这样下去,十几个命令全堆在一个包里的,早晚变成意大利面条。
我想要的是:**每个功能完全独立,互相不知道对方的存在。** 加一个新功能就是新建一个目录、写一个文件、注册一行代码。不想用了就删掉那行注册,干净彻底。
### 目录即插件
```
cli/go/
└── plugins/
├── server/ ───── server Cmd() → sw server
│ └── plugin.go
├── ssl/ ──────── ssl Cmd() → sw ssl
│ └── plugin.go
├── todo/ ─────── todo Cmd() → sw todo
│ └── plugin.go
├── secret/ ───── secret Cmd() → sw secret
│ ├── plugin.go
│ └── crypto.go
├── env/ ──────── env Cmd() → sw env
│ └── plugin.go
├── config/ ───── config Cmd() → sw config
│ └── plugin.go
├── domain/ ───── domain Cmd() → sw domain
│ └── plugin.go
├── notify/ ───── notify Cmd() → sw notify
│ └── plugin.go
└── project/ ──── project Cmd() → sw project
└── plugin.go
```
每个目录就是一个插件包,暴露一个 `Cmd()` 函数,返回 `*cobra.Command`。
### 三步加一个插件
**第一步:创建目录和文件**
```
mkdir plugins/myplugin && touch plugins/myplugin/plugin.go
```
**第二步:实现 Cobra 命令**
```go
package myplugin
import (
"github.com/spf13/cobra"
)
func Cmd() *cobra.Command {
return &cobra.Command{
Use: "myplugin",
Short: "My new plugin",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Hello from my plugin!")
return nil
},
}
}
```
**第三步:在 root 注册**
```go
// cmd/root.go
import "github.com/shenyb/solo-workspace/cli/go/plugins/myplugin"
func init() {
rootCmd.AddCommand(myplugin.Cmd())
}
```
三步结束。编译、运行、收工。
### 为什么不做动态加载
有人可能会问:为什么不做成像 VS Code 那种动态加载插件?
因为我**不需要**。Solo Workspace 是给独立开发者用的,不是给企业用的。不会有人装几百个插件,不会有人热插拔。编译时静态链接就够用了,简单可靠,还不用写插件 SDK。
---
## 三、配置系统:数据在哪,用户说了算
CLI 工具绕不开一个问题:**配置和用户数据存在哪?**
最简单的方案:硬编码 `~/.solo/`。但这样有个问题——用户想用 `-c` 指定不同的配置怎么办?数据文件是跟着配置走,还是永远在 `~/.solo/`?
### 配置加载链
```
-c <path> 指定 → 最高优先级
~/.solo/config.yaml → 全局默认
.solo.yaml (当前目录) → 项目级配置
空默认值 → 兜底
```
先找 `-c`,没有再找 `~/.solo/config.yaml`,还没有就找当前目录的 `.solo.yaml`,全没有就返回空配置。
逻辑就 15 行,不多:
```go
func LoadConfig(path string) (*Config, error) {
if path == "" {
home, _ := os.UserHomeDir()
homePath := home + "/.solo/config.yaml"
if _, err := os.Stat(homePath); err == nil {
path = homePath
} else if _, err := os.Stat(".solo.yaml"); err == nil {
path = ".solo.yaml"
}
}
// 读取并解析 path
}
```
### 数据文件跟随配置
环境变量文件 `env.yaml` 和加密密钥文件 `secrets.enc` 存在哪?我没有硬编码到 `~/.solo/`,而是让它们跟随配置文件。
```go
func DataDir() (string, error) {
if ConfigPath == "" {
return filepath.Join(home, ".solo"), nil
}
return filepath.Dir(absPath), nil
}
```
简单说:**配置文件在哪,数据文件就在哪。**
- 默认 `~/.solo/config.yaml` → 数据在 `~/.solo/`
- `-c /path/to/project/config.yaml` → 数据在 `/path/to/project/`
这样用户可以把不同项目的数据分开,也可以用一个目录管理所有东西。**选择权在用户手上。**
### 共享状态的设计
还有一个细节:多个插件怎么共享数据?
我的做法很粗暴——**全局变量**:
```go
// internal/config.go
var CurrentConfig *Config
var ConfigPath string
```
每个插件的 `RunE` 直接从 `core.CurrentConfig` 读配置、改完写回去。不是什么优雅的设计,但够用。Go 的 `sync` 包加持下,并发安全也不是问题。
如果要吹毛求疵,这当然不是"最佳实践"。但对我来说,一个单用户的 CLI 工具,可读性和简单性比架构 purity 重要得多。
---
## 下期预告
前两篇讲了**为什么做**和**怎么做**。下一篇是最有料的一篇——**5 天 12 个 commit 的真实演进**:
- 实际使用发现的问题,编辑不方便 加ID
- todo越来越多,就归档
- 发现一个好用的方式:给AI agent直接调用sw todo list 出周报
- ...
---
> GitHub: [github.com/shenyb/solo-workspace](https://github.com/shenyb/solo-workspace)
>
> 上一篇:[# 受够了在 6 个工具之间来回切换,我花 5 天写了个 CLI](https://juejin.cn/post/7649652284339453952)
---