
Writing Go
- 98 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Idiomatic Go 1.25+ development. Use when writing Go code, designing APIs, discussing patterns, reviewing Go implementations.
About
Idiomatic Go 1.25+ development.. Use for Go code writing, API design, pattern discussion, implementation review.
- intermediate skill
- core: go
Writing Go by the numbers
- 98 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #42 of 98 Go skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill writing-goAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 98 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Idiomatic Go 1.25+ development. Use when writing Go code, designing APIs, discussing patterns, reviewing Go implementations.
Files
Go Development (1.25+)
Core Principles
- Stdlib first: External deps only when justified
- Concrete types: Define interfaces at consumer, return structs
- Composition: Over inheritance, always
- Fail fast: Clear errors with context
- Simple: The obvious solution is usually correct
Quick Patterns
Error Handling
if err := doThing(); err != nil {
return fmt.Errorf("do thing: %w", err)
}Struct with Options
type Server struct {
addr string
timeout time.Duration
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second}
for _, opt := range opts {
opt(s)
}
return s
}Table-Driven Tests
tests := []struct {
name string
input string
want string
wantErr bool
}{
{"valid", "hello", "HELLO", false},
{"empty", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Process(tt.input)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}Go 1.25 Features
- testing/synctest: Deterministic concurrent testing with simulated clock
- encoding/json/v2: Experimental, 3-10x faster (GOEXPERIMENT=jsonv2)
- runtime/trace.FlightRecorder: Production trace capture on-demand
- Container-aware GOMAXPROCS: Auto-detects cgroup limits
- GreenTea GC: Experimental, lower latency (GOEXPERIMENT=greenteagc)
References
- PATTERNS.md - Detailed code patterns
- TESTING.md - Testing strategies with testify/mockery
- CLI.md - CLI application patterns
Tooling
go build ./... # Build
go test -race ./... # Test with race detector
golangci-lint run # Lint
mockery --all # Generate mocks---
Gotchas
- `nil` channel sends/receives block forever; closed channel receives return zero value immediately —
selectwith a nil channel case disables that case, useful pattern but easy to do accidentally. - `defer` captures arguments at the call site, not at execution —
defer fmt.Println(time.Now())captures NOW, not the deferred time. - Pre-Go 1.22 for-loop variable capture closures over ONE variable across all iterations — the goroutine-in-loop bug. Go 1.22 changed semantics; old habits create subtle bugs in mixed-version code.
- `errors.Is` walks `Unwrap()` chains, BUT if a wrapped error implements `Is(target error) bool` itself, that custom Is wins over walking — confusing when migrating from xerrors.
- `sync.Pool` items can be GC'd between `Get` and the next `Put` — never rely on a Pool to retain state.
Go CLI Patterns
Framework Choice: Cobra vs urfave/cli
Both are production-ready. Choose based on project needs.
| Aspect | Cobra | urfave/cli |
|---|---|---|
| Community | Larger (kubectl, hugo, gh) | Solid (many cloud tools) |
| Code gen | cobra-cli scaffolding | Manual |
| Completions | Built-in shell completions | Add-on |
| Docs gen | Auto man pages, markdown | Manual |
| Structure | File-per-command scales well | Single file works |
| Learning | More boilerplate initially | Simpler start |
| Env vars | Viper integration | Built-in EnvVars field |
Cobra fits well:
- Complex CLIs with many subcommands
- Need shell completions and doc generation
- kubectl/gh style patterns
urfave/cli fits well:
- Simpler CLIs, fewer commands
- Built-in env var support needed
- Less boilerplate preferred
Cobra Patterns
Project Structure
cmd/
├── root.go
├── process.go
├── list.go
└── version.go
main.goRoot Command
// cmd/root.go
var (
cfgFile string
verbose bool
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "A helpful CLI tool",
}
func Execute() error {
return rootCmd.Execute()
}
func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
}Subcommand
// cmd/process.go
var processCmd = &cobra.Command{
Use: "process [file]",
Short: "Process input files",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
input := args[0]
output, _ := cmd.Flags().GetString("output")
dryRun, _ := cmd.Flags().GetBool("dry-run")
if dryRun {
fmt.Printf("Would process %s -> %s\n", input, output)
return nil
}
return process(input, output)
},
}
func init() {
processCmd.Flags().StringP("output", "o", "output.json", "output file")
processCmd.Flags().Bool("dry-run", false, "preview without applying")
rootCmd.AddCommand(processCmd)
}Shell Completions
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish]",
Short: "Generate shell completions",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
return rootCmd.GenZshCompletion(os.Stdout)
case "fish":
return rootCmd.GenFishCompletion(os.Stdout, true)
}
return fmt.Errorf("unknown shell: %s", args[0])
},
}urfave/cli Patterns
Single File CLI
func main() {
app := &cli.App{
Name: "mytool",
Usage: "A helpful CLI tool",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
EnvVars: []string{"MYTOOL_CONFIG"},
},
&cli.BoolFlag{Name: "verbose"},
},
Commands: []*cli.Command{
{
Name: "process",
Usage: "Process input files",
Flags: []cli.Flag{
&cli.StringFlag{Name: "input", Aliases: []string{"i"}, Required: true},
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Value: "output.json"},
&cli.BoolFlag{Name: "dry-run"},
},
Action: runProcess,
},
},
}
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func runProcess(c *cli.Context) error {
if c.Bool("dry-run") {
fmt.Printf("Would process %s -> %s\n", c.String("input"), c.String("output"))
return nil
}
return process(c.String("input"), c.String("output"))
}Output Formats (Both Frameworks)
type OutputFormat string
const (
FormatTable OutputFormat = "table"
FormatJSON OutputFormat = "json"
FormatCSV OutputFormat = "csv"
)
func printItems(items []Item, format OutputFormat) error {
switch format {
case FormatJSON:
return json.NewEncoder(os.Stdout).Encode(items)
case FormatCSV:
w := csv.NewWriter(os.Stdout)
defer w.Flush()
w.Write([]string{"ID", "Name", "Status"})
for _, item := range items {
w.Write([]string{item.ID, item.Name, item.Status})
}
return nil
default:
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "ID\tNAME\tSTATUS")
for _, item := range items {
fmt.Fprintf(tw, "%s\t%s\t%s\n", item.ID, item.Name, item.Status)
}
return tw.Flush()
}
}Embedded Data
//go:embed templates/*
var templates embed.FS
//go:embed data/defaults.json
var defaultsJSON []byteBuild Information
var (
version = "dev"
commit = "none"
buildDate = "unknown"
)
// Build with:
// go build -ldflags "-X main.version=1.0.0 -X main.commit=$(git rev-parse HEAD)"Exit Codes
const (
ExitOK = 0
ExitError = 1
ExitUsageError = 2
)
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(ExitError)
}
}Logging
func setupLogger(verbose bool) *slog.Logger {
level := slog.LevelInfo
if verbose {
level = slog.LevelDebug
}
if isTerminal() {
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level}))
}
return slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level}))
}
func isTerminal() bool {
fi, _ := os.Stdout.Stat()
return (fi.Mode() & os.ModeCharDevice) != 0
}Go Patterns Reference
Project Structure
cmd/ # Entry points (main.go per binary)
internal/ # Private application code
├── domain/ # Business entities and logic
├── service/ # Business operations
├── handler/ # HTTP/gRPC handlers
└── repo/ # Data access
pkg/ # Public libraries (rarely needed)Interfaces
Define at Consumer
Interfaces belong where they're USED, not where they're implemented.
// service/user.go - consumer defines what it needs
type UserStore interface {
Get(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
type Service struct {
store UserStore
}// repo/postgres.go - implementation returns concrete type
type PostgresStore struct{ db *sql.DB }
func (s *PostgresStore) Get(ctx context.Context, id string) (*User, error) {
// ...
}Keep Interfaces Focused
Small interfaces are better. Prefer composition.
// Good: focused interfaces
type Reader interface { Read(ctx context.Context, id string) (*Entity, error) }
type Writer interface { Write(ctx context.Context, e *Entity) error }
type Deleter interface { Delete(ctx context.Context, id string) error }
// Compose when needed
type ReadWriter interface {
Reader
Writer
}// Bad: kitchen sink interface
type Repository interface {
Get(ctx context.Context, id string) (*Entity, error)
List(ctx context.Context, filter Filter) ([]*Entity, error)
Save(ctx context.Context, e *Entity) error
Delete(ctx context.Context, id string) error
Archive(ctx context.Context, id string) error
// ... 10 more methods
}Type Visibility
Prefer private (lowercase) types unless they need external access.
// internal/service/user.go
// userService is private - exposed via interface or constructor
type userService struct {
store UserStore
cache cache
}
// NewUserService exposes a public interface, not the struct
func NewUserService(store UserStore) *userService {
return &userService{store: store}
}
// config is private, no reason to export
type config struct {
timeout time.Duration
retries int
}Public types only when:
- Part of your API contract
- Needed by external packages
- Used in function signatures that must be public
Comments
Write comments that explain WHY, not WHAT. Avoid obvious comments.
// Bad: obvious
// GetUser gets a user
func GetUser(id string) (*User, error)
// incrementCounter increments the counter by 1
counter++
// Good: explains non-obvious behavior
// GetUser returns ErrNotFound if user doesn't exist, not nil.
func GetUser(id string) (*User, error)
// Batch size tuned for Postgres query planner; larger batches cause seq scans.
const batchSize = 100Package comments are useful:
// Package ratelimit provides token bucket rate limiting with
// automatic backpressure and circuit breaking for HTTP clients.
package ratelimitDesign Patterns
Functional Options
type Option func(*Config)
func WithTimeout(d time.Duration) Option {
return func(c *Config) { c.Timeout = d }
}
func New(opts ...Option) *Client {
cfg := &Config{Timeout: 30 * time.Second}
for _, opt := range opts {
opt(cfg)
}
return &Client{cfg: cfg}
}Context Propagation
func (s *service) Process(ctx context.Context, req Request) error {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
data, err := s.fetch(ctx, req.ID)
if err != nil {
return fmt.Errorf("fetch: %w", err)
}
return s.store(ctx, data)
}Graceful Shutdown
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)
}Error Handling
Wrap with Context
if err := db.QueryRow(ctx, query, id).Scan(&user); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("query user %s: %w", id, err)
}Sentinel Errors
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
if errors.Is(err, ErrNotFound) {
return http.StatusNotFound
}Concurrency
Worker Pool
func ProcessBatch(ctx context.Context, items []Item, workers int) error {
g, ctx := errgroup.WithContext(ctx)
ch := make(chan Item)
for i := 0; i < workers; i++ {
g.Go(func() error {
for item := range ch {
if err := process(ctx, item); err != nil {
return err
}
}
return nil
})
}
g.Go(func() error {
defer close(ch)
for _, item := range items {
select {
case ch <- item:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
return g.Wait()
}Configuration
type config struct {
Port int `env:"PORT" envDefault:"8080"`
DatabaseURL string `env:"DATABASE_URL,required"`
Timeout time.Duration `env:"TIMEOUT" envDefault:"30s"`
}
func loadConfig() (*config, error) {
var cfg config
if err := env.Parse(&cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
return &cfg, nil
}Style
- Early returns reduce nesting
- Meaningful names:
userIDnotid,cfgnotc - Short names in small scopes:
for i, v := range items - No stuttering:
user.Namenotuser.UserName - Group imports: stdlib, external, internal
Go Testing Reference
Frameworks
- testify: Assertions (
require,assert) - mockery: Interface mock generation with EXPECT pattern
go install github.com/vektra/mockery/v2@latest
mockery --all --keeptree
mockery --name=UserStore --dir=internal/servicerequire vs assert
require stops test immediately on failure (t.FailNow()) — use for prerequisites. assert logs failure but continues (t.Fail()) — use for independent checks.
func TestUser(t *testing.T) {
user, err := GetUser("123")
// Prerequisites: must pass or test is meaningless
require.NoError(t, err)
require.NotNil(t, user)
// Independent assertions: collect all failures
assert.Equal(t, "123", user.ID)
assert.Equal(t, "test@example.com", user.Email)
assert.True(t, user.IsActive)
}When to use require:
- Nil checks before accessing fields/methods
- Error checks when success is required to proceed
- Setup validation (db connection, file exists)
- Any precondition where failure makes remaining assertions meaningless
When to use assert:
- Multiple property checks on same object
- Validating several independent conditions
- When you want to see all failures in one run
Never call require/assert from goroutines — must be called from test goroutine.
Table-Driven Tests
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr string
}{
{"valid", "user@example.com", ""},
{"empty", "", "email required"},
{"no_at", "invalid", "invalid format"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if tt.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tt.wantErr)
}
})
}
}Mocking with Mockery
Generate mocks with EXPECT pattern (typesafe):
//go:generate mockery --name=UserStore
type UserStore interface {
Get(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}func TestService_GetUser(t *testing.T) {
store := mocks.NewUserStore(t)
svc := NewService(store)
expected := &User{ID: "123", Name: "Test"}
store.EXPECT().
Get(mock.Anything, "123").
Return(expected, nil)
user, err := svc.GetUser(context.Background(), "123")
require.NoError(t, err)
assert.Equal(t, expected, user)
}
func TestService_CreateUser(t *testing.T) {
store := mocks.NewUserStore(t)
svc := NewService(store)
store.EXPECT().
Save(mock.Anything, mock.MatchedBy(func(u *User) bool {
return u.Email == "test@example.com"
})).
Return(nil)
err := svc.CreateUser(context.Background(), "test@example.com")
require.NoError(t, err)
}
func TestService_GetUser_NotFound(t *testing.T) {
store := mocks.NewUserStore(t)
svc := NewService(store)
store.EXPECT().
Get(mock.Anything, "unknown").
Return(nil, ErrNotFound)
_, err := svc.GetUser(context.Background(), "unknown")
require.ErrorIs(t, err, ErrNotFound)
}HTTP Handler Tests
func TestHandler_CreateUser(t *testing.T) {
svc := mocks.NewUserService(t)
h := NewHandler(svc)
svc.EXPECT().
CreateUser(mock.Anything, mock.AnythingOfType("CreateUserRequest")).
Return(&User{ID: "123"}, nil)
body := `{"name": "Test", "email": "test@example.com"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.CreateUser(rec, req)
assert.Equal(t, http.StatusCreated, rec.Code)
}Go 1.25: testing/synctest
Deterministic concurrent testing:
func TestRetryWithBackoff(t *testing.T) {
synctest.Run(func() {
attempts := 0
client := &RetryClient{
Do: func() error {
attempts++
if attempts < 3 {
return errors.New("temporary")
}
return nil
},
MaxRetries: 3,
Backoff: time.Second,
}
err := client.Execute()
require.NoError(t, err)
assert.Equal(t, 3, attempts)
})
}Integration Tests
//go:build integration
func TestDatabase_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
db, cleanup := setupTestDB(t)
defer cleanup()
store := NewPostgresStore(db)
user := &User{Name: "Test", Email: "test@example.com"}
err := store.Save(context.Background(), user)
require.NoError(t, err)
got, err := store.Get(context.Background(), user.ID)
require.NoError(t, err)
assert.Equal(t, user.Name, got.Name)
}Benchmarks
func BenchmarkProcess(b *testing.B) {
data := generateTestData(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Process(data)
}
}
func BenchmarkProcess_Parallel(b *testing.B) {
data := generateTestData(1000)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Process(data)
}
})
}Test Fixtures
func loadFixture(t *testing.T, name string) []byte {
t.Helper()
data, err := os.ReadFile(filepath.Join("testdata", name))
require.NoError(t, err)
return data
}Coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
go tool cover -func=coverage.out | grep totalGuidelines
- Test behavior, not implementation
- One logical assertion per test case
- Use
t.Parallel()for independent tests - Prefer table-driven for multiple cases
- Keep tests focused and readable