Use Cases (更新: 2026/6/2)

用 Claude Code 做 Go 开发:go.mod、测试、并发与竞态检查

用 Claude Code 安全推进 Go 开发:仓库梳理、go.mod、go work、API/CLI、测试、context 与并发。

用 Claude Code 做 Go 开发:go.mod、测试、并发与竞态检查

用 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.modgo.work,把 API/CLI 变更收敛到 service 层,把 table-driven test、context cancellation、race detector、benchmark 都写进完成条件。

我按这个流程试了一个小型任务 API。最有效的改变,是把验证命令直接写进提示词。gofmtgo test ./...go test -race ./...go test -bench=. -benchmem成为完成标准后,Claude Code 的报告从“已实现”变成了“已验证,并列出残余风险”。发布前还要检查官方链接、内部链接、正文厚度、代码围栏、updatedDateheroImage

#Claude Code #Go #Golang #go.mod #测试 #并发
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。