Pengembangan Go dengan Claude Code: go.mod, Tes, Concurrency, dan Race Detector
Panduan praktis Claude Code untuk Go: peta repo, go.mod, go work, API/CLI, tes, context, concurrency, dan benchmark.
Saat memakai Claude Code untuk pengembangan Go, prompt yang berisiko adalah “buat API” atau “tambahkan tes” tanpa konteks repository. Kode mungkin berjalan, tetapi bisa menyimpang dari go.mod, batas package, kebijakan error, propagasi context, dan keamanan concurrency. Go terlihat sederhana, sehingga masalah seperti ini sering tersembunyi dalam diff kecil.
Panduan ini memperlakukan Claude Code sebagai agen pengembangan Go, bukan sekadar generator kode. Alurnya mencakup pemetaan repository, keputusan module dan workspace, perubahan API/CLI, table-driven tests, error wrapping, context cancellation, jebakan concurrency, race detector, benchmark, dan prompt yang aman. Module adalah unit pengelolaan dependency, workspace adalah dasar kerja untuk beberapa module lokal, dan context membawa sinyal pembatalan serta deadline ke panggilan di bawahnya.
Masa mencoba alur ini pada API task kecil berbasis Go. Prompt pertama hanya “tambahkan handler”; hasilnya berjalan lokal, tetapi tidak ada tes, perilaku pembatalan tidak jelas, dan ada risiko goroutine menulis ke map bersama. Setelah prompt diubah menjadi “baca repo dan verifikasi dengan go test -race”, review menjadi jauh lebih konkret.
Buat peta repository terlebih dahulu
Tugas Go pertama bukan menghasilkan kode. Tugas pertama adalah membuat peta: direktori mana yang berisi command, package mana yang internal, go.mod mana yang mengatur dependency, dan apa yang sudah dicek CI. Minta Claude Code melakukan pass pertama secara read-only.
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 ./...
Gunakan prompt yang dibatasi:
Inspeksi repository Go ini. Jangan edit file dulu.
Laporkan:
- Apakah go.mod dan go.work ada
- Tujuan cmd, internal, pkg, api, migrations, dan testdata
- Public types/functions yang dapat memutus kompatibilitas jika diubah
- Gaya error handling yang sudah ada
- Boundary yang menerima context.Context
- Hasil go test ./...
- Kumpulan file minimum yang aman disentuh untuk tugas berikutnya
Untuk referensi resmi, gunakan Organizing a Go module untuk struktur dan Go Modules Reference untuk perilaku module. Untuk Claude Code, rujuk 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"]
Untuk workflow terkait, lihat pemetaan codebase yang sudah ada dan praktik terbaik CLAUDE.md. Di Go, atur perjanjian kerja sebelum meminta implementasi agar Claude Code tidak menyentuh file yang tidak relevan.
Jangan biarkan go.mod dan go work drift
go.mod mencatat module path, versi Go, dan dependency. Aplikasi kecil biasanya cukup dengan satu go.mod. Repository multi-module dapat memakai go.work agar perintah go melihat beberapa module lokal sekaligus. Tutorial resminya adalah Getting started with multi-module workspaces.
Sebelum mengizinkan Claude Code menambah dependency, inspeksi kondisi saat ini:
go env GOMOD GOWORK
go list -m all
go mod tidy
go test ./...
Buat workspace hanya saat benar-benar perlu mengedit beberapa module bersama:
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
Jebakannya adalah memakai go.work sebagai trik dependency tersembunyi. Kode bisa lulus lokal karena workspace menunjuk ke module lokal, lalu gagal di CI atau mesin anggota tim lain. Tentukan apakah go.work adalah infrastruktur tim atau state lokal pribadi, dan minta Claude Code menjelaskan module atau dependency baru sebelum mengubahnya.
Bagikan service layer antara API dan CLI
Di Go, executable sering ditempatkan di cmd/, sedangkan logika bisnis reusable ditempatkan di internal/. Contoh berikut memisahkan Store inti dari HTTP sehingga CLI dapat memakai perilaku yang sama nanti. Contoh ini hanya memakai standard library. Tempelkan ke 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"})
}
}
Jalankan dengan:
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"}'
Jika nanti menambahkan CLI, jangan menyalin logic handler. Panggil service layer yang sama. Topik ini dibahas di pengembangan CLI dengan Claude Code.
Jadikan table-driven tests bagian dari Done
Table-driven test menyusun input dan expected result dalam slice, lalu menjalankan logika test yang sama untuk setiap baris. Ini pola Go yang cocok untuk validasi, error, dan edge case. Minta kasus yang spesifik, bukan hanya “tulis tes”.
// 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)
}
}
}
Masukkan perintah ini ke acceptance criteria:
gofmt -w cmd/taskapi
go test ./...
go test -race ./...
go test -run TestStoreCreate -count=1 ./...
go test -bench=. -benchmem ./...
Package resmi testing mendokumentasikan test dan benchmark. Benchmark adalah loop pengukuran yang dikendalikan Go lewat b.N, sehingga keputusan tidak hanya berdasarkan rasa “lebih cepat”.
Pertahankan context cancellation
context.Context membawa cancellation, deadline, dan nilai request antar boundary API. Package resmi context menjelaskan bahwa request masuk ke server membuat Context, dan panggilan keluar sebaiknya menerima Context.
Kesalahan umum Claude Code adalah menerima r.Context() di handler, lalu membuat context.Background() lagi di service atau repository. Itu memutus rantai cancellation. Jika client disconnect, query database atau panggilan API eksternal bisa tetap berjalan. Tulis di prompt: jangan membuat background context baru di layer bawah; teruskan ctx dari caller.
Kesalahan lain adalah lupa memanggil cancel dari context.WithTimeout. Minta Claude Code memakai defer cancel() dan menjalankan go vet saat memperkenalkan derived context.
Periksa concurrency dengan race detector
Goroutine ringan, tetapi data bersama tetap butuh sinkronisasi. Data race terjadi saat goroutine mengakses variabel yang sama secara bersamaan, setidaknya satu akses menulis, dan tidak ada sinkronisasi. Data Race Detector resmi dijalankan dengan go test -race. Ia hanya menemukan race pada code path yang dieksekusi.
Hindari penulisan concurrent ke map bersama:
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
}
Lindungi state bersama dengan 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
}
Prompt tidak cukup hanya “buat concurrent”. Minta Claude Code mengidentifikasi shared variables, siapa yang berhak menutup channel, perilaku cancellation, kebenaran WaitGroup, dan hasil go test -race. Untuk sudut review, lihat code review dengan Claude Code.
Prompt Claude Code yang aman untuk Go
Prompt Go yang baik menjelaskan scope edit, kontrak, command verifikasi, dan perubahan yang dilarang:
Goal: tambahkan validasi dan tes untuk POST /tasks di taskapi.
File yang boleh diedit:
- cmd/taskapi/main.go
- cmd/taskapi/store_test.go
Rules:
- Jangan ubah module path di go.mod
- Jangan tambah external dependency baru
- Jangan rename public types atau JSON fields
- Propagasikan context.Context dari handler ke service
- Wrap error dengan fmt.Errorf("%w") dan cek dengan errors.Is
Done berarti:
- gofmt -w cmd/taskapi
- go test ./...
- go test -race ./...
- Laporkan perubahan dan remaining risk secara singkat
Jika dependency mungkin diperlukan, paksa berhenti dulu:
Jika kamu yakin library eksternal diperlukan, jelaskan kandidat, alasan, mengapa standard library tidak cukup, dan dampak ke go.mod sebelum implementasi. Jangan jalankan go get tanpa approval.
Ini mencegah masuknya framework CLI, router, ORM, atau library mock secara mengejutkan. Go sering cukup jauh dengan standard library, jadi perubahan dependency harus selalu direview.
Use case dan jebakan
Use case pertama adalah menambahkan endpoint kecil ke API yang sudah ada. Sertakan handler, method service, table-driven tests, dan go test -race dalam satu tugas. Jebakannya adalah hanya menambah handler lalu merusak boundary service atau format error.
Use case kedua adalah berbagi logic antara CLI dan API. Jika tim operasi memakai CLI dan admin UI memanggil API untuk rule yang sama, pindahkan rule ke internal/service. Jebakannya adalah menyalin logic ke CLI lalu membiarkannya drift.
Use case ketiga adalah parallelisasi agregasi lambat. Dashboard yang memanggil tiga API eksternal dapat terbantu oleh goroutine. Jebakannya adalah menulis ke slice/map bersama, beberapa goroutine menutup channel yang sama, dan goroutine leak setelah cancellation.
Use case keempat adalah optimasi performa. Sebelum meminta Claude Code “buat lebih cepat”, ambil baseline dengan go test -bench=. -benchmem. Jebakannya adalah menambah cache atau goroutine tanpa pengukuran, sehingga memori dan kompleksitas naik.
Use case kelima adalah repository multi-module. go.work membuat edit lokal nyaman, tetapi cek go env GOWORK dan jalankan test dari lokasi yang sama dengan CI. Jebakannya adalah menganggap lulus di workspace lokal sebagai bukti production.
CTA dan langkah berikutnya
Pembaca yang memakai Claude Code untuk Go biasanya ingin adopsi aman di repository tim nyata. Untuk workflow pribadi cepat, mulai dari cheatsheet gratis. Untuk mengatur CLAUDE.md, permissions, prompt review, dan checklist Go dalam satu paket, lihat produk dan template. Untuk adopsi tim pada Go API, CLI, CI, dan aturan review, gunakan training dan konsultasi Claude Code.
Ringkasan
Workflow Go yang stabil adalah: petakan repository dulu, inspeksi go.mod dan go.work, simpan perubahan API/CLI di balik service layer, jadikan table-driven tests bagian dari Done, pertahankan context cancellation, jalankan race detector, dan ukur sebelum mengklaim peningkatan performa.
Saya menguji alur ini pada API task kecil. Perubahan paling berdampak adalah memasukkan command verifikasi langsung ke prompt. Saat gofmt, go test ./..., go test -race ./..., dan go test -bench=. -benchmem menjadi completion criteria, laporan Claude Code berubah dari “implemented” menjadi “verified, with these remaining risks.” Sebelum publish, periksa juga official links, internal links, kedalaman isi, code fences, updatedDate, dan heroImage.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.