docs: add when-not to structs + testing-advanced + api-conventions

This commit is contained in:
2026-04-30 13:24:01 +00:00
parent 631be02392
commit a7a853bb43
5 changed files with 570 additions and 0 deletions
+98
View File
@@ -143,6 +143,32 @@ func InternalFormat(s string) Thing { ... } // only importable by pkg/mylib and
import "pkg/mylib/internal/parse" // ✓ allowed
```
### When NOT to Use
**Don't use `internal/` when:**
- The code is only used by a single package (just keep it unexported in that package)
- You're hiding code that *should* be public API — `internal/` isn't a staging area for "maybe later"
- You have a flat package structure with no sub-packages (no one to share with)
**Over-application example:**
```go
// pkg/mylib/internal/config/config.go
package config
// Only used by pkg/mylib itself — no sub-packages import this
func DefaultTimeout() time.Duration { return 30 * time.Second }
```
**Better alternative:**
```go
// pkg/mylib/config.go — just make it unexported in the parent package
package mylib
func defaultTimeout() time.Duration { return 30 * time.Second }
```
**Why:** `internal/` adds directory structure complexity. If you have no sub-packages sharing the code, an unexported function in the parent package is simpler and achieves the same encapsulation.
### Anti-pattern
```go
@@ -246,6 +272,47 @@ func init() {
import _ "github.com/lib/pq" // driver registers itself
```
### When NOT to Use
**Don't use `init()` when:**
- The initialization can fail (you can't return errors from `init()`)
- The setup requires configuration or parameters (init takes no args)
- You need to control initialization order across packages
- It's a one-off application (not a library/driver) — just call setup in `main()`
**Over-application example:**
```go
// internal/metrics/metrics.go
func init() {
// Bad: init() hides this dependency, makes testing impossible,
// and panics if prometheus isn't reachable
prometheus.MustRegister(requestCounter)
prometheus.MustRegister(errorCounter)
prometheus.MustRegister(latencyHistogram)
}
```
**Better alternative:**
```go
// internal/metrics/metrics.go
func Register(reg prometheus.Registerer) error {
if err := reg.Register(requestCounter); err != nil {
return fmt.Errorf("registering request counter: %w", err)
}
// ...
return nil
}
// main.go
func main() {
if err := metrics.Register(prometheus.DefaultRegisterer); err != nil {
log.Fatal(err)
}
}
```
**Why:** `init()` is invisible, untestable, and can't fail gracefully. Use it only when the registration pattern demands it (database/sql drivers, codec registration) and failure is impossible.
### Anti-pattern
```go
@@ -486,6 +553,37 @@ func UserID(ctx context.Context) (int, bool) {
}
```
### When NOT to Use
**Don't use context values when:**
- The data is a required function parameter (pass it explicitly)
- The data controls behavior/logic (timeouts, retry counts) — use function args or config structs
- You're using it to avoid refactoring function signatures
- The value is large or expensive to retrieve (context isn't a cache)
**Over-application example:**
```go
// Passing database connection through context — it's required everywhere!
func HandleRequest(ctx context.Context) {
db := DatabaseFromContext(ctx) // nil if forgotten — runtime panic
users, err := db.Query(ctx, "SELECT ...")
}
```
**Better alternative:**
```go
// Make the dependency explicit
type Handler struct {
db *sql.DB
}
func (h *Handler) HandleRequest(ctx context.Context) {
users, err := h.db.QueryContext(ctx, "SELECT ...")
}
```
**Why:** Context values are untyped, invisible in function signatures, and can silently be nil. They're meant for *request-scoped metadata* that crosses API boundaries (trace IDs, auth tokens), not for dependency injection or configuration.
### Anti-pattern
```go