Skill
Community

go-conventions

Install
1
Install the plugin
$
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-golang

Want just this skill?

Then install: npx claudepluginhub u/[userId]/[slug]

Description

This skill should be used when working on Go projects, writing Go code, running Go tests, managing Go dependencies, or reviewing Go code.

Tool Access

This skill uses the workspace's default tool permissions.

Skill Content

Go Conventions and Idiomatic Patterns

This skill defines comprehensive conventions for writing idiomatic Go code following community best practices and the official Go team guidelines.

Error Handling

Always Check Errors

Every error must be checked. Ignoring errors leads to subtle bugs and production failures.

// CORRECT: Check all errors
file, err := os.Open("config.json")
if err != nil {
    return fmt.Errorf("failed to open config: %w", err)
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
    return fmt.Errorf("failed to read config: %w", err)
}
// WRONG: Ignoring errors
file, _ := os.Open("config.json")
data, _ := io.ReadAll(file)

Error Wrapping with fmt.Errorf and %w

Wrap errors to add context while preserving the error chain for errors.Is and errors.As.

// CORRECT: Wrap errors with %w
func loadUser(id string) (*User, error) {
    data, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("load user %s: %w", id, err)
    }
    return parseUser(data)
}
// WRONG: Losing error chain with %v
func loadUser(id string) (*User, error) {
    data, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("load user %s: %v", id, err)
    }
    return parseUser(data)
}

Sentinel Errors

Define sentinel errors as package-level variables for expected error conditions.

// CORRECT: Sentinel errors for known conditions
var (
    ErrNotFound     = errors.New("resource not found")
    ErrUnauthorized = errors.New("unauthorized access")
    ErrInvalidInput = errors.New("invalid input")
)

func GetUser(id string) (*User, error) {
    if id == "" {
        return nil, ErrInvalidInput
    }
    user, err := db.Find(id)
    if err != nil {
        return nil, err
    }
    if user == nil {
        return nil, ErrNotFound
    }
    return user, nil
}
// WRONG: Creating new error instances
func GetUser(id string) (*User, error) {
    if id == "" {
        return nil, errors.New("invalid input")  // Not comparable
    }
    return db.Find(id)
}

Use errors.Is and errors.As

Check for specific errors using errors.Is and extract error types with errors.As.

// CORRECT: Using errors.Is and errors.As
func handleError(err error) {
    if errors.Is(err, ErrNotFound) {
        log.Println("resource not found")
        return
    }

    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        log.Printf("validation failed: %v", validationErr.Fields)
        return
    }

    log.Printf("unexpected error: %v", err)
}
// WRONG: Direct comparison loses wrapped context
func handleError(err error) {
    if err == ErrNotFound {  // Fails if error is wrapped
        log.Println("not found")
    }
}

Return Errors, Do Not Panic

Panics should only be used for programming errors, not for expected error conditions.

// CORRECT: Return errors for expected failures
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}
// WRONG: Panic for expected conditions
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")  // Don't panic on expected errors
    }
    return a / b
}

Code Style and Formatting

Use gofumpt for Formatting

Always format code with gofumpt, which is stricter than gofmt.

# Install gofumpt
go install mvdan.cc/gofumpt@latest

# Format all files
gofumpt -l -w .
// CORRECT: gofumpt style
package main

import (
    "context"
    "fmt"
)

type Config struct {
    Host string
    Port int
}

Run golangci-lint

Use golangci-lint with a comprehensive configuration for static analysis.

# Install golangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Run linter
golangci-lint run ./...
# .golangci.yml recommended configuration
linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
    - gofumpt
    - revive
    - gosec

Prefer Standard Library

Use the standard library when possible before adding external dependencies.

// CORRECT: Use stdlib encoding/json
import "encoding/json"

func parseConfig(data []byte) (*Config, error) {
    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}
// WRONG: Unnecessary dependency for basic JSON
import "github.com/goccy/go-json"  // Only if performance critical

Context First Parameter

Always pass context.Context as the first parameter in functions that may block or need cancellation.

