Go — Best Practices & Patterns
Go is a language of constraints: explicit error handling, a minimal type system, no generics-style ceremony for many years, and a concurrency model built around goroutines and channels. Those constraints are features. They force teams toward readable, maintainable, and predictable code — but only when the team agrees on conventions.
This guide distills production-tested Go best practices: how to lay out a project, name things consistently, handle errors without drowning in boilerplate, design interfaces that are small and composable, use concurrency safely, write useful tests, log observably, and keep performance predictable. The patterns here are drawn from the Go Proverbs, the standard library, and real-world codebases that ship at scale.
info
These rules are heuristics, not absolute laws. They cover the most common sources of review friction: error handling, interface size, goroutine lifetimes, package naming, and mutation of shared state. Internalizing them will make your Go code idiomatic and safer by default.
| Avoid | Prefer | Why |
|---|---|---|
| panic for normal errors | Return explicit error values | Callers decide how to recover; errors are values in Go |
| Empty interfaces everywhere | Concrete types or small, focused interfaces | Type safety and compiler help disappear with interface |
| Huge interfaces | Interfaces with one or two methods | Small interfaces are easy to implement and mock |
| Starting goroutines without cleanup | Managed lifetimes with context and sync.WaitGroup | Leaked goroutines leak memory and hide bugs |
| Using init for complex setup | Explicit constructors and dependency injection | init is hard to test and impossible to skip |
| Global mutable state | Types with their own state and explicit configuration | Globals make tests flaky and concurrency unsafe |
| Naked returns | Explicit return statements | Naked returns hurt readability in non-trivial functions |
| Shadowing package names | Variables named differently from imported packages | Shadowing breaks tooling and confuses readers |
| Ignoring errors silently | Check or annotate every error | Silent failures are the hardest to debug |
| Overusing reflection | Code generation or explicit types | Reflection is slow, unsafe, and hard to maintain |
warning
Go does not enforce a project layout, but the Go community has converged on a few patterns. For services and CLIs, the Standard Go Project Layout is the most common starting point. It separates binaries, libraries, internal packages, configuration, deployments, and test helpers.
| 1 | myapp/ |
| 2 | ├── cmd/ |
| 3 | │ ├── api/ # main entry point for the API service |
| 4 | │ │ └── main.go |
| 5 | │ └── worker/ # main entry point for the background worker |
| 6 | │ └── main.go |
| 7 | ├── internal/ |
| 8 | │ ├── server/ # HTTP server setup (private to module) |
| 9 | │ ├── storage/ # database access layer |
| 10 | │ └── config/ # configuration loading |
| 11 | ├── pkg/ |
| 12 | │ └── api/ # public packages importable by others |
| 13 | ├── api/ # OpenAPI / protobuf definitions |
| 14 | ├── web/ # static assets and templates |
| 15 | ├── configs/ # example configuration files |
| 16 | ├── deployments/ # Docker, k8s, terraform |
| 17 | ├── scripts/ # build and migration scripts |
| 18 | ├── Makefile |
| 19 | ├── go.mod |
| 20 | └── README.md |
The internal/ directory is enforced by the compiler: packages inside it can only be imported by code within the module tree. Use internal/ for application-specific code that should not become a public API surface. Place reusable, stable packages under pkg/ only if external projects genuinely need to import them.
best practice
| 1 | // cmd/api/main.go — minimal entry point |
| 2 | package main |
| 3 | |
| 4 | import ( |
| 5 | "context" |
| 6 | "log" |
| 7 | "os" |
| 8 | "os/signal" |
| 9 | "syscall" |
| 10 | |
| 11 | "github.com/example/myapp/internal/config" |
| 12 | "github.com/example/myapp/internal/server" |
| 13 | ) |
| 14 | |
| 15 | func main() { |
| 16 | cfg, err := config.Load() |
| 17 | if err != nil { |
| 18 | log.Fatalf("load config: %v", err) |
| 19 | } |
| 20 | |
| 21 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 22 | defer stop() |
| 23 | |
| 24 | srv := server.New(cfg) |
| 25 | if err := srv.Run(ctx); err != nil { |
| 26 | log.Fatalf("server error: %v", err) |
| 27 | } |
| 28 | } |
Each program in cmd/ should be a thin wrapper around packages from internal/ or pkg/. The main function is responsible for reading configuration, wiring dependencies, handling OS signals, and reporting fatal errors. It should contain almost no business logic.
Naming in Go is intentionally terse. The language relies on package names to provide context, so identifiers inside a package can be short. Clarity is measured by how well a name reads at the call site, not by how descriptive it is in isolation.
| 1 | // Good: package name provides context |
| 2 | package user |
| 3 | |
| 4 | func NewStore(db *sql.DB) *Store { ... } |
| 5 | func (s *Store) ByID(ctx context.Context, id int64) (User, error) { ... } |
| 6 | |
| 7 | // Call site reads naturally |
| 8 | u, err := user.NewStore(db).ByID(ctx, 42) |
| 9 | |
| 10 | // Bad: stutter and Hungarian notation |
| 11 | package user |
| 12 | |
| 13 | type UserStore struct{ ... } |
| 14 | func NewUserStore(userDb *sql.DB) *UserStore { ... } |
| 15 | func (us *UserStore) GetUserByUserID(ctx context.Context, userID int64) (User, error) { ... } |
| Category | Convention | Example |
|---|---|---|
| Packages | Short, lowercase, no underscores; singular noun | net/http, io/ioutil, config |
| Interfaces | Method-name plus -er suffix, or plain noun | Reader, Writer, Storage |
| Constructor | New or NewT | NewServer, NewStore |
| Errors | Prefix Err for package-level errors | ErrNotFound, ErrInvalidID |
| Constants | CamelCase; exported if public | DefaultTimeout, maxRetries |
| Acronyms | All caps when exported: URL, HTTP | ServeHTTP, parseURL |
| Receivers | One or two letters of the type name | func (s *Store), func (r *Request) |
| Tests | _test.go suffix; table-driven | store_test.go |
info
Go's explicit error handling is a feature, not a bug. Every function that can fail returns an error. The caller decides whether to handle, wrap, or propagate. The key discipline is to add context at every layer so that the eventual log entry tells a complete story.
| 1 | // Bad: context-free error chain |
| 2 | if err != nil { |
| 3 | return err |
| 4 | } |
| 5 | |
| 6 | // Good: wrap with context using %w for error chaining |
| 7 | if err != nil { |
| 8 | return fmt.Errorf("load user %d: %w", id, err) |
| 9 | } |
| 10 | |
| 11 | // Good: sentinel errors for stable comparison |
| 12 | var ErrNotFound = errors.New("not found") |
| 13 | |
| 14 | if errors.Is(err, ErrNotFound) { |
| 15 | http.Error(w, "user not found", http.StatusNotFound) |
| 16 | return |
| 17 | } |
Wrap errors with fmt.Errorf("...: %w", ...) when crossing package boundaries or significant abstraction layers. Use errors.Is to check against sentinels and errors.As to extract typed errors. Never compare error strings directly in production code.
| 1 | package storage |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "database/sql" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | ) |
| 9 | |
| 10 | var ErrNotFound = errors.New("record not found") |
| 11 | |
| 12 | type UserStore struct { |
| 13 | db *sql.DB |
| 14 | } |
| 15 | |
| 16 | func (s *UserStore) User(ctx context.Context, id int64) (User, error) { |
| 17 | const q = `SELECT id, email FROM users WHERE id = $1` |
| 18 | var u User |
| 19 | if err := s.db.QueryRowContext(ctx, q, id).Scan(&u.ID, &u.Email); err != nil { |
| 20 | if errors.Is(err, sql.ErrNoRows) { |
| 21 | return User{}, fmt.Errorf("user %d: %w", id, ErrNotFound) |
| 22 | } |
| 23 | return User{}, fmt.Errorf("query user %d: %w", id, err) |
| 24 | } |
| 25 | return u, nil |
| 26 | } |
best practice
| 1 | // Custom error type for richer inspection |
| 2 | type ValidationError struct { |
| 3 | Field string |
| 4 | Message string |
| 5 | } |
| 6 | |
| 7 | func (e *ValidationError) Error() string { |
| 8 | return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) |
| 9 | } |
| 10 | |
| 11 | // Caller can use errors.As |
| 12 | var verr *ValidationError |
| 13 | if errors.As(err, &verr) { |
| 14 | log.Printf("field=%s message=%s", verr.Field, verr.Message) |
| 15 | } |
Custom error types are useful when the caller needs more than a string. Keep them simple and document the intended use of errors.As. Avoid deep error hierarchies — Go is not Java, and checked exceptions do not exist.
Go interfaces are satisfied implicitly. This is one of the language's most powerful features, but it only works well when interfaces are small. Large interfaces create tight coupling and make testing difficult. The standard library models this perfectly: io.Reader has one method.
| 1 | // Good: small, composable interfaces |
| 2 | package storage |
| 3 | |
| 4 | type Reader interface { |
| 5 | Read(ctx context.Context, id int64) (User, error) |
| 6 | } |
| 7 | |
| 8 | type Writer interface { |
| 9 | Create(ctx context.Context, u User) error |
| 10 | } |
| 11 | |
| 12 | // Compose only where needed |
| 13 | type ReadWriter interface { |
| 14 | Reader |
| 15 | Writer |
| 16 | } |
Define interfaces on the consumer side, not the producer side. If a package needs something that can read bytes, it should define interface { Read([]byte) (int, error) } rather than importing a broad interface from elsewhere. This keeps packages loosely coupled and mocks trivial.
warning
| 1 | // Consumer-side interface in the service layer |
| 2 | package user |
| 3 | |
| 4 | import "context" |
| 5 | |
| 6 | type UserFinder interface { |
| 7 | ByID(ctx context.Context, id int64) (User, error) |
| 8 | } |
| 9 | |
| 10 | type Service struct { |
| 11 | store UserFinder |
| 12 | } |
| 13 | |
| 14 | func NewService(store UserFinder) *Service { |
| 15 | return &Service{store: store} |
| 16 | } |
| 17 | |
| 18 | // In tests, pass a stub that implements only ByID |
| 19 | type stubStore struct { |
| 20 | user User |
| 21 | err error |
| 22 | } |
| 23 | |
| 24 | func (s stubStore) ByID(_ context.Context, _ int64) (User, error) { |
| 25 | return s.user, s.err |
| 26 | } |
Accept interfaces, return concrete types. A function signature should ask for the smallest surface area it needs and return the most useful concrete value it can. This maximizes flexibility for callers while keeping implementations simple.
Go makes concurrency easy to start and hard to get right. Goroutines are cheap, but their lifetimes must be managed. Channels are elegant, but shared memory is often simpler when ownership is clear. The Go Proverbs capture the philosophy: "Do not communicate by sharing memory; share memory by communicating."
| 1 | // Fan-out / fan-in with cancellation |
| 2 | func Process(ctx context.Context, inputs []int) ([]Result, error) { |
| 3 | ctx, cancel := context.WithCancel(ctx) |
| 4 | defer cancel() |
| 5 | |
| 6 | var wg sync.WaitGroup |
| 7 | results := make(chan Result, len(inputs)) |
| 8 | |
| 9 | for _, in := range inputs { |
| 10 | wg.Add(1) |
| 11 | go func(n int) { |
| 12 | defer wg.Done() |
| 13 | select { |
| 14 | case <-ctx.Done(): |
| 15 | return |
| 16 | case results <- work(ctx, n): |
| 17 | } |
| 18 | }(in) |
| 19 | } |
| 20 | |
| 21 | go func() { |
| 22 | wg.Wait() |
| 23 | close(results) |
| 24 | }() |
| 25 | |
| 26 | var out []Result |
| 27 | for r := range results { |
| 28 | if r.Err != nil { |
| 29 | cancel() |
| 30 | return nil, r.Err |
| 31 | } |
| 32 | out = append(out, r) |
| 33 | } |
| 34 | return out, nil |
| 35 | } |
Always pass context.Context as the first argument to functions that may run in goroutines. Use sync.WaitGroup to wait for goroutines, and close channels only from the sender side. If any worker fails, cancel siblings promptly to avoid wasted work.
danger
| 1 | // Wrong: shares loop variable i across goroutines |
| 2 | for i := 0; i < 10; i++ { |
| 3 | go func() { |
| 4 | fmt.Println(i) // unpredictable |
| 5 | }() |
| 6 | } |
| 7 | |
| 8 | // Right: each goroutine gets its own copy |
| 9 | for i := 0; i < 10; i++ { |
| 10 | go func(n int) { |
| 11 | fmt.Println(n) |
| 12 | }(i) |
| 13 | } |
Use mutexes for protecting shared state, but keep the critical sections small. Prefer sync.RWMutex when reads dominate. Never call out to network or disk inside a mutex, and never hold a lock while sending on a channel — that invites deadlocks.
| 1 | // Safe counter with small critical section |
| 2 | type Counter struct { |
| 3 | mu sync.RWMutex |
| 4 | value int64 |
| 5 | } |
| 6 | |
| 7 | func (c *Counter) Add(n int64) { |
| 8 | c.mu.Lock() |
| 9 | c.value += n |
| 10 | c.mu.Unlock() |
| 11 | } |
| 12 | |
| 13 | func (c *Counter) Value() int64 { |
| 14 | c.mu.RLock() |
| 15 | defer c.mu.RUnlock() |
| 16 | return c.value |
| 17 | } |
Go's built-in testing package is intentionally minimal. Table-driven tests are the dominant pattern. They keep related cases together, make adding new scenarios easy, and provide a clear structure for setup, execution, and assertion.
| 1 | func TestParseSize(t *testing.T) { |
| 2 | tests := []struct { |
| 3 | name string |
| 4 | input string |
| 5 | want int64 |
| 6 | wantErr bool |
| 7 | }{ |
| 8 | {"bytes", "10B", 10, false}, |
| 9 | {"kilobytes", "2KB", 2048, false}, |
| 10 | {"megabytes", "1.5MB", 1572864, false}, |
| 11 | {"empty", "", 0, true}, |
| 12 | {"bad unit", "5XB", 0, true}, |
| 13 | } |
| 14 | |
| 15 | for _, tt := range tests { |
| 16 | t.Run(tt.name, func(t *testing.T) { |
| 17 | got, err := ParseSize(tt.input) |
| 18 | if (err != nil) != tt.wantErr { |
| 19 | t.Fatalf("ParseSize(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) |
| 20 | } |
| 21 | if got != tt.want { |
| 22 | t.Errorf("ParseSize(%q) = %d, want %d", tt.input, got, tt.want) |
| 23 | } |
| 24 | }) |
| 25 | } |
| 26 | } |
Use t.Run with named subtests for clarity. Keep helpers in the same file if they are specific, or place reusable helpers in a testutil package under internal/. Use t.Helper() so failure lines point to the actual test case.
best practice
| 1 | // Test with helpers and parallel execution |
| 2 | func TestUserStore(t *testing.T) { |
| 3 | ctx := context.Background() |
| 4 | store := newTestStore(t) |
| 5 | |
| 6 | t.Run("create and read", func(t *testing.T) { |
| 7 | t.Parallel() |
| 8 | u := User{Email: "test@example.com"} |
| 9 | id, err := store.Create(ctx, u) |
| 10 | if err != nil { |
| 11 | t.Fatalf("create: %v", err) |
| 12 | } |
| 13 | |
| 14 | got, err := store.User(ctx, id) |
| 15 | if err != nil { |
| 16 | t.Fatalf("read: %v", err) |
| 17 | } |
| 18 | if got.Email != u.Email { |
| 19 | t.Errorf("email = %q, want %q", got.Email, u.Email) |
| 20 | } |
| 21 | }) |
| 22 | } |
| 23 | |
| 24 | func newTestStore(t *testing.T) *UserStore { |
| 25 | t.Helper() |
| 26 | db := testdb.Open(t) |
| 27 | t.Cleanup(func() { db.Close() }) |
| 28 | return &UserStore{db: db} |
| 29 | } |
For integration tests, use t.Cleanup and test fixtures. Mark slow tests with -short guards so developers can run a fast subset locally. Keep unit tests fast and deterministic; integration tests can be slower but must still clean up after themselves.
Logging is your primary observability tool in production. Go provides log in the standard library, and since Go 1.21, log/slog provides structured logging. Structured logs are easier to query, aggregate, and alert on than free-form strings.
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "log/slog" |
| 5 | "os" |
| 6 | ) |
| 7 | |
| 8 | func main() { |
| 9 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ |
| 10 | Level: slog.LevelInfo, |
| 11 | })) |
| 12 | slog.SetDefault(logger) |
| 13 | |
| 14 | slog.Info("server starting", |
| 15 | slog.String("addr", ":8080"), |
| 16 | slog.Int("workers", 4), |
| 17 | ) |
| 18 | } |
Pass a logger through your application rather than relying on the global default. Inject *slog.Logger into constructors. This makes tests quiet and lets different subsystems run at different log levels. Avoid logging sensitive fields like tokens, passwords, or PII.
warning
| 1 | // Good: return rich errors, log at the boundary |
| 2 | func (s *Service) CreateOrder(ctx context.Context, req CreateOrderRequest) (Order, error) { |
| 3 | order, err := s.store.CreateOrder(ctx, req) |
| 4 | if err != nil { |
| 5 | return Order{}, fmt.Errorf("create order for customer %s: %w", req.CustomerID, err) |
| 6 | } |
| 7 | return order, nil |
| 8 | } |
| 9 | |
| 10 | // In the handler, log once with request context |
| 11 | order, err := svc.CreateOrder(ctx, req) |
| 12 | if err != nil { |
| 13 | slog.ErrorContext(ctx, "create order failed", |
| 14 | slog.String("customer_id", req.CustomerID), |
| 15 | slog.Any("error", err), |
| 16 | ) |
| 17 | http.Error(w, "failed to create order", http.StatusInternalServerError) |
| 18 | return |
| 19 | } |
Use log levels intentionally. Debug for fine-grained tracing during development, Info for normal lifecycle events, Warn for recoverable issues, and Error for failures that require attention. Reserve Fatal and Panic for program startup only.
Go is fast by default, but performance pitfalls still exist: unnecessary allocations, goroutine leaks, large interface conversions, and contention on hot paths. The first rule is to measure. Use benchmarks, profiling, and tracing before optimizing.
| 1 | // Benchmark example |
| 2 | func BenchmarkParseSize(b *testing.B) { |
| 3 | for i := 0; i < b.N; i++ { |
| 4 | if _, err := ParseSize("1.5MB"); err != nil { |
| 5 | b.Fatal(err) |
| 6 | } |
| 7 | } |
| 8 | } |
| 9 | |
| 10 | // Run with profiling |
| 11 | // go test -bench=. -benchmem -cpuprofile=cpu.out -memprofile=mem.out |
| 12 | // go tool pprof cpu.out |
Prefer sync.Pool for short-lived, allocation-heavy objects on hot paths. Preallocate slices when the final size is known. Use strings.Builder for repeated string concatenation. Avoid boxing values into interface in tight loops.
| 1 | // Preallocate slices to avoid repeated growth |
| 2 | func FilterEven(nums []int) []int { |
| 3 | out := make([]int, 0, len(nums)) // worst-case cap |
| 4 | for _, n := range nums { |
| 5 | if n%2 == 0 { |
| 6 | out = append(out, n) |
| 7 | } |
| 8 | } |
| 9 | return out |
| 10 | } |
| 11 | |
| 12 | // Use strings.Builder for many concatenations |
| 13 | func JoinNames(names []string) string { |
| 14 | var b strings.Builder |
| 15 | b.Grow(len(names) * 16) // estimate average length |
| 16 | for i, n := range names { |
| 17 | if i > 0 { |
| 18 | b.WriteString(", ") |
| 19 | } |
| 20 | b.WriteString(n) |
| 21 | } |
| 22 | return b.String() |
| 23 | } |
info
For HTTP services, set reasonable timeouts on every server and client. Go's default HTTP server has no timeouts, which makes it vulnerable to slowloris and idle-connection leaks. Always configure ReadTimeout, WriteTimeout, and IdleTimeout.
| 1 | srv := &http.Server{ |
| 2 | Addr: ":8080", |
| 3 | Handler: handler, |
| 4 | ReadTimeout: 5 * time.Second, |
| 5 | WriteTimeout: 10 * time.Second, |
| 6 | IdleTimeout: 120 * time.Second, |
| 7 | } |
| 8 | |
| 9 | client := &http.Client{ |
| 10 | Timeout: 30 * time.Second, |
| 11 | Transport: &http.Transport{ |
| 12 | MaxIdleConns: 100, |
| 13 | MaxIdleConnsPerHost: 10, |
| 14 | IdleConnTimeout: 90 * time.Second, |
| 15 | }, |
| 16 | } |
Use this checklist for every Go pull request. Many checks can be automated with go vet, staticcheck, and CI. Reserve human review for design decisions, error-message quality, and concurrency safety.
| Check | Automated? | Details |
|---|---|---|
| Formatting | gofmt | Code is formatted; CI rejects unformatted files |
| Vet | go vet | Common mistakes: printf format strings, shadowed variables |
| Lint | staticcheck / golangci-lint | Unused code, suspicious constructs, style issues |
| Tests | go test | Unit and integration tests pass; race detector enabled |
| Error handling | Review | Errors wrapped with context, none silently ignored |
| Interfaces | Review | Small, consumer-side; accept interfaces, return concrete types |
| Concurrency | go test -race | Goroutines have bounded lifetimes; no data races |
| Naming | Review | Idiomatic names, no stutter, no shadowed imports |
| Dependencies | go mod tidy | go.mod and go.sum are clean and minimal |
info
Every Go developer hits the same traps eventually. Knowing them in advance saves hours of debugging. Here are the most frequent sources of subtle bugs and how to avoid them.
| 1 | // Pitfall 1: nil pointer through interface |
| 2 | var p *MyType |
| 3 | var r Reader = p // r is non-nil interface wrapping nil pointer |
| 4 | if r != nil { |
| 5 | r.Read() // panics with nil pointer dereference |
| 6 | } |
| 7 | |
| 8 | // Pitfall 2: slice aliasing |
| 9 | func trimFirst(s []int) []int { |
| 10 | return s[1:] // shares backing array with original |
| 11 | } |
| 12 | |
| 13 | // Pitfall 3: defer in a loop (argument evaluated immediately) |
| 14 | for _, f := range files { |
| 15 | f := f // capture |
| 16 | defer f.Close() |
| 17 | } |
| 18 | |
| 19 | // Pitfall 4: ignoring context cancellation |
| 20 | func slow(ctx context.Context) error { |
| 21 | select { |
| 22 | case <-time.After(10 * time.Second): |
| 23 | return nil |
| 24 | case <-ctx.Done(): |
| 25 | return ctx.Err() // must respect cancellation |
| 26 | } |
| 27 | } |
Interface nils surprise newcomers because an interface value is a tuple of (type, value). A nil concrete value inside a non-nil interface is still non-nil. For slices, understand when operations share backing arrays and copy explicitly when needed. Always consider ctx.Done() in long-running operations.
danger
Go's toolchain is one of its strengths. Mastering the standard tools and a few well-chosen third-party tools will make you far more productive than chasing the latest library trends.
| 1 | # Essential commands |
| 2 | go fmt ./... # format all packages |
| 3 | go vet ./... # static analysis |
| 4 | go test ./... # run tests |
| 5 | go test -race ./... # run tests with race detector |
| 6 | go test -cover ./... # coverage summary |
| 7 | go test -bench=. -benchmem # run benchmarks |
| 8 | |
| 9 | # Recommended third-party tools |
| 10 | go install honnef.co/go/tools/cmd/staticcheck@latest |
| 11 | staticcheck ./... |
| 12 | |
| 13 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest |
| 14 | golangci-lint run |
| 15 | |
| 16 | # Module maintenance |
| 17 | go mod tidy |
| 18 | go mod verify |
| 19 | go list -m all |
| Tool | Purpose | When to run |
|---|---|---|
| gofmt | Consistent formatting | On save and in CI |
| go vet | Common mistakes | Before every commit |
| staticcheck | Advanced static analysis | In CI |
| golangci-lint | Configurable lint aggregator | In CI and locally |
| go test -race | Detect data races | On every PR |
| pprof | CPU and memory profiling | When optimizing |
These references are the authoritative sources behind the recommendations in this guide. They are worth revisiting as the language and ecosystem evolve.
| Resource | Topic |
|---|---|
| Effective Go | go.dev/doc/effective_go — idiomatic Go |
| Go Proverbs | go-proverbs.github.io — pithy design guidance |
| Standard Library Docs | pkg.go.dev/std — authoritative package reference |
| Standard Go Project Layout | github.com/golang-standards/project-layout |
| Go Code Review Comments | github.com/golang/go/wiki/CodeReviewComments |
| Google Go Style Guide | google.github.io/styleguide/go/ |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.