Go strict
Twelve Claude Code skills enforcing strict code quality, security, and modern language patterns across TypeScript, Rust, Swift, Go, JavaScript, and PostgreSQL.
npx -y skills add 0xMassi/claude-skills --skill go-strictAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Go coding standards, clean code, and security rules. Use when writing, reviewing, or refactoring Go code. Covers error handling (wrapping with %w), package organization (internal/pkg split), concurrency safety (sync.RWMutex, goroutine lifecycle), Gin handler patterns, input validation, structured logging with zerolog, graceful shutdown, and naming conventions. Derived from production Go services.
SKILL.md
12.9 KB, as published. Nobody here has run it
Go Strict Standard
Rules extracted from 2 production Go services.
CRITICAL: Error Handling
GO-01: Always wrap errors with context
// BAD: no context, impossible to trace
if err != nil {
return err
}
// GOOD: fmt.Errorf with %w for wrapping
store, err := createStore(path)
if err != nil {
return nil, fmt.Errorf("create store: %w", err)
}
// GOOD: multiple levels of context
session, err := validateToken(token)
if err != nil {
return fmt.Errorf("validate session for user %s: %w", userID, err)
}
GO-02: Check errors immediately, never defer
// BAD: error ignored
json.Unmarshal(data, &result)
// GOOD
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
}
GO-03: Drain response bodies for connection reuse
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body) // DRAIN before closing
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
GO-04: Typed errors for API responses
type APIError struct {
Status int `json:"status"`
Message string `json:"message"`
Code string `json:"code,omitempty"`
}
func (e *APIError) Error() string {
return fmt.Sprintf("[%d] %s", e.Status, e.Message)
}
func SendError(c *gin.Context, status int, message string) {
c.JSON(status, APIError{Status: status, Message: message})
}
CRITICAL: Package Organization
GO-05: internal/ for private, pkg/ for reusable
project/
├── cmd/
│ └── server/
│ └── main.go # Entry point only: no logic
├── internal/
│ ├── handlers/ # HTTP handlers (Gin)
│ ├── middleware/ # Auth, rate limit, CORS, validation
│ ├── services/ # Business logic
│ ├── models/ # Data structures
│ ├── config/ # Env var loading
│ └── utils/ # Helpers
├── pkg/
│ ├── redis/ # Reusable Redis client
│ └── sqlite/ # Reusable SQLite wrapper
├── go.mod
└── go.sum
internal/ prevents import from outside the module. Use it.
GO-06: main.go is thin: wire and run
func main() {
cfg := config.Load()
redisClient := redis.NewClient(cfg.RedisURL)
handlers := handlers.NewHandlers(redisClient, cfg)
router := setupRouter(handlers, cfg)
srv := &http.Server{Addr: ":" + cfg.Port, Handler: router}
go func() { srv.ListenAndServe() }()
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
HIGH: Concurrency Safety
GO-07: sync.RWMutex: read lock for reads, write lock for writes
type RateLimiter struct {
mu sync.RWMutex
entries map[string]*entry
}
func (rl *RateLimiter) GetLimiter(key string) *entry {
rl.mu.RLock()
e, ok := rl.entries[key]
rl.mu.RUnlock()
if ok { return e }
rl.mu.Lock()
defer rl.mu.Unlock()
// Double-check after acquiring write lock
if e, ok := rl.entries[key]; ok { return e }
e = &entry{/* ... */}
rl.entries[key] = e
return e
}
Pattern: RLock → check → RUnlock → Lock → double-check → create.
GO-08: Background goroutines with proper lifecycle
func (rl *RateLimiter) evictLoop(interval, maxAge time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
rl.mu.Lock()
now := time.Now()
for key, e := range rl.entries {
if now.Sub(e.lastAccess) > maxAge {
delete(rl.entries, key)
}
}
rl.mu.Unlock()
}
}
// Start with: go rl.evictLoop(5*time.Minute, 10*time.Minute)
GO-09: Graceful shutdown with context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal().Err(err).Msg("server forced to shutdown")
}
HIGH: HTTP Handler Patterns (Gin)
GO-10: Constructor injection for handlers
type Handlers struct {
redis *redis.Client
config *config.Config
sqlite *sqlite.Client
}
func NewHandlers(r *redis.Client, cfg *config.Config, s *sqlite.Client) *Handlers {
return &Handlers{redis: r, config: cfg, sqlite: s}
}
GO-11: Middleware chaining
router := gin.New()
router.Use(middleware.CORS())
router.Use(middleware.InjectConfig(cfg))
// Group with shared auth
internal := router.Group("/", middleware.InternalAuth(cfg))
internal.GET("/validate-token", h.ValidateToken)
// Per-route rate limiting
router.POST("/login", middleware.RateLimit(10, time.Minute), h.Login)
GO-12: Input validation in middleware
func ValidateLicenseKey() gin.HandlerFunc {
pattern := regexp.MustCompile(`^A-[A-Z0-9]{6}-[A-Z0-9]{8}-[A-Z0-9]{7}$`)
return func(c *gin.Context) {
key := c.Query("license_key")
if key == "" {
key = c.PostForm("license_key")
}
if !pattern.MatchString(key) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid license format"})
c.Abort()
return
}
c.Set("license_key", key)
c.Next()
}
}
Compile regex once in closure, not per-request.
MEDIUM: Naming Conventions
GO-13: Follow Go idioms strictly
// Receivers: single letter matching type
func (rl *RateLimiter) GetLimiter(key string) { ... }
func (m *Manager) CreateInstance(cfg Config) { ... }
func (h *Handlers) ValidateToken(c *gin.Context) { ... }
// Constructors: NewType()
func NewHandlers(...) *Handlers { ... }
func NewRateLimiter(max int) *RateLimiter { ... }
// Getters: no Get prefix (unless disambiguating)
func (u *User) Name() string { return u.name } // Not GetName()
func (u *User) SetName(n string) { u.name = n } // Set prefix OK
// Boolean: use Is/Has/Can
func (s *Session) IsExpired() bool { ... }
func (u *User) HasPermission(p string) bool { ... }
// Packages: lowercase, no underscores
package handlers // Not package http_handlers
package middleware // Not package Middleware
// Constants: PascalCase (exported), camelCase (unexported)
const MaxRetries = 3
const defaultTimeout = 30 * time.Second
GO-14: Use typed structs, not gin.H{} in hot paths
// BAD: heap allocation per response
c.JSON(200, gin.H{"status": "ok", "data": result})
// GOOD: stack-allocated struct
type Response struct {
Status string `json:"status"`
Data interface{} `json:"data"`
}
c.JSON(200, Response{Status: "ok", Data: result})
MEDIUM: Logging
GO-15: Use zerolog for structured logging
import "github.com/rs/zerolog"
// Setup
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
// Usage
logger.Info().Str("component", "auth").Str("user", userID).Msg("login successful")
logger.Error().Err(err).Str("endpoint", path).Msg("request failed")
logger.Warn().Int("attempts", count).Msg("approaching rate limit")
Never log.Printf in production: it's unstructured and hard to query.
GO-16: Component context in loggers
func NewManager(logger zerolog.Logger) *Manager {
return &Manager{
log: logger.With().Str("component", "instance-mgr").Logger(),
}
}
Security Rules
GO-17: No secrets in source code
// BAD
const apiKey = "sk-abc123..."
// GOOD
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
log.Fatal().Msg("API_KEY environment variable required")
}
GO-18: Validate all external input
Never trust query params, POST bodies, or headers. Validate format, length, and type.
GO-19: Use http.StatusXxx constants
// BAD
c.JSON(401, gin.H{"error": "unauthorized"})
// GOOD
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
HIGH: Context and Error Plumbing
GO-20: context.Context is the first argument
// BAD: caller cannot cancel or propagate deadlines
func FetchUser(id string) (*User, error) { ... }
// GOOD
func FetchUser(ctx context.Context, id string) (*User, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
return doRequest(req)
}
In handlers, derive from the request: ctx := c.Request.Context(). Never call context.Background() inside a request path.
GO-21: Inspect wrapped errors with errors.Is / errors.As
// Sentinel error
var ErrNotFound = errors.New("not found")
if err != nil {
if errors.Is(err, ErrNotFound) {
return c.JSON(http.StatusNotFound, ...)
}
var apiErr *APIError
if errors.As(err, &apiErr) {
return c.JSON(apiErr.Status, apiErr)
}
return c.JSON(http.StatusInternalServerError, ...)
}
== comparison on wrapped errors fails. Always unwrap with Is/As.
GO-22a: log/slog is the structured logger, not third-party libs
// Old: zerolog / zap / logrus
// New (Go 1.21+): stdlib log/slog
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
slog.Info("user signed in", "user_id", uid, "tenant", tenantID)
slog.Error("save failed", "err", err, "request_id", reqID)
slog covers JSON output, log levels, attribute groups, and context propagation. New code should not pull in zerolog or zap unless there's a specific reason.
GO-22b: math/rand/v2 for non-cryptographic randomness
// BAD: math/rand needs explicit seeding, has known bias issues
import "math/rand"
// GOOD: math/rand/v2 (Go 1.22+) seeds from a CSPRNG by default
import "math/rand/v2"
n := rand.IntN(100) // 0..99
shuffled := rand.Perm(10) // permutation
For tokens, secrets, or anything cryptographic, still use crypto/rand.
GO-22c: os.Root for filesystem-confined access (Go 1.24+)
// Confine FS access to a directory, prevents path traversal
root, err := os.OpenRoot("/var/app/uploads")
if err != nil {
return err
}
defer root.Close()
// All operations resolve relative to root, ".." escapes are blocked
f, err := root.Open(userPath)
Use os.Root for any filesystem code that touches user-supplied paths.
GO-22ca: Go 1.26 highlights (Feb 2026)
- Green Tea garbage collector is now default. Real-world programs see 10-40% lower GC overhead, no source change required.
crypto/hpkestdlib package implements RFC 9180 Hybrid Public Key Encryption with post-quantum hybrid KEMs. Use this for new key-agreement code instead of pulling incloudflare/circl.new(expr):newaccepts an expression and returns a pointer to its initial value. Replacesx := expr; p := &x.- Self-referencing generic types: a type can name itself in its own type-parameter list. Simplifies trees, graphs, and CRTP-style APIs.
GOEXPERIMENT=simdopens the experimentalsimd/archsimdpackage. Useful for hot loops, still subject to change.- cgo overhead down ~30% baseline. Tighten cgo budgets accordingly.
GO-22d: range over functions and integers
// Go 1.22+: range over int
for i := range 10 { ... }
// Go 1.23+: range over function (custom iterators)
for k, v := range myMap.All() {
fmt.Println(k, v)
}
// Define an iterator
func (m *Map[K, V]) All() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for k, v := range m.data {
if !yield(k, v) { return }
}
}
}
Replaces hand-rolled Iterator interfaces. Pair with the iter package.
GO-22e: Never expose /debug/pprof publicly
// BAD: pprof leaks heap, goroutine names, source paths to anyone
import _ "net/http/pprof"
http.ListenAndServe(":8080", nil)
// GOOD: separate port, bound to localhost or behind auth
go http.ListenAndServe("127.0.0.1:6060", nil)
Same for expvar. Either gate behind auth middleware or bind to loopback.
Vulnerability Checklist
- All errors wrapped with
fmt.Errorf("context: %w", err) - Response bodies drained before close (
io.Copy(io.Discard, resp.Body)) - No secrets in source (use env vars)
- Input validated in middleware (regex compiled once)
- RWMutex with double-check locking pattern
- Background goroutines have shutdown mechanism
- Graceful shutdown with 30s timeout
- Structured logging (zerolog, not fmt/log)
- No
gin.H{}in hot paths (use typed structs) -
c.Abort()called after error responses in middleware