// CORRECT: Context as first parameter
func FetchUser(ctx context.Context, id string) (*User, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", "/users/"+id, nil)
    if err != nil {
        return nil, err
    }

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var user User
    if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
        return nil, err
    }
    return &user, nil
}
// WRONG: Context not first or missing
func FetchUser(id string, ctx context.Context) (*User, error) {  // Wrong order
    // ...
}

func FetchUser(id string) (*User, error) {  // Missing context
    req, _ := http.NewRequest("GET", "/users/"+id, nil)
    // ...
}

Short Receiver Names

Use short, consistent receiver names (one or two letters), typically the first letter of the type.

// CORRECT: Short receiver names
type User struct {
    ID   string
    Name string
}

func (u *User) SetName(name string) {
    u.Name = name
}

func (u *User) Validate() error {
    if u.ID == "" {
        return errors.New("ID required")
    }
    return nil
}
// WRONG: Long or inconsistent receiver names
func (user *User) SetName(name string) {  // Too long
    user.Name = name
}

func (this *User) Validate() error {  // Don't use "this" or "self"
    return nil
}

Verb-er Interface Naming

Name interfaces with an -er suffix describing their behavior.

// CORRECT: Verb-er interface names
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type UserFetcher interface {
    FetchUser(ctx context.Context, id string) (*User, error)
}
// WRONG: Poor interface naming
type ReaderInterface interface {  // Redundant "Interface"
    Read(p []byte) (n int, err error)
}

type IReader interface {  // Don't use "I" prefix
    Read(p []byte) (n int, err error)
}

Unexported by Default

Start with unexported types and functions. Only export what consumers need.

// CORRECT: Unexported internals, exported API
package config

type Config struct {  // Exported
    settings map[string]string  // Unexported field
}

func Load(path string) (*Config, error) {  // Exported
    return parseFile(path)
}

func parseFile(path string) (*Config, error) {  // Unexported
    // ...
    return &Config{settings: make(map[string]string)}, nil
}
// WRONG: Over-exporting internals
type Config struct {
    Settings map[string]string  // Don't expose internal state
}

func ParseFile(path string) (*Config, error) {  // Don't export if only used internally
    // ...
}

Iota Pattern for Constants

Use iota for enumerated constants with clear zero values.

// CORRECT: iota with explicit zero value
type Status int

const (
    StatusUnknown Status = iota  // 0 is explicit unknown/unset
    StatusPending
    StatusActive
    StatusInactive
)

func (s Status) String() string {
    switch s {
    case StatusPending:
        return "pending"
    case StatusActive:
        return "active"
    case StatusInactive:
        return "inactive"
    default:
        return "unknown"
    }
}
// WRONG: Missing zero value consideration
const (
    StatusActive Status = iota  // 0 is "active"? Unclear
    StatusInactive
)

Struct Embedding for Composition

Use struct embedding to compose behavior, not for inheritance.

// CORRECT: Embedding for composition
type Logger interface {
    Log(msg string)
}

type Service struct {
    Logger  // Embedded interface
    db      *sql.DB
}

func (s *Service) Process(ctx context.Context) error {
    s.Log("starting process")  // Delegated to embedded Logger
    return s.db.PingContext(ctx)
}
// WRONG: Embedding just to avoid typing
type Service struct {
    *sql.DB  // Don't embed concrete types unless clear benefit
}

Tooling and Development Workflow

Use Task (Taskfile)

Prefer Task over Makefiles for Go projects. Task uses YAML and has better cross-platform support.

# Taskfile.yml
version: '3'

tasks:
  build:
    desc: Build the application
    cmds:
      - go build -o bin/app ./cmd/app

  test:
    desc: Run tests
    cmds:
      - go test -v -race -coverprofile=coverage.out ./...

  lint:
    desc: Run linters
    cmds:
      - golangci-lint run ./...
      - gofumpt -l -w .

  tidy:
    desc: Tidy dependencies
    cmds:
      - go mod tidy
      - go mod verify

  vuln:
    desc: Check for vulnerabilities
    cmds:
      - govulncheck ./...

  default:
    desc: Run all checks
    cmds:
      - task: lint
      - task: test
      - task: build
# Run tasks
task          # Run default task
task test     # Run specific task
task -l       # List all tasks

