Part 4 · 1 chapters · ~8 min
Idioms
Effective Go in practice: errors handled where they occur, small interfaces, accept interfaces and return structs, the functional options pattern, constructors, table-driven tests, defer for cleanup, zero values that are useful, package layout, and what not to import from other languages.
9
Writing Go like Go
code
// functional options: readable constructors with defaults
type Server struct{ addr string; readTimeout time.Duration; logger *slog.Logger }
type Option func(*Server)
func WithReadTimeout(d time.Duration) Option { return func(s *Server) { s.readTimeout = d } }
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, readTimeout: 5 * time.Second, logger: slog.Default()}
for _, o := range opts { o(s) }
return s
}
srv := NewServer(":8080", WithReadTimeout(10*time.Second))
// defer for cleanup, in reverse order
f, err := os.Open(path); if err != nil { return err }; defer f.Close()
tx, err := db.BeginTx(ctx, nil); if err != nil { return err }; defer tx.Rollback() // no-op after Commit| idiom | instead of |
|---|---|
if err != nil { return fmt.Errorf("context: %w", err) } | exceptions and try/catch |
| small interfaces defined by the consumer | large interfaces defined up front by the implementer |
flat packages by domain (ledger, transfers) | layers named models, utils, helpers |
| composition and embedding | inheritance hierarchies |
| the standard library first (net/http, slog, database/sql) | a framework for everything |
A common layout: cmd/ledger/main.go for binaries, internal/ for packages other modules may not import, domain packages inside it. Avoid pkg/ and deep hierarchies until you need them.