go-dev
Go Development
Write Go code that is readable, maintainable, and production-ready using battle-tested patterns from major production codebases.
For comprehensive coverage of all idioms, patterns, and pitfalls, read
references/go-styleguide.md. This file focuses on quick decisions and
workflows.
MCP
Always use Context7 MCP to fetch the latest documentation.
Libraries
- Prefer well-maintained, zero-dependency libraries from the awesome-go list.
- HTTP routing: Chi.
- Logging:
log/slog(structured, leveled, stdlib since Go 1.21). - Configuration: flags or environment variables — no external config frameworks.
- Database access: sqlc for typesafe SQL code generation.
- Migrations: goose.
- Testing: stdlib
testingpackage. Avoid third-party assertion libraries.
Linters
Run golangci-lint on every commit and pull request. Use the bundled
.golangci.yml config:
# Setup linting for a project
scripts/setup_golangci_lint.sh /path/to/project
# Run all linters
golangci-lint run ./...
# Auto-fix
golangci-lint run --fix ./...
Run goimports before committing to keep imports formatted.
Quick Decision Trees
When to use generics?
Use generics (Go 1.18+) when:
- Writing data structures (trees, caches, pools) that work across types.
- Utility functions that operate on slices, maps, or channels of any type.
- Type constraints reduce duplication without sacrificing readability.
Avoid generics when:
- A concrete type or
anysuffices. - The function body would need type assertions anyway.
- It makes the code harder to read for marginal DRY benefit.
// GOOD: generic utility
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
// GOOD: constrained type
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
When to use interfaces?
Define interfaces at the consumption site, not the implementation:
// GOOD: consumer defines what it needs
package storage
type Store interface {
Get(key string) ([]byte, error)
}
// BAD: implementation forces interface on consumers
package postgres
type PostgresStore interface { ... }
Interface size: 1 method is perfect, 2-3 if cohesive, 4+ consider splitting. Larger interfaces are acceptable for SaaS/enterprise products; keep them small for libraries.
Accept interfaces, return concrete types.
How to handle errors?
- Can I handle this completely here? → Log and continue.
- Does caller need programmatic access? →
%wwrapping. - Should I hide implementation details? →
%vwrapping. - Is this a library? → Never log, always return.
// Wrap with context
if err != nil {
return fmt.Errorf("connect to database: %w", err)
}
Error strings: lowercase, no punctuation, no "failed to" prefix. Handle each error exactly once — log OR return, never both.
Use errors.Join (Go 1.20+) to combine multiple independent errors:
var errs []error
for _, item := range items {
if err := process(item); err != nil {
errs = append(errs, err)
}
}
if err := errors.Join(errs...); err != nil {
return fmt.Errorf("processing batch: %w", err)
}
When to use concurrency?
Leave concurrency to the caller unless building a server/daemon, worker pool, or managing background operations.
Before launching a goroutine, know when it will stop:
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
go func() {
for {
select {
case <-ctx.Done():
return
case work := <-ch:
process(work)
}
}
}()
Context as first parameter. Always.
Iterators (Go 1.23+)
Use iter.Seq and iter.Seq2 for lazy iteration:
// Iterator that yields values
func FilterPositive(nums []int) iter.Seq[int] {
return func(yield func(int) bool) {
for _, n := range nums {
if n > 0 {
if !yield(n) {
return
}
}
}
}
}
// Consuming an iterator
for v := range FilterPositive(data) {
fmt.Println(v)
}
Use range-over-int (Go 1.22+): for i := range n instead of
for i := 0; i < n; i++.
Structured logging with slog
Use log/slog for all logging. Pass the logger as a dependency, never as a
package-level global:
type Server struct {
logger *slog.Logger
}
func NewServer(logger *slog.Logger) *Server {
return &Server{logger: logger}
}
func (s *Server) HandleRequest(ctx context.Context, req *Request) {
s.logger.InfoContext(ctx, "handling request",
slog.String("method", req.Method),
slog.String("path", req.Path),
)
}
Use slog.With to add common attributes. Use LogValuer for expensive
values that should only be computed when the log level is enabled.
Testing
Table-driven tests with map[string]testCase for descriptive names:
func TestProcess(t *testing.T) {
type testCase struct {
input string
want string
wantErr bool
}
tests := map[string]testCase{
"valid input": {
input: "hello",
want: "HELLO",
},
"empty input returns error": {
input: "",
wantErr: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got, err := Process(tc.input)
if (err != nil) != tc.wantErr {
t.Fatalf("Process() error = %v, wantErr %v", err, tc.wantErr)
}
if got != tc.want {
t.Errorf("Process() = %q, want %q", got, tc.want)
}
})
}
}
Test helpers call t.Helper() so failure line numbers point to the actual test.
Integration tests skip when environment is not set:
func TestIntegration(t *testing.T) {
if os.Getenv("INTEGRATION_TESTS") == "" {
t.Skip("skipping integration tests")
}
}
Common Workflows
Creating a new HTTP service
Project structure:
myservice/
├── cmd/server/main.go
├── internal/
│ ├── handler/
│ ├── service/
│ └── storage/
├── db/
│ ├── migrations/
│ └── queries/
├── go.mod
├── Makefile
└── .golangci.yml
main.go pattern — flags, graceful shutdown:
func main() {
addr := flag.String("addr", ":8080", "listen address")
flag.Parse()
srv := &http.Server{
Addr: *addr,
Handler: setupRoutes(),
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
go func() {
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt)
<-sigint
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
}()
log.Printf("listening on %s", *addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}
Working with SQL databases using sqlc
sqlc generates typesafe Go code from SQL queries. Write SQL, get Go.
1. Install and configure:
# sqlc.yaml
version: "2"
sql:
- schema: "db/migrations"
queries: "db/queries"
engine: "postgresql"
gen:
go:
package: "db"
out: "internal/db"
emit_json_tags: true
emit_interface: true
2. Write migrations (with goose):
-- db/migrations/001_create_users.sql
-- +goose Up
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- +goose Down
DROP TABLE users;
3. Write queries with annotations:
-- db/queries/users.sql
-- name: GetUser :one
SELECT id, email, name, created_at
FROM users
WHERE id = $1;
-- name: ListUsers :many
SELECT id, email, name, created_at
FROM users
ORDER BY created_at DESC;
-- name: CreateUser :one
INSERT INTO users (email, name)
VALUES ($1, $2)
RETURNING id, email, name, created_at;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
4. Generate and use:
sqlc generate
// internal/db/ now contains typesafe Go code
func (s *Service) GetUser(ctx context.Context, id int64) (db.User, error) {
return s.queries.GetUser(ctx, id)
}
5. Testing with sqlc:
Enable emit_interface: true in sqlc.yaml to get a Querier interface for
mocking in unit tests. Use a real database for integration tests.
Creating a CLI tool
mycli/
├── main.go
├── internal/command/
├── go.mod
└── .golangci.yml
Use flag.NewFlagSet for subcommands. Write errors to stderr, exit non-zero
on failure.
Quick Reference
Naming: packages lowercase/singular, no Get prefix on getters, acronyms
consistent case (URL not Url), constants in mixedCaps.
Structure: return early with guard clauses, success path left-aligned, imports grouped: stdlib → external → internal.
Critical pitfalls: loop variable capture in closures, nil interface vs nil value in interface, defer in loops (wrap in closure), map writes to nil map.
For the full reference on all patterns, see references/go-styleguide.md.
Linting Setup
Run the setup script to configure golangci-lint for a project:
scripts/setup_golangci_lint.sh /path/to/your/project
This copies the bundled .golangci.yml and optionally installs a pre-commit
hook. Common commands:
golangci-lint run ./... # run all linters
golangci-lint run --fix ./... # auto-fix issues
golangci-lint run ./internal/...# lint specific paths
More from marsolab/skills
sys-arch
Design production-grade software systems with expert knowledge of architecture patterns, distributed systems, cloud platforms, and operational excellence. Use this skill when architecting complex systems, evaluating technology choices, designing scalable infrastructure, or making critical architectural decisions requiring trade-off analysis.
18front-dev
>-
15copy
Professional copywriter for SaaS and startups. Expert in landing page copy, positioning, messaging, conversion optimization, and voice-of-customer research. Use when writing compelling copy for SaaS products, landing pages, marketing materials, or when you need help with product positioning and messaging strategy.
9apple-dev
Comprehensive macOS and iOS development expertise covering Swift best practices, SwiftUI design patterns, Human Interface Guidelines, Apple frameworks, and performance optimization. Use when developing native Apple applications, implementing SwiftUI interfaces, working with Apple frameworks (Foundation, UIKit, AppKit, Core Data, CloudKit, etc.), optimizing app performance, following HIG principles, or writing production-quality Swift code for iOS, macOS, watchOS, or tvOS platforms.
3system-architect
Design production-grade software systems with expert knowledge of architecture patterns, distributed systems, cloud platforms, and operational excellence. Use this skill when architecting complex systems, evaluating technology choices, designing scalable infrastructure, or making critical architectural decisions requiring trade-off analysis.
1landing-page-breakdown
Comprehensive landing page design analysis for extracting typography, color palette, spacing systems, visual elements, and conversion optimization insights. Use when a user provides a landing page URL for design analysis, wants to understand what makes a page effective, needs to extract design specifications, or wants to learn from high-converting landing pages.
1