gofumpt for Stricter Formatting

Use gofumpt instead of gofmt for more opinionated formatting.

# Format and check
gofumpt -l -w .

# In CI, check formatting
gofumpt -l . | grep . && exit 1 || exit 0

golangci-lint Configuration

Configure golangci-lint with appropriate linters for your project.

# .golangci.yml
linters-settings:
  errcheck:
    check-blank: true
  govet:
    enable-all: true
  revive:
    rules:
      - name: exported
        severity: warning

linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
    - gofumpt
    - revive
    - gosec
    - misspell
    - unconvert
    - unparam

run:
  timeout: 5m
  tests: true

go mod tidy

Always run go mod tidy to clean up dependencies.

# Clean dependencies
go mod tidy

# Verify checksums
go mod verify

# Explain why dependency is needed
go mod why github.com/pkg/errors

govulncheck for Security

Regularly check for known vulnerabilities in dependencies.

# Install govulncheck
go install golang.org/x/vuln/cmd/govulncheck@latest

# Check for vulnerabilities
govulncheck ./...

# In CI
govulncheck -test ./...

go generate Hygiene

Generated File Headers

Always add clear headers to generated files indicating they are auto-generated.

// CORRECT: Clear generated file header
// Code generated by generate.go; DO NOT EDIT.

package models

type User struct {
    ID   string
    Name string
}
# Generate command
go generate ./...

Commit Generated Files

Commit generated files to version control for reproducible builds.

# Generate before commit
go generate ./...
git add .
git commit -m "chore: regenerate mocks and code"
# WRONG: Don't ignore generated files in most cases
# *_generated.go  # Usually should be committed

CI Verification

Verify generated files are up-to-date in CI.

# CI script to verify generation
go generate ./...
git diff --exit-code || (echo "Generated files out of date" && exit 1)
# GitHub Actions example
- name: Verify generated files
  run: |
    go generate ./...
    git diff --exit-code

Existing Repository Compatibility

Respect Established Toolchain

When contributing to existing repositories, respect their established conventions.

// Check existing patterns
// - Look at go.mod for Go version
// - Check for Makefile or Taskfile.yml
// - Review .golangci.yml configuration
// - Follow existing error handling patterns
// - Match existing test structure
# Discover project conventions
cat go.mod                # Check Go version
cat Taskfile.yml          # Or Makefile
cat .golangci.yml         # Linter config
ls cmd/                   # Entry points
ls internal/              # Internal packages

Match Existing Code Style

Analyze existing code patterns before making changes.

// If project uses:
// - Custom logger -> use it
// - Specific error wrapping -> follow it
// - Test helpers -> reuse them
// - Mocking library -> use same library

// CORRECT: Follow project patterns
// If project uses zerolog:
import "github.com/rs/zerolog/log"

func Process() {
    log.Info().Msg("processing")  // Match existing style
}
// WRONG: Introducing new patterns without discussion
import "log/slog"  // Don't introduce new logger if project uses zerolog

func Process() {
    slog.Info("processing")  // Inconsistent with project
}

Package Organization

Internal Package for Private Code

Use internal/ directory for code that should not be imported by external modules.

myapp/
  cmd/
    myapp/
      main.go
  internal/
    service/
      user.go
    repository/
      postgres.go
  pkg/
    client/
      client.go
  go.mod
// internal/service/user.go - Cannot be imported outside module
package service

type UserService struct{}

cmd Directory for Entry Points

Place main packages in cmd/ directory with subdirectories for each binary.

cmd/
  server/
    main.go
  worker/
    main.go
  migrate/
    main.go
// CORRECT: cmd/server/main.go
package main

import "myapp/internal/service"

func main() {
    svc := service.New()
    svc.Start()
}

Naming Conventions

Use MixedCaps

Use MixedCaps or mixedCaps, not underscores, for multi-word names.

// CORRECT: MixedCaps naming
type UserService struct{}

func (s *UserService) GetUserByID(id string) (*User, error) {
    return s.repo.FindByID(id)
}

var errNotFound = errors.New("not found")
// WRONG: Underscores in names
type User_Service struct{}  // Wrong

