agentleFS
eduardo-sleduardo-sl/go-agent-skills/skills/(architecture)/go-project-layout/SKILL.md

go-project-layout

Skill85 starsChanged 18 days agoMITView on GitHub

Scaffold new Go projects and services: directory structure, cmd/ and internal/ conventions, when to use a flat layout, module naming, and main package wiring. Use when: "new Go project", "scaffold a service", "create project structure", "start a Go module", "how do I organize a new service", "set up folder structure". Not for: reviewing an existing architecture (go-architecture-review), DI wiring (go-dependency-injection), CI setup (go-ci).

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never start with empty pkg/, api/, docs/, build/ directories "for later".

    line 38
  2. Very specific:

    Keep main.go to wiring plus a delegating call, so the app is testable:

    line 85
  3. Very specific:

    Don't stutter: payment.Service, not payment.PaymentService.

    line 138
  4. Very specific:

    Do NOT create: src/ (un-idiomatic), vendor/ (unless the team explicitly vendors), one-file packages like types/ or models/ that become …

    line 146
  5. Very specific:

    Default to one module per repository.

    line 163
  6. Distinctive:

    Ask/decide: tool, library, or service?

    line 170
  7. Distinctive:

    Create only the directories the first feature needs.

    line 174
  8. Very specific:

    Write main.go with the thin-main pattern above, and a config struct.

    line 175
  9. Very specific:

    Verify: go build ./... and go vet ./...

    line 178
  10. Very specific:

    All non-public packages live under internal/

    line 184

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Go Project Layout

Structure follows size. The biggest layout mistake in Go is copying a microservice skeleton for a 500-line tool — or growing a 50-package service inside a flat directory. Match the layout to the project.

Community default. A project skill or agent-config rule that explicitly supersedes go-project-layout takes precedence over this one.

1. Pick the Layout by Project Size

ProjectLayout
Small tool, single binary, <5 filesFlat: everything in package main at the root
Library for others to importRoot package named after the module, internal/ for helpers
Service with one binarycmd/<name>/main.go + internal/ packages
Multiple binaries sharing codecmd/<name1>/, cmd/<name2>/ + internal/

Never start with empty pkg/, api/, docs/, build/ directories "for later". Add structure when the code demands it, not before.

2. Module Naming

# ✅ Good — repository path, lowercase
go mod init github.com/acme/payment-service

# ❌ Bad — not fetchable, uppercase, or vanity without DNS
go mod init PaymentService
go mod init payment_service

The last path element should match what users will see: for a library, it becomes the default import name.

3. Service Layout (the default for APIs and workers)

payment-service/
├── cmd/
│   └── payment-api/
│       └── main.go         # flag/env parsing, wiring, Run() — nothing else
├── internal/
│   ├── domain/             # core types, business rules; zero external deps
│   ├── service/            # use cases orchestrating domain + stores
│   ├── store/              # data access implementations (postgres/, redis/)
│   ├── handler/            # HTTP/gRPC adapters
│   └── config/             # config loading and validation
├── migrations/             # if the service owns a database
├── go.mod
├── Makefile
└── README.md

Rules:

  • internal/ by default — the compiler enforces that nobody outside the module imports it. Promote to a public package only on demand.
  • pkg/ only when external consumers exist AND the module also has private code. When in doubt, don't create it.
  • Dependencies point inward: handler → service → domain ← store. domain imports neither store nor handler.

4. Thin main, Runnable Run

Keep main.go to wiring plus a delegating call, so the app is testable:

func main() {
    if err := run(context.Background(), os.Args[1:], os.Getenv); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(ctx context.Context, args []string, getenv func(string) string) error {
    cfg, err := config.Load(getenv)
    if err != nil {
        return fmt.Errorf("load config: %w", err)
    }

    db, err := store.Open(ctx, cfg.DatabaseURL)
    if err != nil {
        return fmt.Errorf("open db: %w", err)
    }
    defer db.Close()

    svc := service.New(store.NewUserRepo(db))
    srv := handler.NewServer(cfg.Addr, svc)
    return srv.ListenAndServe(ctx)
}
  • os.Exit appears exactly once, in main.
  • run takes its dependencies (args, getenv) so tests can call it.
  • No init() functions for wiring — explicit construction order only.

5. Library Layout

retry/
├── retry.go            # package retry — the API, in the root
├── retry_test.go
├── backoff.go          # same package, split by topic
├── internal/
│   └── clock/          # implementation details users must not import
├── examples_test.go    # Example* functions shown in godoc
└── go.mod
  • The root directory IS the package. No src/, no lib/.
  • One package per concept. Resist util, common, helpers — name packages after what they provide (retry, clock, httpsign).

6. Naming Rules for Directories and Packages

  • Package name == directory name, short, lowercase, no underscores: store/postgres, not store/postgres_impl.
  • Don't stutter: payment.Service, not payment.PaymentService.
  • Binary names in cmd/ are user-facing: cmd/payment-api, hyphenated is fine (directory only holds package main).

7. Files That Belong at the Root

  • go.mod, go.sum, README.md, LICENSE, Makefile, .golangci.yml, Dockerfile (single-binary projects).
  • Do NOT create: src/ (un-idiomatic), vendor/ (unless the team explicitly vendors), one-file packages like types/ or models/ that become dumping grounds.

8. Configuration

  • One config struct, loaded from the environment in one place (internal/config), validated at startup — fail fast with every missing or invalid value listed, not the first one.
  • Flags for CLIs; environment variables for services (12-factor); defaults in code. No config files baked into images, no environment-specific branches (if env == "prod") in business code.
  • Secrets come from the environment or a secret manager, never from flags (ps shows argv) or the repository.

9. One Module or Several

Default to one module per repository. Split into modules only when parts are versioned and released independently (a public client library next to a service). Develop across modules locally with go.work; don't commit replace => ../path directives.

Scaffolding Procedure

  1. Ask/decide: tool, library, or service? How many binaries?
  2. go mod init <repo-path>, then set the go directive deliberately — since Go 1.26, go mod init writes the previous release (go 1.(N-1).0), not the toolchain's.
  3. Create only the directories the first feature needs.
  4. Write main.go with the thin-main pattern above, and a config struct.
  5. Pin developer tools as module tools (go get -tool ..., Go 1.24+) and add Makefile targets: build, test, lint.
  6. Verify: go build ./... and go vet ./... pass on the skeleton.

Verification Checklist

  1. Layout matches project size — no empty scaffolding directories
  2. Module path is the fetchable repository path
  3. All non-public packages live under internal/
  4. main.go is thin: parse, wire, call run, exit
  5. os.Exit only in main; no wiring in init()
  6. Dependencies flow inward; domain has zero infrastructure imports
  7. No util/common/helpers/models grab-bag packages
  8. Package names match directories, lowercase, no stutter
  9. go build ./... passes on the fresh skeleton
  10. Config loaded and validated in one place at startup; no secrets in flags
  11. One module unless parts are released independently; no committed local replace

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.