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.
Source
View on GitHubGo (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, OpenAPIcmd/x/main.gofor 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 isinternal/, 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)โ%wpreserves the chain forerrors.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) { ... } paniconly 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)
...
}ctxis the first parameter in any function that does I/O, blocks, or might be cancelled.- Never store
Contextin a struct โ pass it through. context.Background()only at program entry (main, test setup, request handlers).context.WithTimeoutanddefer 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
IUserServicefrom your storage package; let callers declare what they need. - Small interfaces (1-3 methods) compose better than fat ones.
io.Readeris one method. - No
Iprefix orinterfacesuffix.Reader, notIReaderorReaderInterface.
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.Groupfor 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.
selectfor 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}
}NewXconstructors when you have invariants to enforce or zero value isn't useful.- No constructors needed when the zero value works โ
var sb strings.Builderis 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.Rungives each case its own name. t.Parallel()in subtests when they don't share state.testify/assertortestify/requireif you want richer assertions, but stdlib is fine.t.Helper()in test helpers so failure lines point at the caller.go test -racein 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.Printlnin 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{}/anyreflexively โ 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.GOMAXPROCStuning โ Go does this fine on its own in modern versions.