func (s *User_Service) Get_User_By_ID(id string) (*User, error) {  // Wrong
    return nil, nil
}

var err_not_found = errors.New("not found")  // Wrong

Package Names

Package names should be short, concise, and lowercase without underscores.

// CORRECT: Good package names
package user
package httputil
package ioutil
// WRONG: Poor package names
package user_service  // No underscores
package userService   // No mixed caps
package utils         // Too generic

Concurrency Patterns

Always Close Channels from Sender

The sender should close channels, not the receiver.

// CORRECT: Sender closes channel
func produce(n int) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)  // Sender closes
        for i := 0; i < n; i++ {
            ch <- i
        }
    }()
    return ch
}

func consume() {
    for v := range produce(10) {  // Receiver just ranges
        fmt.Println(v)
    }
}
// WRONG: Receiver closing channel
func consume(ch chan int) {
    for v := range ch {
        fmt.Println(v)
    }
    close(ch)  // Wrong: receiver shouldn't close
}

Use sync.WaitGroup for Goroutine Coordination

Use sync.WaitGroup to wait for multiple goroutines to complete.

// CORRECT: WaitGroup for coordination
func processBatch(items []Item) error {
    var wg sync.WaitGroup
    errCh := make(chan error, len(items))

    for _, item := range items {
        wg.Add(1)
        go func(item Item) {
            defer wg.Done()
            if err := process(item); err != nil {
                errCh <- err
            }
        }(item)
    }

    wg.Wait()
    close(errCh)

    for err := range errCh {
        if err != nil {
            return err
        }
    }
    return nil
}

Context for Cancellation

Use context for cancellation and timeout control in concurrent operations.

// CORRECT: Context-aware goroutines
func worker(ctx context.Context, jobs <-chan Job) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case job, ok := <-jobs:
            if !ok {
                return nil
            }
            if err := processJob(ctx, job); err != nil {
                return err
            }
        }
    }
}

Performance Considerations

Preallocate Slices When Size Known

Preallocate slices when the final size is known to avoid reallocations.

// CORRECT: Preallocate slice
func transform(input []int) []int {
    result := make([]int, 0, len(input))  // Preallocate capacity
    for _, v := range input {
        result = append(result, v*2)
    }
    return result
}
// WRONG: No preallocation
func transform(input []int) []int {
    var result []int  // Will reallocate multiple times
    for _, v := range input {
        result = append(result, v*2)
    }
    return result
}

Use strings.Builder for String Concatenation

Use strings.Builder for efficient string building instead of concatenation.

// CORRECT: strings.Builder
func buildMessage(parts []string) string {
    var b strings.Builder
    b.Grow(len(parts) * 10)  // Estimate size
    for i, part := range parts {
        if i > 0 {
            b.WriteString(", ")
        }
        b.WriteString(part)
    }
    return b.String()
}
// WRONG: String concatenation
func buildMessage(parts []string) string {
    msg := ""
    for i, part := range parts {
        if i > 0 {
            msg += ", "  // Creates new string each time
        }
        msg += part
    }
    return msg
}

Documentation

Document All Exported Items

All exported functions, types, constants, and variables must have documentation comments.

// CORRECT: Documented exports
// User represents a user in the system.
type User struct {
    // ID is the unique identifier for the user.
    ID string
    // Name is the user's display name.
    Name string
}

// NewUser creates a new User with the given ID and name.
func NewUser(id, name string) *User {
    return &User{ID: id, Name: name}
}
// WRONG: Missing documentation
type User struct {  // No doc comment
    ID   string
    Name string
}

func NewUser(id, name string) *User {  // No doc comment
    return &User{ID: id, Name: name}
}

Package Documentation

Every package should have a package comment describing its purpose.

// CORRECT: Package documentation
// Package user provides user management functionality.
//
// It includes user creation, validation, and persistence operations.
// Use NewService to create a user service instance.
package user

This skill ensures Go code follows community best practices, is maintainable, performant, and idiomatic. Apply these rules consistently across all Go projects.

Stats
Stars0
Forks0
Last CommitFeb 9, 2026

Similar Skills