๐Ÿ”งAutoSkills
All items
skillv1.0.0

go-modules

Go modules: project layout, errors, context, interfaces, goroutines, table-driven tests

golanguagebackend

Install

$npx autoskills --items go-modules

Or scan + install everything matching your stack with npx autoskills.

Go (Modules)

Go is small. The hard part isn't learning the syntax โ€” it's resisting the urge to import patterns from elsewhere that don't fit.

Project layout

mymodule/
  go.mod
  go.sum
  cmd/
    server/main.go      # binary 1
    cli/main.go         # binary 2
  internal/             # not importable from outside this module
    auth/
    storage/
  pkg/                  # importable utilities (use sparingly)
  api/                  # generated code, protobuf, OpenAPI
  • cmd/x/main.go for each binary; main is tiny โ€” wiring only.
  • internal/ is enforced by the compiler โ€” packages there can't be imported by other modules.
  • pkg/ is a Java-ism; not required and often misused. If everything is internal/, you're fine.

Error handling

file, err := os.Open(path)
if err != nil {
    return fmt.Errorf("opening %s: %w", path, err)
}
defer file.Close()
  • Wrap with fmt.Errorf("...: %w", err) โ€” %w preserves the chain for errors.Is / errors.As.
  • Sentinel errors with errors.Is:
    if errors.Is(err, os.ErrNotExist) { ... }
  • Typed errors with errors.As:
    var pathErr *fs.PathError
    if errors.As(err, &pathErr) { ... }
  • panic only for programmer errors (impossible conditions, init failures). Recover at goroutine boundaries.

Context

func FetchUser(ctx context.Context, id string) (*User, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    ...
}
  • ctx is the first parameter in any function that does I/O, blocks, or might be cancelled.
  • Never store Context in a struct โ€” pass it through.
  • context.Background() only at program entry (main, test setup, request handlers).
  • context.WithTimeout and defer cancel() for bounded waits.
  • Values in context (context.WithValue) are for request-scoped data only (trace IDs, user IDs). Not for dependency injection.

Interfaces

// good: small interface defined at the consumer
type Storer interface {
    Save(ctx context.Context, user *User) error
}
 
func RegisterUser(ctx context.Context, store Storer, u *User) error {
    return store.Save(ctx, u)
}
  • Interfaces are defined by the consumer, not the producer. Don't ship IUserService from your storage package; let callers declare what they need.
  • Small interfaces (1-3 methods) compose better than fat ones. io.Reader is one method.
  • No I prefix or interface suffix. Reader, not IReader or ReaderInterface.

Concurrency

var wg sync.WaitGroup
for _, item := range items {
    wg.Add(1)
    go func(item Item) { // pass as arg โ€” don't close over loop var (pre-Go 1.22)
        defer wg.Done()
        process(item)
    }(item)
}
wg.Wait()
  • Go 1.22+ fixes the loop-variable capture bug โ€” but explicit is still clearer.
  • errgroup.Group for goroutines that can fail: cancels siblings on first error.
  • Channels for ownership transfer, mutexes for shared state. Both are fine; pick by data flow.
  • Buffered channels with care โ€” buffer size encodes assumptions about producer/consumer rates. Unbuffered is often the right default.
  • select for multi-channel reads and timeouts:
    select {
    case msg := <-ch:
        handle(msg)
    case <-ctx.Done():
        return ctx.Err()
    case <-time.After(5 * time.Second):
        return errors.New("timeout")
    }

Structs and constructors

type Server struct {
    db     *sql.DB
    logger *slog.Logger
    addr   string
}
 
func NewServer(db *sql.DB, logger *slog.Logger, addr string) *Server {
    return &Server{db: db, logger: logger, addr: addr}
}
  • NewX constructors when you have invariants to enforce or zero value isn't useful.
  • No constructors needed when the zero value works โ€” var sb strings.Builder is fine.
  • Functional options pattern for many-parameter constructors:
    type Option func(*Server)
    func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
    func NewServer(opts ...Option) *Server { ... }

Testing

func TestAdd(t *testing.T) {
    cases := []struct {
        name string
        a, b int
        want int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            t.Parallel()
            if got := Add(tc.a, tc.b); got != tc.want {
                t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
            }
        })
    }
}
  • Table-driven tests are idiomatic; t.Run gives each case its own name.
  • t.Parallel() in subtests when they don't share state.
  • testify/assert or testify/require if you want richer assertions, but stdlib is fine.
  • t.Helper() in test helpers so failure lines point at the caller.
  • go test -race in CI catches data races; non-negotiable for concurrent code.

Logging

  • log/slog (stdlib since 1.21) for structured logging.
  • logger := slog.With("request_id", id) โ€” derive scoped loggers per request.
  • No fmt.Println in services. Use the logger so output is structured and levelled.

What to avoid

  • init() for side effects โ€” registration is fine; anything else makes test isolation harder.
  • Global state โ€” pass dependencies via struct fields or function args.
  • interface{} / any reflexively โ€” use concrete types or generics.
  • Catching errors with _ := fn() โ€” silently dropping errors. Either handle or document why.
  • Embedding mutexes by value โ€” they don't copy correctly. Pointer fields only, or use sync.Mutex by value at the struct field level (not in copies).
  • runtime.GOMAXPROCS tuning โ€” Go does this fine on its own in modern versions.