用 Claude Code 做 Go 开发:go.mod、测试、并发与竞态检查
用 Claude Code 安全推进 Go 开发:仓库梳理、go.mod、go work、API/CLI、测试、context 与并发。
用 Claude Code 做 Go 开发时,最危险的提示词是只说“帮我写一个 API”或“加一些测试”。这样通常能得到能跑的代码,但它可能不符合现有的go.mod、包边界、错误处理规则、context传递方式和并发安全要求。Go 的语法很克制,所以这些问题常常藏在看起来很小的 diff 里。
本文把 Claude Code 当成 Go 开发代理,而不是单纯的代码生成器。流程包括仓库地图、模块和 workspace 判断、API/CLI 变更、table-driven test、错误包装、context 取消、并发陷阱、race detector、benchmark,以及更安全的 Go 提示词。这里的模块是依赖管理单位,workspace 是同时编辑多个模块的工作台,context是把取消和超时传给下游函数的机制。
Masa 曾用 Claude Code 改一个小型任务 API。第一次只要求“加 handler”,结果本地能跑,但没有测试,取消行为不清楚,还出现了 goroutine 并发写共享map的风险。后来把提示改成“先读仓库,再用go test -race验证”,代码评审就变得具体很多。
先画仓库地图
Go 开发的第一步不是生成代码,而是画地图:哪些目录是可执行命令,哪些包是内部实现,哪个go.mod管理依赖,CI 已经跑了哪些检查。先让 Claude Code 做只读调查。
pwd
find . -name go.mod -o -name go.work -o -name "*.go" | sort | sed -n '1,120p'
go env GOMOD GOWORK
go list -m
go list ./...
go test ./...
可以这样发起第一次任务:
请检查这个 Go 仓库。现在不要编辑文件。
请报告:
- 是否存在 go.mod 和 go.work
- cmd, internal, pkg, api, migrations, testdata 的作用
- 改动后可能破坏兼容性的 public 类型和函数
- 现有错误处理风格
- 哪些边界接收 context.Context
- go test ./... 的结果
- 下一个任务中最小且安全的可编辑文件集合
官方资料建议以Organizing a Go module理解目录组织,以Go Modules Reference理解模块和依赖。Claude Code 本身参考Claude Code overview。
flowchart LR
A["repo map"] --> B["go.mod / go.work"]
B --> C["API or CLI change"]
C --> D["table-driven tests"]
D --> E["go test -race"]
E --> F["benchmark and review"]
类似的前置调查可以结合既有代码库地图和CLAUDE.md 最佳实践。Go 项目尤其需要先固定工作约定,否则 Claude Code 很容易触碰无关文件。
不要让 go.mod 和 go work 漂移
go.mod记录模块路径、Go 版本和依赖。小应用通常一个go.mod就够。多模块仓库可以用go.work让 Go 命令同时看到多个本地模块,官方教程是Getting started with multi-module workspaces。
允许 Claude Code 加依赖前,先看当前状态:
go env GOMOD GOWORK
go list -m all
go mod tidy
go test ./...
只有确实需要同时编辑多个模块时,才创建 workspace:
mkdir -p services/taskapi tools/taskctl
cd services/taskapi
go mod init example.com/acme/taskapi
cd ../..
cd tools/taskctl
go mod init example.com/acme/taskctl
cd ../..
go work init ./services/taskapi ./tools/taskctl
go work use ./services/taskapi ./tools/taskctl
go work sync
风险在于把go.work当成隐藏的依赖解决方案。本地因为 workspace 指向本地模块而通过,CI 或同事机器却失败。团队要明确go.work是提交到仓库,还是只作为个人本地状态;并要求 Claude Code 在新增模块或依赖前说明理由。
API 和 CLI 共享 service 层
Go 项目通常把可执行入口放在cmd/,把业务逻辑放在internal/。下面的例子把核心Store与 HTTP 分开,将来 CLI 也能复用同一层。它只使用标准库,可以直接放到cmd/taskapi/main.go。
// cmd/taskapi/main.go
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"time"
)
var ErrValidation = errors.New("validation failed")
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
}
type Store struct {
mu sync.Mutex
next int
tasks map[string]Task
}
func NewStore() *Store {
return &Store{next: 1, tasks: make(map[string]Task)}
}
func (s *Store) Create(ctx context.Context, title string) (Task, error) {
select {
case <-ctx.Done():
return Task{}, fmt.Errorf("create task canceled: %w", ctx.Err())
default:
}
title = strings.TrimSpace(title)
if title == "" {
return Task{}, fmt.Errorf("%w: title is required", ErrValidation)
}
s.mu.Lock()
defer s.mu.Unlock()
task := Task{
ID: fmt.Sprintf("task-%06d", s.next),
Title: title,
Status: "open",
CreatedAt: time.Now().UTC(),
}
s.next++
s.tasks[task.ID] = task
return task, nil
}
func (s *Store) List(ctx context.Context) ([]Task, error) {
select {
case <-ctx.Done():
return nil, fmt.Errorf("list tasks canceled: %w", ctx.Err())
default:
}
s.mu.Lock()
defer s.mu.Unlock()
tasks := make([]Task, 0, len(s.tasks))
for _, task := range s.tasks {
tasks = append(tasks, task)
}
return tasks, nil
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
store := NewStore()
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks", func(w http.ResponseWriter, r *http.Request) {
tasks, err := store.List(r.Context())
if err != nil {
writeError(w, err)
return
}
writeJSON(w, http.StatusOK, tasks)
})
mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {
var body struct {
Title string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
task, err := store.Create(r.Context(), body.Title)
if err != nil {
writeError(w, err)
return
}
writeJSON(w, http.StatusCreated, task)
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
logger.Info("taskapi listening", "addr", server.Addr)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("server failed", "error", err)
os.Exit(1)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
logger.Error("graceful shutdown failed", "error", err)
}
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrValidation):
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
writeJSON(w, http.StatusRequestTimeout, map[string]string{"error": err.Error()})
default:
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
}
}
运行方式:
go mod init example.com/taskapi
mkdir -p cmd/taskapi
go run ./cmd/taskapi
curl -s http://localhost:8080/tasks
curl -s -X POST http://localhost:8080/tasks \
-H 'content-type: application/json' \
-d '{"title":"write table-driven tests"}'
之后增加 CLI 时,不要复制 handler 逻辑,而是调用同一个 service 层。CLI 主题可参考用 Claude Code 开发 CLI 工具。
把 table-driven test 作为完成条件
table-driven test 是把输入和期望值像表格一样列出,再用同一个测试逻辑逐行验证的 Go 常见写法。它适合 validation、错误分支和边界值。提示词里要明确测试项。
// cmd/taskapi/store_test.go
package main
import (
"context"
"errors"
"fmt"
"testing"
)
func TestStoreCreate(t *testing.T) {
tests := []struct {
name string
title string
wantErr error
}{
{name: "valid title", title: "ship release notes"},
{name: "blank title", title: " ", wantErr: ErrValidation},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := NewStore()
task, err := store.Create(context.Background(), tt.title)
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Fatalf("error = %v, want %v", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if task.ID == "" || task.Status != "open" {
t.Fatalf("unexpected task: %+v", task)
}
})
}
}
func TestStoreCreateHonorsCanceledContext(t *testing.T) {
store := NewStore()
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := store.Create(ctx, "will not be created")
if !errors.Is(err, context.Canceled) {
t.Fatalf("error = %v, want context.Canceled", err)
}
}
func BenchmarkStoreCreate(b *testing.B) {
store := NewStore()
ctx := context.Background()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := store.Create(ctx, fmt.Sprintf("task-%d", i)); err != nil {
b.Fatal(err)
}
}
}
验收命令也要写进完成条件:
gofmt -w cmd/taskapi
go test ./...
go test -race ./...
go test -run TestStoreCreate -count=1 ./...
go test -bench=. -benchmem ./...
Go 的testing package说明了测试和 benchmark。benchmark 是由 Go 用b.N控制次数的测量循环,避免只凭感觉判断性能。
不要丢失 context cancellation
context.Context用于在 API 边界之间传递取消、截止时间和请求级信息。官方context package说明了服务端请求应该创建 Context,下游调用应该接收 Context。
Claude Code 常见错误是 handler 里拿到了r.Context(),但 service 或 repository 又创建context.Background()。这样会切断取消链路,客户端断开后,数据库查询或外部 API 可能继续运行。提示词要写清楚:下层不要新建 background context,必须传入调用方的ctx。
另一个错误是使用context.WithTimeout后忘记调用cancel。要求 Claude Code 在派生 Context 时加入defer cancel(),并在引入相关代码后运行go vet。
用 race detector 检查并发
goroutine 很轻量,但共享数据仍然需要同步。data race 是多个 goroutine 同时访问同一变量,其中至少一个是写入,且没有同步。官方Data Race Detector可通过go test -race运行,但它只能发现已执行路径上的问题。
不要这样并发写共享map:
func CountByStatusBad(tasks []Task) map[string]int {
counts := make(map[string]int)
var wg sync.WaitGroup
for _, task := range tasks {
task := task
wg.Add(1)
go func() {
defer wg.Done()
counts[task.Status]++
}()
}
wg.Wait()
return counts
}
用sync.Mutex保护共享状态:
func CountByStatus(tasks []Task) map[string]int {
counts := make(map[string]int)
var (
mu sync.Mutex
wg sync.WaitGroup
)
for _, task := range tasks {
task := task
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counts[task.Status]++
mu.Unlock()
}()
}
wg.Wait()
return counts
}
提示词不能只说“并发化”。要让 Claude Code 识别共享变量、channel 的关闭责任、取消行为、WaitGroup是否一定Done、以及go test -race结果。评审角度可结合Claude Code 代码评审。
安全的 Go 提示词
好的 Go 提示词会写清编辑范围、契约、验证命令和禁止事项。
目标: 为 taskapi 的 POST /tasks 增加 validation 和测试。
允许编辑:
- cmd/taskapi/main.go
- cmd/taskapi/store_test.go
规则:
- 不要修改 go.mod 的 module path
- 不要新增外部依赖
- 不要重命名 public 类型或 JSON field
- context.Context 必须从 handler 传到 service
- error 用 fmt.Errorf("%w") 包装,调用方用 errors.Is 判断
完成条件:
- gofmt -w cmd/taskapi
- go test ./...
- go test -race ./...
- 简短报告变更和残余风险
如果可能需要依赖,先暂停说明:
如果你认为必须引入外部库,请先说明候选库、原因、为什么标准库不够、对 go.mod 的影响。未经批准不要执行 go get。
这能减少突然加入 CLI 框架、router、ORM 或 mock 库的事故。Go 很多场景靠标准库就足够,因此依赖变更要始终进入评审。
实务用例与陷阱
第一个用例是给既有 API 增加小 endpoint。把 handler、service 方法、table-driven test、go test -race放在同一个任务里。陷阱是只加 handler,破坏现有 service 边界或错误格式。
第二个用例是 CLI 和 API 共用逻辑。运维人员用 CLI,管理界面调用 API,如果业务规则相同,应放到internal/service。陷阱是复制逻辑,之后两边行为漂移。
第三个用例是并行化慢聚合。dashboard 同时调用三个外部 API 时,goroutine 会有帮助。陷阱是共享 slice/map 写入、多个 goroutine 关闭同一 channel、取消后 goroutine 泄漏。
第四个用例是性能改善。让 Claude Code “变快”前,先用go test -bench=. -benchmem建立基线。陷阱是没有测量就增加缓存或 goroutine,只增加内存和复杂度。
第五个用例是多模块仓库。go.work让本地编辑方便,但要确认go env GOWORK和 CI 的执行位置。陷阱是把本地 workspace 通过当成生产证据。
变现 CTA 与下一步
用 Claude Code 做 Go 的读者,通常关心的是如何安全放进真实团队仓库。个人先固定提示词和验证命令,可以从免费速查表开始。想把CLAUDE.md、权限、评审提示词和 Go 检查清单打包整理,可看产品与模板。如果团队要把 Claude Code 引入 Go API、CLI、CI 和评审规则,可以使用Claude Code 培训与导入咨询。
总结
稳定的 Go 工作流是:先画仓库地图,检查go.mod和go.work,把 API/CLI 变更收敛到 service 层,把 table-driven test、context cancellation、race detector、benchmark 都写进完成条件。
我按这个流程试了一个小型任务 API。最有效的改变,是把验证命令直接写进提示词。gofmt、go test ./...、go test -race ./...、go test -bench=. -benchmem成为完成标准后,Claude Code 的报告从“已实现”变成了“已验证,并列出残余风险”。发布前还要检查官方链接、内部链接、正文厚度、代码围栏、updatedDate和heroImage。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。