[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,858 @@
|
||||
# Common Go Bugs
|
||||
|
||||
→ See `samber/cc-skills-golang@golang-safety` skill for in-depth nil, slice, and map safety patterns.
|
||||
|
||||
## Nil Pointer Dereference
|
||||
|
||||
Pointers from external sources MUST be checked before dereferencing.
|
||||
|
||||
The most common Go panic. The stack trace tells you the exact line.
|
||||
|
||||
```go
|
||||
// 1. Uninitialized struct field
|
||||
type Server struct {
|
||||
logger *log.Logger // nil if not set in constructor
|
||||
}
|
||||
|
||||
// 2. Unchecked error return — if err != nil, val may be nil/zero
|
||||
val, err := doSomething()
|
||||
val.Method() // panic if doSomething returned nil val with an error
|
||||
|
||||
// 3. Map lookup returns zero value
|
||||
m := map[string]*Config{}
|
||||
cfg := m["missing"] // cfg is nil
|
||||
cfg.Timeout // panic
|
||||
|
||||
// 4. Type assertion without comma-ok
|
||||
var i interface{} = "hello"
|
||||
n := i.(int) // panic
|
||||
n, ok := i.(int) // ok == false, no panic
|
||||
```
|
||||
|
||||
## Interface Nil Gotcha
|
||||
|
||||
NEVER compare an interface to nil when it may contain a typed nil pointer.
|
||||
|
||||
A typed nil pointer inside an interface is **not** a nil interface:
|
||||
|
||||
```go
|
||||
type MyError struct{ msg string }
|
||||
func (e *MyError) Error() string { return e.msg }
|
||||
|
||||
func doWork() error {
|
||||
var err *MyError // typed nil pointer
|
||||
return err // returns non-nil interface containing nil pointer!
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := doWork(); err != nil {
|
||||
// This EXECUTES — the interface is non-nil
|
||||
fmt.Println(err) // panic: nil pointer in Error()
|
||||
}
|
||||
}
|
||||
|
||||
// FIX: return nil explicitly, not a typed nil variable
|
||||
func doWork() error {
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Variable Shadowing with `:=`
|
||||
|
||||
The `:=` short declaration creates a new variable in the inner scope instead of assigning to the outer one. Especially dangerous when shadowing `err`, because error handling silently breaks.
|
||||
|
||||
```go
|
||||
// BAD
|
||||
func doWork() error {
|
||||
var err error
|
||||
if condition {
|
||||
result, err := someFunc() // BUG: new err variable, doesn't set outer one
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
process(result)
|
||||
}
|
||||
return err // always nil — inner err was a different variable
|
||||
}
|
||||
|
||||
// GOOD
|
||||
func doWork() error {
|
||||
var err error
|
||||
if condition {
|
||||
var result ResultType
|
||||
result, err = someFunc() // assigns to outer err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
process(result)
|
||||
}
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
**Detect:** run the `golang.org/x/tools/go/analysis/passes/shadow` analyzer through your lint setup. The old shadow flag is not part of standard `go vet`.
|
||||
|
||||
## Slice and Map Gotchas
|
||||
|
||||
```go
|
||||
// 1. Nil map write panics
|
||||
var m map[string]int
|
||||
m["key"] = 1 // panic: assignment to entry in nil map
|
||||
// FIX: m := make(map[string]int)
|
||||
// Note: nil map reads are fine — they return zero value
|
||||
|
||||
// 2. Append may share underlying array
|
||||
a := []int{1, 2, 3}
|
||||
b := a[:2]
|
||||
b = append(b, 99) // overwrites a[2]!
|
||||
// FIX: full slice expression — b := a[:2:2] to limit capacity
|
||||
|
||||
// 3. Range variable capture in goroutine (Go < 1.22)
|
||||
for _, v := range items {
|
||||
go func() {
|
||||
process(v) // v is shared, will likely be last element
|
||||
}()
|
||||
}
|
||||
// FIX: pass as argument
|
||||
for _, v := range items {
|
||||
go func(v Item) { process(v) }(v)
|
||||
}
|
||||
// In Go 1.22+, loop variables are per-iteration (no fix needed)
|
||||
```
|
||||
|
||||
## Defer Gotchas
|
||||
|
||||
```go
|
||||
// 1. Arguments evaluated immediately
|
||||
x := 1
|
||||
defer fmt.Println(x) // prints 1, not 2
|
||||
x = 2
|
||||
|
||||
// 2. Defer in loop — doesn't run until function returns
|
||||
for _, f := range files {
|
||||
file, _ := os.Open(f)
|
||||
defer file.Close() // all Close() calls pile up until return
|
||||
}
|
||||
// FIX: wrap in closure
|
||||
for _, f := range files {
|
||||
func() {
|
||||
file, _ := os.Open(f)
|
||||
defer file.Close()
|
||||
// use file
|
||||
}()
|
||||
}
|
||||
|
||||
// 3. Named return + defer interaction
|
||||
func readFile() (err error) {
|
||||
f, err := os.Open("file.txt")
|
||||
if err != nil { return }
|
||||
defer func() {
|
||||
if closeErr := f.Close(); err == nil {
|
||||
err = closeErr // modifies named return
|
||||
}
|
||||
}()
|
||||
// ...
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Pitfalls
|
||||
|
||||
**Silent error swallowing** is the single most common source of "mysterious" bugs:
|
||||
|
||||
```go
|
||||
// BAD — silent failure
|
||||
result, _ := doSomething()
|
||||
json.Unmarshal(data, &config)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
|
||||
// GOOD — handle or propagate
|
||||
result, err := doSomething()
|
||||
if err != nil {
|
||||
return fmt.Errorf("doSomething: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
**Find ignored errors:**
|
||||
|
||||
```bash
|
||||
go vet ./...
|
||||
|
||||
# More thorough
|
||||
go get -tool github.com/kisielk/errcheck@latest
|
||||
go tool errcheck ./...
|
||||
```
|
||||
|
||||
**Error wrapping — use `%w`, not `%v`:**
|
||||
|
||||
```go
|
||||
return fmt.Errorf("reading config from %s: %v", path, err) // BAD — loses error chain
|
||||
return fmt.Errorf("reading config from %s: %w", path, err) // GOOD — preserves Is/As
|
||||
|
||||
// Check for specific errors — use errors.Is, not ==
|
||||
if err == sql.ErrNoRows { ... } // BAD — breaks if wrapped
|
||||
if errors.Is(err, sql.ErrNoRows) { ... } // GOOD — traverses chain
|
||||
|
||||
// Extract typed errors
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) { ... }
|
||||
```
|
||||
|
||||
## Context Misuse
|
||||
|
||||
```go
|
||||
// 1. Forgetting to cancel — leaks goroutines
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// Missing: defer cancel()
|
||||
|
||||
// 2. Using background context when you should propagate
|
||||
go doWork(context.Background()) // BAD — can't cancel from parent
|
||||
go doWork(ctx) // GOOD — respects parent cancellation
|
||||
|
||||
// 3. Not checking context error
|
||||
err := doWork(ctx)
|
||||
if err != nil {
|
||||
// Distinguish timeout from other errors
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
log.Printf("operation timed out")
|
||||
} else if ctx.Err() == context.Canceled {
|
||||
log.Printf("operation cancelled")
|
||||
} else {
|
||||
log.Printf("operation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Background work outliving request context
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// BAD — background work uses request context that cancels when client disconnects
|
||||
go processAsync(r.Context(), data)
|
||||
|
||||
// GOOD — derive a new context for background work (Go 1.21+)
|
||||
bgCtx := context.WithoutCancel(r.Context())
|
||||
go processAsync(bgCtx, data)
|
||||
}
|
||||
```
|
||||
|
||||
## Concurrent Map Read/Write (Fatal)
|
||||
|
||||
Maps MUST NOT be accessed concurrently without synchronization.
|
||||
|
||||
Unlike most Go runtime errors, a concurrent map read/write is a **fatal error** — it **cannot be caught with `recover()`** and crashes the entire process. Hard to catch in tests because it depends on timing.
|
||||
|
||||
```go
|
||||
// BAD — fatal: concurrent map read and map write
|
||||
m := make(map[string]int)
|
||||
go func() { m["key"] = 1 }() // concurrent write
|
||||
go func() { _ = m["key"] }() // concurrent read — fatal!
|
||||
|
||||
// GOOD — protect with mutex
|
||||
var mu sync.RWMutex
|
||||
m := make(map[string]int)
|
||||
go func() { mu.Lock(); m["key"] = 1; mu.Unlock() }()
|
||||
go func() { mu.RLock(); _ = m["key"]; mu.RUnlock() }()
|
||||
|
||||
// Or use sync.Map for read-heavy workloads with stable key sets
|
||||
```
|
||||
|
||||
**Detect:** `go test -race ./...` — always run in CI.
|
||||
|
||||
## Copying sync Types
|
||||
|
||||
Sync types MUST NEVER be copied — use pointer receivers and pass by pointer.
|
||||
|
||||
All `sync` types (`Mutex`, `RWMutex`, `WaitGroup`, `Once`, `Cond`, `Map`, `Pool`) must not be copied. Copying them via value receivers, function arguments, or struct assignment silently breaks synchronization.
|
||||
|
||||
```go
|
||||
// BAD — value receiver copies the Mutex
|
||||
type Counter struct {
|
||||
mu sync.Mutex
|
||||
count int
|
||||
}
|
||||
|
||||
func (c Counter) Increment() { // BUG: copies mutex on every call
|
||||
c.mu.Lock()
|
||||
c.count++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// GOOD — pointer receiver
|
||||
func (c *Counter) Increment() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.count++
|
||||
}
|
||||
```
|
||||
|
||||
**Detect:** `go vet` detects mutex copies. Apply to all sync types.
|
||||
|
||||
## WaitGroup.Add Inside Goroutine
|
||||
|
||||
If `wg.Add(1)` is called inside the goroutine instead of before it, `wg.Wait()` may return before all goroutines start — a race condition that passes tests most of the time but fails intermittently.
|
||||
|
||||
```go
|
||||
// BAD
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
wg.Add(1) // BUG: may run after wg.Wait() returns
|
||||
defer wg.Done()
|
||||
doWork()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// GOOD
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1) // called BEFORE launching the goroutine
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
doWork()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
## Missing Return After HTTP Error Response
|
||||
|
||||
After writing an error with `http.Error()`, execution continues. This can cause double writes, corrupted responses, or executing logic that should have been skipped.
|
||||
|
||||
```go
|
||||
// BAD
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
if !authorized(r) {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
// BUG: missing return — handler keeps executing
|
||||
}
|
||||
doSensitiveAction(r)
|
||||
}
|
||||
|
||||
// GOOD
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
if !authorized(r) {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
doSensitiveAction(r)
|
||||
}
|
||||
```
|
||||
|
||||
## JSON Pitfalls
|
||||
|
||||
### Numbers into `interface{}` become `float64`
|
||||
|
||||
When unmarshaling into `map[string]interface{}` or `interface{}`, all JSON numbers become `float64`. Type-asserting to `int` panics. Large integers (> 2^53) silently lose precision.
|
||||
|
||||
```go
|
||||
// BAD
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal([]byte(`{"id": 1234567890123456789}`), &result)
|
||||
id := result["id"].(int) // PANIC: it's float64, not int
|
||||
|
||||
// GOOD — use typed struct (preferred)
|
||||
type Response struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
// GOOD — use json.Number when you must use interface{}
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.UseNumber()
|
||||
var result map[string]interface{}
|
||||
dec.Decode(&result)
|
||||
id, _ := result["id"].(json.Number).Int64()
|
||||
```
|
||||
|
||||
### Unexported fields silently ignored
|
||||
|
||||
Fields starting with lowercase are invisible to `encoding/json`. Marshal produces empty output, unmarshal skips them — no error in either case.
|
||||
|
||||
```go
|
||||
// BAD
|
||||
type User struct {
|
||||
name string `json:"name"` // unexported — silently ignored!
|
||||
email string `json:"email"` // unexported — silently ignored!
|
||||
}
|
||||
u := User{name: "Alice", email: "alice@example.com"}
|
||||
data, _ := json.Marshal(u) // data is "{}" — no error
|
||||
|
||||
// GOOD
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
```
|
||||
|
||||
**Detect:** `go vet` warns when unexported fields have JSON struct tags.
|
||||
|
||||
## `strings.Trim` vs `strings.TrimPrefix`
|
||||
|
||||
`strings.Trim` treats its second argument as a **set of characters** to strip from both ends, not as a substring. This over-trims unexpectedly.
|
||||
|
||||
```go
|
||||
// BAD
|
||||
s := strings.Trim("application/json", "application/")
|
||||
// Result: "js" — stripped all chars in set {a,p,l,i,c,t,o,n,/} from both ends!
|
||||
|
||||
// GOOD
|
||||
s := strings.TrimPrefix("application/json", "application/")
|
||||
// Result: "json"
|
||||
```
|
||||
|
||||
Use `strings.TrimPrefix`/`strings.TrimSuffix` to remove substrings. Only use `strings.Trim` when you intend to strip a set of characters.
|
||||
|
||||
## String Length and Indexing
|
||||
|
||||
`len()` on strings returns bytes, not characters. Indexing returns a byte. For multi-byte UTF-8 characters, this gives wrong counts and corrupts data when slicing.
|
||||
|
||||
```go
|
||||
s := "Hello, 世界"
|
||||
fmt.Println(len(s)) // 13 (bytes), not 9 (characters)
|
||||
fmt.Println(s[:8]) // "Hello, \xe4" — corrupted! cuts a multi-byte rune
|
||||
|
||||
// FIX: use utf8.RuneCountInString for character count
|
||||
fmt.Println(utf8.RuneCountInString(s)) // 9
|
||||
|
||||
// FIX: convert to []rune for character-based slicing
|
||||
runes := []rune(s)
|
||||
fmt.Println(string(runes[:8])) // "Hello, 世"
|
||||
|
||||
// FIX: use for-range to iterate over characters (runes), not bytes
|
||||
for _, r := range s { ... } // iterates runes
|
||||
```
|
||||
|
||||
## `break` in `select`/`switch` Inside `for` Loop
|
||||
|
||||
A bare `break` inside a `select` or `switch` that is inside a `for` loop only exits the `select`/`switch`, not the loop.
|
||||
|
||||
```go
|
||||
// BAD
|
||||
for {
|
||||
select {
|
||||
case msg := <-ch:
|
||||
if msg == "quit" {
|
||||
break // BUG: only breaks the select, loop continues forever
|
||||
}
|
||||
process(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD — use labeled break
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case msg := <-ch:
|
||||
if msg == "quit" {
|
||||
break loop // breaks the for loop
|
||||
}
|
||||
process(msg)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Enum Zero Value with `iota`
|
||||
|
||||
When `iota` starts at 0, the zero value of the type (from uninitialized variables, zero-value struct fields, or missing JSON fields) is indistinguishable from the first constant.
|
||||
|
||||
```go
|
||||
// BAD
|
||||
type Status int
|
||||
const (
|
||||
Active Status = iota // 0 — same as zero value!
|
||||
Inactive // 1
|
||||
)
|
||||
type User struct {
|
||||
Status Status // zero value is Active — but was it intentional?
|
||||
}
|
||||
|
||||
// GOOD — reserve 0 for "unknown"
|
||||
type Status int
|
||||
const (
|
||||
StatusUnknown Status = iota // 0 — explicit unset sentinel
|
||||
StatusActive // 1
|
||||
StatusInactive // 2
|
||||
)
|
||||
```
|
||||
|
||||
## `recover()` Only Works in the Same Goroutine
|
||||
|
||||
`recover()` can only catch panics in the goroutine where it's deferred. A panic in a child goroutine will crash the entire program — no parent goroutine can catch it.
|
||||
|
||||
```go
|
||||
// BAD — recover() in main cannot catch panic in child goroutine
|
||||
func main() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("recovered:", r) // NEVER REACHED
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
panic("crash!") // crashes the whole program
|
||||
}()
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// GOOD — each goroutine must recover its own panics
|
||||
func main() {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("goroutine recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
panic("crash!") // recovered within this goroutine
|
||||
}()
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
```
|
||||
|
||||
## `os.Exit` Skips Deferred Functions
|
||||
|
||||
`os.Exit` terminates the process immediately. No deferred functions run — cleanup, flush, and close operations are skipped. `log.Fatal` calls `os.Exit(1)` internally and has the same problem.
|
||||
|
||||
```go
|
||||
// BAD — deferred cleanup never runs
|
||||
func main() {
|
||||
f, _ := os.Create("data.tmp")
|
||||
defer f.Close() // NEVER RUNS
|
||||
defer os.Remove(f.Name()) // NEVER RUNS
|
||||
|
||||
if err := process(); err != nil {
|
||||
log.Fatal(err) // calls os.Exit(1) — skips all defers!
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD — return from main instead, or restructure so defers run
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1) // defers in run() already ran when it returned
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
f, _ := os.Create("data.tmp")
|
||||
defer f.Close()
|
||||
return process()
|
||||
}
|
||||
```
|
||||
|
||||
## `time.Time` Comparison: `==` vs `.Equal()`
|
||||
|
||||
`time.Time` includes a monotonic clock reading. Two `time.Time` values representing the same instant may not be `==` if one has a monotonic component and the other doesn't (e.g., one from `time.Now()`, the other deserialized from JSON/database).
|
||||
|
||||
```go
|
||||
// BAD — may fail even for the same instant
|
||||
t1 := time.Now()
|
||||
data, _ := t1.MarshalJSON()
|
||||
var t2 time.Time
|
||||
t2.UnmarshalJSON(data)
|
||||
fmt.Println(t1 == t2) // false! t1 has monotonic, t2 doesn't
|
||||
|
||||
// GOOD — .Equal() ignores monotonic clock
|
||||
fmt.Println(t1.Equal(t2)) // true
|
||||
|
||||
// Also: strip monotonic explicitly when storing/comparing
|
||||
t1 = t1.Round(0) // strips monotonic reading
|
||||
```
|
||||
|
||||
## `sql.Rows` Must Be Closed
|
||||
|
||||
`sql.Rows` MUST call `rows.Close()` — always defer it immediately after the query.
|
||||
|
||||
Forgetting to close `sql.Rows` leaks database connections. The connection is held until `Rows` is garbage collected, but under load the connection pool exhausts first.
|
||||
|
||||
```go
|
||||
// BAD — connection leak if rows aren't closed
|
||||
rows, err := db.Query("SELECT id FROM users")
|
||||
if err != nil { return err }
|
||||
for rows.Next() {
|
||||
// ...
|
||||
}
|
||||
// rows never closed — connection leak!
|
||||
|
||||
// GOOD — always defer Close
|
||||
rows, err := db.Query("SELECT id FROM users")
|
||||
if err != nil { return err }
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
// ...
|
||||
}
|
||||
if err := rows.Err(); err != nil { // don't forget to check rows.Err()
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
Also: use `db.QueryRow()` for single-row queries and `db.Exec()` for non-SELECT statements (INSERT, UPDATE, DELETE). Using `db.Query()` for non-SELECT leaks connections because the returned `Rows` is never iterated/closed.
|
||||
|
||||
## Writing to a Closed Channel Panics
|
||||
|
||||
Sending to a closed channel panics. Reading from a closed channel returns the zero value immediately (with `ok == false`).
|
||||
|
||||
```go
|
||||
// BAD — panic: send on closed channel
|
||||
ch := make(chan int, 1)
|
||||
close(ch)
|
||||
ch <- 1 // panic!
|
||||
|
||||
// GOOD — only the sender should close, never the receiver
|
||||
// Use a done channel or context to signal completion
|
||||
func producer(ch chan<- int, done <-chan struct{}) {
|
||||
defer close(ch)
|
||||
for i := 0; ; i++ {
|
||||
select {
|
||||
case ch <- i:
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rule of thumb:** Only the sender closes the channel. If multiple senders, use a `sync.Once` or coordinate with a `sync.WaitGroup`.
|
||||
|
||||
## Closed Channel in `select` Causes Busy Loop
|
||||
|
||||
A closed channel is always ready to receive (returns zero value). In a `select`, this causes the case to fire continuously — a CPU-burning busy loop.
|
||||
|
||||
```go
|
||||
// BAD — after ch is closed, this loops at 100% CPU
|
||||
for {
|
||||
select {
|
||||
case v := <-ch: // fires continuously after ch closes
|
||||
process(v) // processes zero values forever
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD — nil the channel after it closes
|
||||
for {
|
||||
select {
|
||||
case v, ok := <-ch:
|
||||
if !ok {
|
||||
ch = nil // nil channel blocks forever in select — disables this case
|
||||
continue
|
||||
}
|
||||
process(v)
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `select` with `default` Can Spin CPU
|
||||
|
||||
A `select` with a `default` case never blocks. Inside a `for` loop, this creates a busy-wait spin loop that burns CPU.
|
||||
|
||||
```go
|
||||
// BAD — spins at 100% CPU waiting for a message
|
||||
for {
|
||||
select {
|
||||
case msg := <-ch:
|
||||
process(msg)
|
||||
default:
|
||||
// runs immediately when ch has nothing — tight loop!
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD — remove default to block until a message arrives
|
||||
for {
|
||||
select {
|
||||
case msg := <-ch:
|
||||
process(msg)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD — if you need non-blocking check, add a small sleep or ticker
|
||||
for {
|
||||
select {
|
||||
case msg := <-ch:
|
||||
process(msg)
|
||||
default:
|
||||
time.Sleep(10 * time.Millisecond) // yield CPU
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integer Conversion Silently Truncates
|
||||
|
||||
Go integer conversions don't check for overflow — they silently truncate. This is especially dangerous when converting from user input or external data.
|
||||
|
||||
```go
|
||||
// BAD — silent truncation
|
||||
var big int64 = 256
|
||||
small := int8(big)
|
||||
fmt.Println(small) // 0 — silently overflowed!
|
||||
|
||||
var n int64 = math.MaxInt64
|
||||
n32 := int32(n)
|
||||
fmt.Println(n32) // -1 — silently wrapped!
|
||||
|
||||
// GOOD — check bounds before converting
|
||||
func safeIntToInt32(n int64) (int32, error) {
|
||||
if n < math.MinInt32 || n > math.MaxInt32 {
|
||||
return 0, fmt.Errorf("value %d overflows int32", n)
|
||||
}
|
||||
return int32(n), nil
|
||||
}
|
||||
```
|
||||
|
||||
## `filepath.Join` Does Not Prevent Path Traversal
|
||||
|
||||
`filepath.Join` cleans the path (resolves `..`) but doesn't prevent escaping the base directory. User-supplied paths can traverse outside the intended root.
|
||||
|
||||
```go
|
||||
// BAD — user can escape the base directory
|
||||
base := "/srv/files"
|
||||
userInput := "../../etc/passwd"
|
||||
path := filepath.Join(base, userInput)
|
||||
// path = "/etc/passwd" — escaped!
|
||||
|
||||
// GOOD (Go 1.24+) — confine access to the base directory
|
||||
root, err := os.OpenRoot("/srv/files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer root.Close()
|
||||
file, err := root.Open(userInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
```
|
||||
|
||||
For Go <1.24, use a lexical fallback only when `os.Root` is unavailable:
|
||||
|
||||
```go
|
||||
func safePath(base, userInput string) (string, error) {
|
||||
if userInput == "" || filepath.IsAbs(userInput) || !filepath.IsLocal(userInput) {
|
||||
return "", fmt.Errorf("invalid relative path: %q", userInput)
|
||||
}
|
||||
|
||||
path := filepath.Join(base, userInput)
|
||||
rel, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("checking path: %w", err)
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("path traversal attempt: %s", userInput)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Pointer Receiver Interface Satisfaction
|
||||
|
||||
A value of type `T` cannot satisfy an interface that requires methods with `*T` receivers. But `*T` satisfies interfaces requiring either `T` or `*T` methods.
|
||||
|
||||
```go
|
||||
type Sizer interface {
|
||||
Size() int
|
||||
}
|
||||
|
||||
type File struct{ size int }
|
||||
func (f *File) Size() int { return f.size } // pointer receiver
|
||||
|
||||
var s Sizer
|
||||
s = File{} // COMPILE ERROR: File does not implement Sizer (*File does)
|
||||
s = &File{} // OK — *File has the Size method
|
||||
|
||||
// This is because the compiler can't always take the address of a value
|
||||
// (e.g., map values, return values). Pointer receiver = pointer required.
|
||||
```
|
||||
|
||||
## `regexp.MustCompile` in Hot Path
|
||||
|
||||
Long-lived regexp MUST be compiled once at package level — not inside functions called repeatedly. Short-lived regexp used once (e.g., in a CLI or test) are acceptable inline.
|
||||
|
||||
`regexp.MustCompile` compiles a regex every call. In a hot path (loop, HTTP handler), this is expensive and wasteful.
|
||||
|
||||
```go
|
||||
// BAD — recompiles regex on every call
|
||||
func isEmail(s string) bool {
|
||||
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
return re.MatchString(s)
|
||||
}
|
||||
|
||||
// GOOD — compile once at package level
|
||||
var emailRe = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
|
||||
func isEmail(s string) bool {
|
||||
return emailRe.MatchString(s)
|
||||
}
|
||||
```
|
||||
|
||||
## `init()` Ordering Is Fragile
|
||||
|
||||
`init()` functions run in source file order within a package, and in dependency order across packages. But relying on this order creates brittle, hard-to-debug initialization sequences. Multiple `init()` in the same file run top-to-bottom, but across files it's alphabetical by filename — adding a file can change the order.
|
||||
|
||||
```go
|
||||
// BAD — init() depends on another init() having run first
|
||||
var db *sql.DB
|
||||
|
||||
func init() {
|
||||
// Assumes config init() already ran — fragile!
|
||||
db, _ = sql.Open("postgres", config.DatabaseURL)
|
||||
}
|
||||
|
||||
// GOOD — use explicit initialization
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
db := setupDatabase(cfg)
|
||||
startServer(db)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer explicit initialization in `main()` over `init()`. Use `init()` only for truly self-contained setup (registering drivers, codecs).
|
||||
|
||||
## Map Iteration Order Is Random
|
||||
|
||||
Go deliberately randomizes map iteration order. Code that assumes a specific order will produce inconsistent results.
|
||||
|
||||
```go
|
||||
// BAD — output order is random every run
|
||||
m := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
for k, v := range m {
|
||||
fmt.Printf("%s=%d ", k, v) // different order each time!
|
||||
}
|
||||
|
||||
// GOOD — sort keys when order matters
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Printf("%s=%d ", k, m[k])
|
||||
}
|
||||
```
|
||||
|
||||
This is especially dangerous in tests (non-deterministic output comparison), serialization (non-deterministic JSON/output), and logging (confusing diffs).
|
||||
|
||||
## `fallthrough` in `switch` Executes Unconditionally
|
||||
|
||||
Unlike C, Go's `switch` cases don't fall through by default. But when you explicitly use `fallthrough`, it executes the **next case body unconditionally** — it does not check the next case's condition.
|
||||
|
||||
```go
|
||||
// Surprising: fallthrough doesn't check the next condition
|
||||
switch x := 5; {
|
||||
case x > 10:
|
||||
fmt.Println(">10")
|
||||
fallthrough
|
||||
case x > 0:
|
||||
fmt.Println(">0")
|
||||
fallthrough
|
||||
case x < 0:
|
||||
fmt.Println("<0") // EXECUTES even though 5 is not < 0!
|
||||
}
|
||||
// Output: >0, <0
|
||||
|
||||
// fallthrough is rarely needed. Prefer listing multiple values:
|
||||
switch status {
|
||||
case "active", "enabled":
|
||||
enable()
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user