A Go linter that suggests using value types instead of pointers for small structs.
The name is a pun: "pointer" + "less" = "pointless" (also meaning "unnecessary").
go install github.com/mickamy/pointless@latest# Basic usage
pointless ./...
# Change threshold (default: 1024 bytes)
pointless -threshold 512 ./...// Warning: consider returning value instead of pointer
func GetUser() *User { ... }
// OK: may return nil
func FindUser(id int) *User {
if notFound {
return nil
}
return &user
}
// OK: struct is large (> 1024 bytes)
func GetData() *LargeData { ... }// Warning: consider using value receiver
func (u *User) FullName() string {
return u.FirstName + " " + u.LastName // read-only
}
// OK: mutates receiver
func (u *User) SetName(name string) {
u.Name = name
}// Warning: consider using []User instead of []*User
func GetUsers() []*User { ... }
users := make([]*User, 100)
// OK: uses nil as element
if users[i] == nil { ... }// Too difficult to determine intent
func (r *UserRepo) Update(u *User) error
func Process(u *User) error// nolint:pointless
func GetUser() *User { ... }
// pointless:ignore
func GetUser() *User { ... }
// nolint
func GetUser() *User { ... }Create .pointless.yaml or .pointless.yml in your project root:
threshold: 1024 # bytes
exclude:
- "*_test.go"
- "vendor/**"# .github/workflows/lint.yaml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: pointless
run: |
go install github.com/mickamy/pointless@latest
pointless ./...[]User (value slice):
┌────────┬────────┬────────┬────────┐
│ User 0 │ User 1 │ User 2 │ User 3 │ ← contiguous memory
└────────┴────────┴────────┴────────┘
[]*User (pointer slice):
┌────┬────┬────┬────┐
│ * │ * │ * │ * │ ← pointer array
└─┬──┴─┬──┴─┬──┴─┬──┘
│ │ │ │
▼ ▼ ▼ ▼
┌────┐┌────┐┌────┐┌────┐ ← scattered allocations
│User││User││User││User│
└────┘└────┘└────┘└────┘
- CPU cache efficiency - Contiguous memory improves spatial locality
- Fewer allocations - 1 allocation vs N+1 allocations
- Lower GC pressure - Fewer pointers to track
- Immutability - Prevents unintended mutations
- Go goroutine stacks start at 2KB and grow automatically
- A struct with ~50 fields typically uses 400-800 bytes
- Contiguous memory copies are efficiently handled by CPU (SIMD-optimized memcpy)
- Pointer indirection can cause cache misses, which may be more expensive than copying
- Slice/map fields only copy the header, not the underlying data