From unicorn-team
Guides Go development with idiomatic patterns for error handling, interfaces, and concurrency. Activates on project setup, module management, testing, and tooling configuration.
How this skill is triggered — by the user, by Claude, or both
Slash command
/unicorn-team:goThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
<!-- Last reviewed: 2026-03 -->
Go uses explicit error returns, not exceptions. Every error is a value you handle at the call site.
// Return errors, don't panic
func ParseConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &cfg, nil
}
// Sentinel errors for callers to check
var (
ErrNotFound = errors.New("not found")
ErrForbidden = errors.New("forbidden")
)
// Custom error types for rich context
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}
// Callers use errors.Is / errors.As
if errors.Is(err, ErrNotFound) { /* handle */ }
var ve *ValidationError
if errors.As(err, &ve) { /* access ve.Field */ }
See references/error-handling.md for wrapping strategies, panic/recover, error handling in goroutines, domain error design.
Go interfaces are satisfied implicitly -- no implements keyword. Define them where they're consumed, not where they're implemented.
// Small interfaces at the consumer site
type UserStore interface {
GetUser(ctx context.Context, id string) (*User, error)
}
// Accept interfaces, return structs
func NewUserService(store UserStore) *UserService {
return &UserService{store: store}
}
// Common stdlib interfaces to know
// io.Reader, io.Writer -- streaming data
// fmt.Stringer -- string representation
// sort.Interface -- custom sorting
// encoding.BinaryMarshaler -- serialization
// context.Context -- cancellation and deadlines
// http.Handler -- HTTP request handling
| Guideline | Why |
|---|---|
| 1-3 method interfaces | Easier to implement, compose, mock |
| Define at consumer | Decouples packages, avoids import cycles |
| Accept interface, return struct | Callers get flexibility, producers stay concrete |
| Embed for composition | io.ReadWriter = io.Reader + io.Writer |
// Goroutines + channels for concurrent work
func FetchAll(ctx context.Context, urls []string) ([]Result, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]Result, len(urls))
for i, url := range urls {
g.Go(func() error {
res, err := fetch(ctx, url)
if err != nil {
return err
}
results[i] = res // safe: each goroutine owns its index
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
// Context for cancellation and timeouts
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Select for multiplexing channels
select {
case msg := <-msgCh:
handle(msg)
case <-ctx.Done():
return ctx.Err()
}
See references/concurrency-patterns.md for worker pools, fan-out/fan-in, pipelines, sync primitives, context propagation.
// Table-driven tests -- the Go standard
func TestParseSize(t *testing.T) {
tests := []struct {
name string
input string
want int64
wantErr bool
}{
{name: "bytes", input: "100B", want: 100},
{name: "kilobytes", input: "2KB", want: 2048},
{name: "empty", input: "", wantErr: true},
{name: "invalid", input: "abc", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseSize(tt.input)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("ParseSize(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
// HTTP handler testing
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()
HealthHandler(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
Coverage target: 80%+
go test -cover -coverprofile=coverage.out ./...
go tool cover -func=coverage.out # summary
go tool cover -html=coverage.out # visual report
See references/testing-go.md for subtests, benchmarks, fuzz testing, testify, httptest patterns, test helpers.
go mod init github.com/org/project # initialize
go mod tidy # sync deps
go mod vendor # vendored deps (optional)
go get github.com/pkg/[email protected] # add/update dep
Runs 50+ linters in parallel. Single tool replaces go vet, staticcheck, errcheck, gosec, and more.
# .golangci.yml
linters:
enable:
- errcheck # unchecked errors
- govet # suspicious constructs
- staticcheck # advanced analysis
- unused # unused code
- gosimple # simplifications
- ineffassign # ineffective assignments
- gocritic # opinionated checks
- gosec # security issues
- errname # error naming conventions
- exhaustive # enum exhaustiveness
linters-settings:
govet:
enable-all: true
gocritic:
enabled-tags: [diagnostic, style, performance]
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
golangci-lint run ./...
See references/tooling-config.md for complete linter configs, Makefile patterns, CI setup, build tags.
// Struct with tags for serialization
type User struct {
ID string `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Email string `json:"email" db:"email"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// Constructor function (not a Go "constructor" -- just convention)
func NewUser(name, email string) *User {
return &User{
ID: uuid.NewString(),
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
// Value receiver: doesn't modify receiver, safe on copies
func (u User) DisplayName() string {
return fmt.Sprintf("%s <%s>", u.Name, u.Email)
}
// Pointer receiver: modifies receiver or is large
func (u *User) SetEmail(email string) error {
if !strings.Contains(email, "@") {
return &ValidationError{Field: "email", Message: "invalid format"}
}
u.Email = email
return nil
}
| Use | Receiver | Why |
|---|---|---|
| Read-only, small struct | Value (u User) | No mutation, safe copy |
| Mutates state | Pointer (u *User) | Changes visible to caller |
| Large struct (>3 fields) | Pointer (u *User) | Avoid copy overhead |
| Implements interface with pointer methods | Pointer (u *User) | Consistency required |
myproject/
├── cmd/
│ └── myapp/
│ └── main.go # entry point, wiring only
├── internal/ # private to this module
│ ├── server/ # HTTP server setup
│ ├── user/ # user domain logic
│ └── storage/ # database layer
├── pkg/ # importable by other projects (use sparingly)
├── go.mod
├── go.sum
├── Makefile
└── .golangci.yml
See references/project-structure.md for multi-binary repos, dependency injection, config patterns, build tags.
| Anti-pattern | Fix |
|---|---|
panic() for expected errors | Return error |
Ignoring errors _ = f() | Handle or explicitly document why safe |
interface{} / any everywhere | Use generics (1.18+) or specific types |
| Goroutine leak (no exit path) | Use context.Context + select |
| Shared state without sync | sync.Mutex, channels, or atomic |
init() with side effects | Explicit initialization in main() |
| Giant interfaces (>5 methods) | Split into focused 1-3 method interfaces |
| Package-level mutable state | Dependency injection |
| Premature channel/goroutine use | Start sequential, add concurrency when needed |
log.Fatal in library code | Return errors, let caller decide |
| Naked returns in long functions | Named returns only for short functions or godoc |
Missing defer for cleanup | defer f.Close() immediately after open |
| Element | Convention | Example |
|---|---|---|
| Package | short, lowercase, singular | user, http, json |
| Exported function | PascalCase, verb-noun | ParseConfig, NewServer |
| Unexported function | camelCase | validateInput, buildQuery |
| Interface (1 method) | Method + "er" | Reader, Stringer, Handler |
| Interface (multi) | Descriptive noun | UserStore, EventBus |
| Error variable | Err + description | ErrNotFound, ErrTimeout |
| Error type | Description + Error | ValidationError, TimeoutError |
| Constants | PascalCase (exported) or camelCase | MaxRetries, defaultTimeout |
| Acronyms | All caps | HTTPServer, userID, xmlParser |
# 1. Initialize module
go mod init github.com/org/project
# 2. Create directory structure
mkdir -p cmd/myapp internal/{server,storage}
# 3. Install tooling
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# 4. Create .golangci.yml (see Tooling section)
# 5. Create Makefile
# make build, make test, make lint, make run
# 6. Full quality check
go vet ./... && golangci-lint run ./... && go test -race -cover ./...
go build ./... # Build all packages
go test -v -race -cover ./... # Test with race detector + coverage
go test -run TestSpecific ./internal/user/ # Run specific test
go vet ./... # Static analysis
golangci-lint run ./... # Lint (50+ linters)
go mod tidy # Sync dependencies
npx claudepluginhub aj-geddes/unicorn-team --plugin unicorn-teamIdiomatic Go patterns for concurrency (goroutines, errgroup, channels), error handling (sentinel, wrapping, custom types), project structure (mod, workspace, vendor), and testing (table-driven tests).
Go language conventions, idioms, and toolchain. Invoke when task involves any interaction with Go code — writing, reviewing, refactoring, debugging, or understanding Go projects.
Provides idiomatic Go patterns and best practices for error handling, concurrency like worker pools, simplicity, zero values, and interfaces. Activates for writing, reviewing, refactoring, or designing Go code.