[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# Slice and Map Safety Deep Dive
|
||||
|
||||
## Range Loop Variable Capture
|
||||
|
||||
### Pre-Go 1.22: shared loop variable
|
||||
|
||||
NEVER store pointers to loop variables in Go < 1.22 — capture by value. Before Go 1.22, the range loop variable was reused across iterations. Capturing it in a closure or storing its address caused all references to point to the final value:
|
||||
|
||||
```go
|
||||
// ✗ Bad (pre-1.22) — all goroutines see the last value of v
|
||||
var funcs []func()
|
||||
for _, v := range []string{"a", "b", "c"} {
|
||||
funcs = append(funcs, func() { fmt.Println(v) })
|
||||
}
|
||||
for _, f := range funcs {
|
||||
f() // prints "c", "c", "c"
|
||||
}
|
||||
|
||||
// ✓ Fix (pre-1.22) — shadow the variable
|
||||
for _, v := range []string{"a", "b", "c"} {
|
||||
v := v // re-declare v in inner scope
|
||||
funcs = append(funcs, func() { fmt.Println(v) })
|
||||
}
|
||||
```
|
||||
|
||||
### Go 1.22+: per-iteration scoping
|
||||
|
||||
Go 1.22 changed loop variable semantics — each iteration creates a new variable. The closure bug no longer occurs. However, if your module targets `go 1.21` or earlier in `go.mod`, the old behavior applies. Check your `go.mod` version.
|
||||
|
||||
## Storing Pointer to Loop Variable
|
||||
|
||||
The same pre-1.22 issue applies to storing `&v`:
|
||||
|
||||
```go
|
||||
// ✗ Bad (pre-1.22) — all pointers point to the same address
|
||||
type Item struct{ Name string }
|
||||
items := []Item{{Name: "a"}, {Name: "b"}}
|
||||
var ptrs []*Item
|
||||
for _, item := range items {
|
||||
ptrs = append(ptrs, &item) // all point to same loop variable
|
||||
}
|
||||
// ptrs[0].Name == "b", ptrs[1].Name == "b"
|
||||
|
||||
// ✓ Good — take address of the slice element directly
|
||||
for i := range items {
|
||||
ptrs = append(ptrs, &items[i])
|
||||
}
|
||||
```
|
||||
|
||||
In Go 1.22+, `&item` is safe because each iteration has its own `item`. But taking `&items[i]` is still clearer and avoids a copy.
|
||||
|
||||
## Slice Header vs Backing Array
|
||||
|
||||
A slice is a 3-word struct: `{pointer, length, capacity}`. Multiple slices can share the same backing array:
|
||||
|
||||
```
|
||||
a := make([]int, 3, 5)
|
||||
┌─────┬─────┬─────┐
|
||||
│ ptr │ len=3│cap=5│ ← slice header for a
|
||||
└──┬──┴─────┴─────┘
|
||||
│
|
||||
▼
|
||||
┌───┬───┬───┬───┬───┐
|
||||
│ 0 │ 0 │ 0 │ │ │ ← backing array (5 elements)
|
||||
└───┴───┴───┴───┴───┘
|
||||
|
||||
b := a[1:2]
|
||||
┌─────┬─────┬─────┐
|
||||
│ ptr │ len=1│cap=4│ ← slice header for b (shares backing array)
|
||||
└──┬──┴─────┴─────┘
|
||||
│ (points to a[1])
|
||||
```
|
||||
|
||||
This is why `append(a, x)` can affect `b` if `a` has spare capacity. Use the full slice expression `a[:len(a):len(a)]` to set cap == len and force a new allocation on append.
|
||||
|
||||
## Subslice Retains Full Backing Array
|
||||
|
||||
Subslice retention: MUST use `slices.Clone` or `copy` when keeping a small slice from a large backing array. Slicing a large slice for a small piece prevents GC of the entire backing array:
|
||||
|
||||
```go
|
||||
// ✗ Bad — small keeps the entire 1MB array alive
|
||||
func getHeader(data []byte) []byte {
|
||||
return data[:64] // shares backing array with data
|
||||
}
|
||||
|
||||
// ✓ Good — copy to release the large array
|
||||
func getHeader(data []byte) []byte {
|
||||
header := make([]byte, 64)
|
||||
copy(header, data[:64])
|
||||
return header
|
||||
}
|
||||
|
||||
// ✓ Good (Go 1.21+) — use slices.Clone
|
||||
import "slices"
|
||||
|
||||
func getHeader(data []byte) []byte {
|
||||
return slices.Clone(data[:64])
|
||||
}
|
||||
```
|
||||
|
||||
## Standard Library Clone Helpers (Go 1.21+)
|
||||
|
||||
```go
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Shallow copy a slice
|
||||
clone := slices.Clone(original)
|
||||
|
||||
// Shallow copy a map
|
||||
clone := maps.Clone(original)
|
||||
```
|
||||
|
||||
These are the preferred way to make defensive copies. They are clearer than manual `make` + `copy` and handle nil inputs correctly (returning nil, not an empty collection).
|
||||
|
||||
## Map Iteration Order
|
||||
|
||||
Map iteration order MUST NOT be depended upon — it is randomized by the runtime:
|
||||
|
||||
```go
|
||||
// ✗ Bad — output order changes between runs
|
||||
m := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
for k, v := range m {
|
||||
fmt.Printf("%s=%d ", k, v) // could be "b=2 a=1 c=3" or any permutation
|
||||
}
|
||||
|
||||
// ✓ Good (Go 1.23+) — sort keys when order matters
|
||||
keys := slices.Sorted(maps.Keys(m))
|
||||
for _, k := range keys {
|
||||
fmt.Printf("%s=%d ", k, m[k])
|
||||
}
|
||||
```
|
||||
|
||||
## Deleting During Iteration
|
||||
|
||||
### Maps — safe
|
||||
|
||||
Deleting map entries during `range` is explicitly safe in Go:
|
||||
|
||||
```go
|
||||
// ✓ Safe — defined behavior
|
||||
for k, v := range m {
|
||||
if shouldDelete(v) {
|
||||
delete(m, k) // safe during range
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Slices — needs care
|
||||
|
||||
Deleting from a slice during iteration requires index management:
|
||||
|
||||
```go
|
||||
// ✗ Bad — skips elements after deletion
|
||||
for i, v := range items {
|
||||
if shouldDelete(v) {
|
||||
items = append(items[:i], items[i+1:]...) // shifts elements, next iteration skips one
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ Good — iterate backwards
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
if shouldDelete(items[i]) {
|
||||
items = append(items[:i], items[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ Good (Go 1.21+) — use slices.DeleteFunc
|
||||
items = slices.DeleteFunc(items, shouldDelete)
|
||||
```
|
||||
|
||||
## Comparing Slices and Maps
|
||||
|
||||
Slice/map comparison MUST use `slices.Equal`/`maps.Equal` (Go 1.21+), NEVER `==` (which doesn't compile for slices). Use standard library helpers:
|
||||
|
||||
```go
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// ✓ Good (Go 1.21+)
|
||||
slices.Equal(a, b) // element-wise comparison
|
||||
maps.Equal(m1, m2) // key-value comparison
|
||||
|
||||
// For custom comparison
|
||||
slices.EqualFunc(a, b, func(x, y Item) bool {
|
||||
return x.ID == y.ID
|
||||
})
|
||||
```
|
||||
|
||||
→ See `samber/cc-skills-golang@golang-modernize` skill for Go 1.22+ loop variable semantics.
|
||||
Reference in New Issue
Block a user