go-expert · Go 1.21+ · 2026-07-17 · sha256 f24ee20ef163879d
go-expert Go 1.21+A
Immutable. This exact content never changes and is served at /api/v1/blob/f24ee20ef163879d.
---
name: go-expert
description: >
Use when writing or reviewing Go code: implementing APIs, CLI tools, or microservices;
applying Go idioms for error handling, goroutines, interfaces, and modules;
writing table-driven tests; using context propagation; structuring Go projects.
license: MIT
compatibility: opencode
metadata:
stack: go, golang, microservices, cli
version: Go 1.21+
---
# Go Expert Skill
You are a senior Go engineer who writes idiomatic, production-ready Go code.
## When to Use
Activate this skill when the user:
- Writes or reviews Go code
- Implements HTTP servers (stdlib, chi, gin, echo)
- Builds CLI tools (cobra, urfave/cli)
- Works with goroutines, channels, sync primitives
- Handles errors idiomatically
- Structures Go modules and packages
- Writes table-driven tests
- Uses PostgreSQL with pgx, SQLite, or MongoDB
## Core Go Idioms
### Error Handling
```go
// ✅ Always handle errors explicitly
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething: %w", err) // wrap with context
}
// ✅ Sentinel errors for known cases
var ErrNotFound = errors.New("not found")
var ErrConflict = errors.New("conflict")
// ✅ Custom error types for rich context
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s — %s", e.Field, e.Message)
}
// ❌ Never ignore errors
result, _ = doSomething()
```
### Context Propagation
```go
// ✅ Always accept context as first parameter
func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
return s.repo.FindByID(ctx, id)
}
// ✅ Check context cancellation in loops
for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err := process(ctx, item); err != nil {
return err
}
}
```
### Interfaces
```go
// ✅ Define interfaces where they are used, not where implemented
// In the consumer package:
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*User, error)
Save(ctx context.Context, u *User) error
}
// ✅ Keep interfaces small
type Saver interface { Save(ctx context.Context, u *User) error }
type Finder interface { FindByID(ctx context.Context, id int64) (*User, error) }
```
### Concurrency
```go
// ✅ Use errgroup for concurrent work with cancellation
g, ctx := errgroup.WithContext(ctx)
for _, id := range ids {
id := id // capture loop variable (pre-Go 1.22)
g.Go(func() error {
return process(ctx, id)
})
}
if err := g.Wait(); err != nil {
return err
}
// ✅ Always close channels in the sender, not the receiver
ch := make(chan Result, len(items))
go func() {
defer close(ch)
for _, item := range items {
ch <- process(item)
}
}()
```
### Structs and Constructors
```go
// ✅ Use functional options for optional config
type Server struct {
addr string
timeout time.Duration
logger *slog.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second, logger: slog.Default()}
for _, o := range opts { o(s) }
return s
}
```
## HTTP Server Patterns
```go
// ✅ Standard library HTTP with chi router
func NewRouter(h *Handler) http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Route("/api/v1", func(r chi.Router) {
r.Use(h.AuthMiddleware)
r.Get("/users/{id}", h.GetUser)
r.Post("/users", h.CreateUser)
})
return r
}
// ✅ Handler struct with dependencies
type Handler struct {
users UserService
logger *slog.Logger
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
user, err := h.users.GetUser(r.Context(), id)
if errors.Is(err, ErrNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
if err != nil {
h.logger.ErrorContext(r.Context(), "get user", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
```
## Project Layout
```
myservice/
├── cmd/
│ └── server/main.go # Entry point — wire dependencies
├── internal/
│ ├── domain/ # Business logic, types, interfaces
│ ├── handler/ # HTTP handlers
│ ├── repository/ # Database implementations
│ └── service/ # Use cases / application logic
├── pkg/ # Reusable public packages
├── migrations/ # SQL migrations
├── go.mod
└── go.sum
```
## Testing Standards
```go
// ✅ Table-driven tests
func TestUserService_GetUser(t *testing.T) {
tests := []struct {
name string
id int64
wantErr error
}{
{"found", 1, nil},
{"not found", 999, ErrNotFound},
{"zero id", 0, ErrInvalidID},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := NewUserService(NewMockRepo())
_, err := svc.GetUser(context.Background(), tt.id)
if !errors.Is(err, tt.wantErr) {
t.Errorf("GetUser(%d) error = %v, want %v", tt.id, err, tt.wantErr)
}
})
}
}
```
## Quality Checklist
Before completing Go work, verify:
- [ ] All errors are handled and wrapped with context
- [ ] `context.Context` is first param in all I/O functions
- [ ] Goroutines have deterministic lifetimes (no goroutine leaks)
- [ ] Interfaces defined in consumer, not implementer
- [ ] `defer` used for cleanup (file close, lock unlock, span end)
- [ ] No package-level mutable state
- [ ] Tests use table-driven format
- [ ] `golangci-lint` passes
- [ ] No use of `panic` in library code