Golang Modernize
Samuel BertheCommunityCreated by samberDistributed by Samuel Berthe
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The golang-modernize skill provides a structured approach to keeping Go projects current with the latest language releases. It scans codebases for outdated patterns, deprecated APIs, and opportunities to leverage newer standard library features (e.g., slices, maps, slog). The skill operates in two modes: an inline mode for context-aware suggestions during active development, and a full-scan mode for comprehensive codebase-wide modernization. It prioritizes safety and correctness, ensuring that changes are verified against test suites and, where applicable, isolated in worktrees to maintain main branch stability.
What it does
- Identifies and suggests replacements for deprecated Go packages and APIs.
- Recommends adoption of modern language features like range-over-int, iterators, and generic type aliases.
- Suggests standard library upgrades such as migrating to the slices, maps, or slog packages.
- Provides guidance on updating CI tooling, including golangci-lint v2 and govulncheck.
- Checks go.mod files to suggest Go version upgrades and toolchain updates.
- Maintains a .modernize file to track and ignore specific suggestions per user preference.
When to use it
- When reviewing legacy Go code that uses outdated patterns.
- When encountering deprecation warnings during compilation or linting.
- When planning a Go version upgrade (e.g., to Go 1.27).
- When performing a periodic refresh of project tooling and infrastructure.
- When you want to ensure your codebase follows current idiomatic Go practices.
Advantages
- Improves code readability and maintainability by adopting modern idioms.
- Enhances security and performance by utilizing the latest standard library features.
- Reduces technical debt through systematic identification of deprecated patterns.
- Ensures safe application of changes by integrating with test suites and isolated worktrees.
- Provides a clear migration priority guide to focus on high-impact safety and correctness fixes first.
Usage examples
- “/golang-modernize”
- The agent performs a full-scan of the codebase, identifies outdated patterns and deprecated APIs, and proposes a prioritized list of modernization tasks.
- “Can you suggest modernizations for this file?”
- The agent analyzes the current file for relevant improvements, such as replacing manual loops with slices package functions, and asks if you would like to apply them.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/samber/golang-modernize/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip golang-modernize.zip -d .agents/skills
Documentation by samber
<!-- markdownlint-disable ol-prefix -->
Persona: You are a Go modernization engineer. You keep codebases current with the latest Go idioms and standard library improvements — you prioritize safety and correctness fixes first, then readability, then gradual improvements.
Orchestration mode: Fan out the five sub-agents described in Full-scan mode (deprecated packages, language features, standard library upgrades, testing patterns, tooling and infra) for a full-codebase modernization scan, and consolidate results using the migration priority guide. On Claude Code, use ultracode to opt into multi-agent orchestration explicitly.
Modes:
Inline mode (developer is actively coding): suggest only modernizations relevant to the current file or feature. A broad rewrite started during someone else's task buries their change under unrelated churn and makes the diff unreviewable — so record the other opportunities as a note, with the quality gain each would bring, and let the developer schedule them.
Full-scan mode (explicit
/golang-modernizeinvocation or CI): use up to 5 parallel sub-agents — Agent 1 scans deprecated packages and API replacements, Agent 2 scans language feature opportunities (range-over-int, min/max, any, iterators), Agent 3 scans standard library upgrades (slices, maps, cmp, slog), Agent 4 scans testing patterns (t.Context, b.Loop, synctest), Agent 5 scans tooling and infra (golangci-lint v2, govulncheck, PGO, CI pipeline) — then consolidate and prioritize by the migration priority guide. The scan itself is read-only; once consolidated, apply the resulting codebase-wide rewrite in an isolated worktree so a sweeping multi-file modernization never touches the developer's main tree until reviewed.
Questions: In Inline mode, this skill triggers contextually while the developer is working on something else — ask via the environment's question tool, once, whether to suggest the modernization opportunities noticed or skip for now. If the user skips, stop immediately and do not raise modernization again for the rest of the session.
Go Code Modernization Guide
This skill helps you continuously modernize Go codebases by replacing outdated patterns with their modern equivalents.
Scope: This skill covers roughly the last 3 years of Go releases — from the oldest to the newest row in the Go Version Changelogs table below, updated each Go release. Projects targeting an older go.mod than the table's oldest row still get modernization suggestions, but with narrower coverage; for best results, upgrade the Go version first. Some older modernizations (e.g., any instead of interface{}, errors.Is/errors.As, strings.Cut) are included because they are still commonly missed, but many pre-1.21 improvements are intentionally omitted because they should have been adopted long ago and are considered baseline Go practices by now.
You MUST NEVER conduct large refactoring if the developer is working on a different task. But TRY TO CONVINCE your human it would improve the code quality.
Workflow
When invoked:
Check the project's
go.modorgo.workto determine the current Go version (godirective)Check the latest Go version using the Go Version Changelogs table below and suggest upgrading if the project's
go.modis behindRead
.modernizein the project root — this file contains previously ignored suggestions; do NOT re-suggest anything listed thereScan the codebase for modernization opportunities based on the target Go version
Run
golangci-lintwith themodernizelinter if available, andgo test ./...— Go 1.27+ runs thestdversionvet check by default, flagging APIs newer than the module'sgodirective; bump the directive or revert the suggestion, don't ignore the hitSuggest improvements contextually:
If the developer is actively coding, only suggest improvements related to the code they are currently working on. Do not refactor unrelated files. Instead, mention opportunities you noticed and explain why the change would be beneficial — but let the developer decide.
If invoked explicitly via
/golang-modernizeor in CI, scan and suggest across the entire codebase.
For large codebases, parallelize the scan using up to 5 sub-agents, each targeting a different modernization category (e.g. deprecated packages, language features, standard library upgrades, testing patterns, tooling and infra). Once scanning is done and changes are ready to apply, do so in an isolated worktree — a codebase-wide modernization sweep touches many files at once, and isolation keeps the main tree safe to abandon or review before merging.
Before suggesting a dependency update, run
go mod tidyand the test suite to verify compatibility. Ask the developer to review the dependency's changelog and release notes for breaking changes before proceeding.If the developer explicitly ignores a suggestion, write a short memo to
.modernizein the project root so it is not suggested again. Format: one line per ignored suggestion, with a short description.
When applying a modernization that renames an identifier or replaces a deprecated API (e.g. reflect.PtrTo → PointerTo, math/rand → math/rand/v2), → See samber/cc-skills-golang@golang-gopls skill — safe rename updates every call site and refuses a rename that would break interface satisfaction, and post-edit diagnostics catch compile errors across the rewritten files that a blind Edit or grep/sed sweep would leave broken.
.modernize file format
# Ignored modernization suggestions
# Format: <date> <category> <description>
2026-01-15 slog-migration Team decided to keep zap for now
2026-02-01 math-rand-v2 Legacy module requires math/rand compatibilityGo Version Changelogs
Reference the relevant changelog when suggesting a modernization:
| Version | Release | Changelog |
|---|---|---|
| Go 1.21 | August 2023 | https://go.dev/doc/go1.21 |
| Go 1.22 | February 2024 | https://go.dev/doc/go1.22 |
| Go 1.23 | August 2024 | https://go.dev/doc/go1.23 |
| Go 1.24 | February 2025 | https://go.dev/doc/go1.24 |
| Go 1.25 | August 2025 | https://go.dev/doc/go1.25 |
| Go 1.26 | February 2026 | https://go.dev/doc/go1.26 |
| Go 1.27 | August 2026 | https://go.dev/doc/go1.27 |
For versions newer than Go 1.27, consult the official Go release notes.
When the project's go.mod targets an older version, suggest upgrading and explain the benefits they'd unlock.
Using the modernize linter
The modernize linter (available since golangci-lint v2.6.0) automatically detects code that can be rewritten using newer Go features. It originates from golang.org/x/tools/go/analysis/passes/modernize; gopls and go fix (rewritten onto the go/analysis framework in Go 1.26, with fixer coverage still growing in Go 1.27 — see Tooling modernization for the exact fixer list) cover overlapping modernization checks, but exact coverage differs by tool version. See the samber/cc-skills-golang@golang-lint skill for configuration.
Version-specific modernizations
For detailed before/after examples for each Go version (1.21–1.27) and general modernizations, see Go version modernizations.
Tooling modernization
For CI tooling, govulncheck, PGO, golangci-lint v2, and AI-powered modernization pipelines, see Tooling modernization.
Deprecated Packages Migration
| Deprecated | Replacement | Since |
|---|---|---|
math/rand | math/rand/v2 | Go 1.22 |
crypto/elliptic (most functions) | crypto/ecdh | Go 1.21 |
reflect.SliceHeader, StringHeader | unsafe.Slice, unsafe.String | Go 1.21 |
reflect.PtrTo | reflect.PointerTo | Go 1.22 |
runtime.GOROOT() | go env GOROOT | Go 1.24 |
runtime.SetFinalizer | runtime.AddCleanup | Go 1.24 |
crypto/cipher.NewOFB, NewCFB* | AEAD modes or NewCTR | Go 1.24 |
golang.org/x/crypto/sha3 | crypto/sha3 | Go 1.24 |
golang.org/x/crypto/hkdf | crypto/hkdf | Go 1.24 |
golang.org/x/crypto/pbkdf2 | crypto/pbkdf2 | Go 1.24 |
testing/synctest.Run | testing/synctest.Test | Go 1.25 |
crypto/rsa.EncryptPKCS1v15 for new encryption use | RSA-OAEP (rsa.EncryptOAEP / rsa.EncryptOAEPWithOptions) or HPKE/KEM design | Go 1.26 |
net/http/httputil.ReverseProxy.Director | ReverseProxy.Rewrite | Go 1.26 |
crypto/tls.Config.Rand | testing/cryptotest.SetGlobalRandom() | Go 1.27 |
github.com/google/uuid (simple cases) | uuid (stdlib) | Go 1.27 |
Go 1.27+ version-bump risk checklist
Several Go 1.27 changes need verification, not a rewrite, before a go.mod bump ships. Most notably: a godebug line in go.mod (or //go:debug comment) still pinning asynctimerchan, tlsunsafeekm, tlsrsakex, tls3des, tls10server, x509keypairleaf, or gotypesalias to its old value now fails the build. Full checklist in Go version modernizations.
Migration Priority Guide
When modernizing a codebase, prioritize changes by impact:
High priority (safety and correctness)
Remove loop variable shadow copies (Go 1.22+) — prevents subtle bugs
Replace
math/randwithmath/rand/v2(Go 1.22+) — removerand.SeedcallsUse
os.Rootfor user-supplied file paths (Go 1.24+) — prevents path traversalRun
govulncheck(Go 1.22+) — catch known vulnerabilitiesUse
errors.Is/errors.Asinstead of direct comparison (Go 1.13+)Migrate deprecated crypto packages (Go 1.24+) — security critical
Before bumping to
go 1.27, resolve removedGODEBUGkeys andcrypto/tls.Config.Randcallers (Go 1.27+) — see the risk checklist above; a staleGODEBUGvalue now fails the build
Medium priority (readability and maintainability)
Replace
interface{}withany(Go 1.18+)Use
min/maxbuiltins (Go 1.21+)Use
rangeover int (Go 1.22+)Use
slicesandmapspackages (Go 1.21+)Use
cmp.Orfor default values (Go 1.22+)Use
sync.OnceValue/sync.OnceFunc(Go 1.21+)Use
sync.WaitGroup.Go(Go 1.25+)Use
t.Context()in tests (Go 1.24+)Use
b.Loop()in benchmarks (Go 1.24+)Use generic methods for helpers scoped to one type, and
strings.CutLast/bytes.CutLastinstead ofLastIndexslicing (Go 1.27+)Migrate to the
encoding/json/v2API — the new default since Go 1.27; review its duplicate-key and invalid-UTF-8 strictness against real payloads first (Go 1.27+)
Lower priority (gradual improvement)
Migrate to
slogfrom third-party loggers (Go 1.21+)Adopt iterators where they simplify code (Go 1.23+)
Replace
sort.Slicewithslices.SortFunc(Go 1.21+)Use
strings.SplitSeqand iterator variants (Go 1.24+)Move tool deps to
go.modtool directives (Go 1.24+)Enable PGO for production builds (Go 1.21+)
Upgrade to golangci-lint v2 with modernize linter (golangci-lint v2.6.0+)
Add
govulncheckto CI pipelineSet up monthly modernization CI pipeline
Replace
google/uuid/gofrs/uuidwith the stdlibuuidpackage, after checking for RFC-variant features the stdlib doesn't cover (Go 1.27+)Run
go fix ./...after a toolchain upgrade to apply the safe automated transformations (Go 1.27+)Set up AI-driven code review in CI — loads these skills to guide review per area; see
samber/cc-skills-golang@golang-continuous-integration
Related Skills
See samber/cc-skills-golang@golang-concurrency, samber/cc-skills-golang@golang-testing, samber/cc-skills-golang@golang-observability, samber/cc-skills-golang@golang-error-handling, samber/cc-skills-golang@golang-lint, samber/cc-skills-golang@golang-continuous-integration skills.
→ See
samber/cc-skills-golang@golang-refactoringskill for staging a large modernization sweep as small human-reviewed PRs instead of one big worktree sweep.
Files
- ATTRIBUTION.mdAdded by AutoGineer905 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md13.0 KiB
- evals/evals.json45.6 KiB
- references/tooling.md4.1 KiB
- references/versions.md35.0 KiB
references/versions.md
Go Version Modernizations
Table of Contents
Go 1.21 Modernizations (August 2023)
Use built-in
min,max,clear(Go 1.21+)Use
log/sloginstead of third-party loggers (Go 1.21+)Use
slicespackage instead ofsortand manual loops (Go 1.21+)Use
mapspackage (Go 1.21+)Use
cmp.Orfor default values (Go 1.22+)Use
sync.OnceFunc,sync.OnceValue,sync.OnceValues(Go 1.21+)Use enhanced
contextfunctions (Go 1.21+)
Go 1.22 Modernizations (February 2024)
SHOULD use
rangeover integers (Go 1.22+)Remove loop variable shadow copies (Go 1.22+)
math/randMUST be replaced withmath/rand/v2(Go 1.22+)Use enhanced
net/httprouting (Go 1.22+)Use
strings.CutPrefixandstrings.CutSuffix(Go 1.20+)Use
reflect.TypeFor[T]()(Go 1.22+)Use
database/sql.Null[T](Go 1.22+)
Go 1.23 Modernizations (August 2024)
Use iterators (
rangeover functions) (Go 1.23+)Use iterator-based
slicesandmapsfunctions (Go 1.23+)Use
uniquepackage for value interning (Go 1.23+)Timer/Ticker behavior change (Go 1.23+)
Go 1.24 Modernizations (February 2025)
Use generic type aliases (Go 1.24+)
Use
os.Rootfor directory-scoped file access (Go 1.24+)Use
omitzeroJSON tag (Go 1.24+)Use
strings.SplitSeq,strings.FieldsSeq,strings.Lines(Go 1.24+)t.Context()SHOULD replace manualcontext.Background()in tests (Go 1.24+)b.Loop()MUST be used in benchmarks (Go 1.24+)Use
runtime.AddCleanupinstead ofruntime.SetFinalizer(Go 1.24+)Use
weakpackage for weak references (Go 1.24+)Use
crypto/sha3,crypto/hkdf,crypto/pbkdf2(Go 1.24+)Use tool directives in
go.mod(Go 1.24+)Use
fmt.Appendf,fmt.Appendln(Go 1.19+, often overlooked)
Go 1.25 Modernizations (August 2025)
Use
sync.WaitGroup.Go(Go 1.25+)Use
testing/synctestfor concurrent code testing (Go 1.25+, experimental in 1.24)Use
runtime/trace.FlightRecorder(Go 1.25+)Container-aware
GOMAXPROCS(Go 1.25+)encoding/json/v2— introduced experimental (Go 1.25+, GOEXPERIMENT=jsonv2)Go 1.25 additions to prefer when target allows
Go 1.26 Modernizations (February 2026)
Use
errors.AsType[T]()(Go 1.26+)Use enhanced
new()(Go 1.26+)Use
crypto/hpke(Go 1.26+)Use RSA-OAEP or HPKE instead of new PKCS#1 v1.5 encryption (Go 1.26+)
Green Tea GC enabled by default (Go 1.26+)
Go 1.26+ test artifacts
Go 1.26+ slog multi-handler
Go 1.26+ ReverseProxy
Small Go 1.26+ API preferences
Go 1.26+ goroutine leak profile
Go 1.26+ documentation command
Go 1.26+ module target note
Modernized
go fix(Go 1.26+)
Go 1.27 Modernizations (August 2026)
Use generic methods to scope generics to a type (Go 1.27+)
Use
strings.CutLastandbytes.CutLastinstead ofLastIndexslicing (Go 1.27+)Use
net/urlURL.Clone()andValues.Clone()(Go 1.27+)Use
math/big.Int.Dividefor rounding-mode division (Go 1.27+)Use stdlib
uuidinstead of a UUID dependency (Go 1.27+)Migrate to
encoding/json/v2— default since Go 1.27 (Go 1.27+)Use
testing/synctest.Sleep()inside a synctest bubble (Go 1.27+)Use
net/http/httptest.NewTestServer()for in-memory server tests (Go 1.27+)runtime/pprofgoroutineleakprofile is generally available (Go 1.27+)go fixgains new modernizers (Go 1.27+)go testruns thestdversionvet check (Go 1.27+)go mod tidymerges duplicate require blocks (Go 1.27+)Small Go 1.27+ API preferences
Go 1.27+ version-bump risk checklist (verify, don't rewrite)
General Modernization (Any Version)
Code MUST use
anyinstead ofinterface{}(Go 1.18+)Use generics instead of
interface{}+ type assertions (Go 1.18+)Use
errors.Joininstead of multi-error libraries (Go 1.20+)Use
net.JoinHostPortinstead offmt.Sprintf(any version)
Go 1.21 Modernizations (August 2023)
Changelog: https://go.dev/doc/go1.21
Use built-in min, max, clear (Go 1.21+)
Remove custom implementations. min/max work with any ordered type and accept variadic arguments:
// Before
func minInt(a, b int) int {
if a < b { return a }
return b
}
x := minInt(a, b)
// After (Go 1.21+)
x := min(a, b)
smallest := min(a, b, c, d)clear zeroes maps and slices:
// Before
for k := range m { delete(m, k) }
// After (Go 1.21+)
clear(m)Use log/slog instead of third-party loggers (Go 1.21+)
log/slog is the standard structured logging package. New code SHOULD migrate to slog over zap, logrus, or zerolog.
// Before: zap
logger, _ := zap.NewProduction()
logger.Info("request handled", zap.String("method", r.Method), zap.Int("status", status))
// Before: logrus
logrus.WithFields(logrus.Fields{"method": r.Method, "status": status}).Info("request handled")
// After (Go 1.21+): slog
slog.Info("request handled", "method", r.Method, "status", status)
// Or with type-safe attributes:
slog.Info("request handled", slog.String("method", r.Method), slog.Int("status", status))Migration guidance: For existing projects heavily invested in third-party loggers, migration is optional; for new projects, prefer slog. The samber/slog-* ecosystem provides handlers for routing slog output to various backends. Go 1.24 added slog.DiscardHandler for silent loggers.
Use slices package instead of sort and manual loops (Go 1.21+)
// Before
sort.Strings(names)
sort.Slice(users, func(i, j int) bool { return users[i].Name < users[j].Name })
// After (Go 1.21+)
slices.Sort(names)
slices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.Name, b.Name) })// Before: manual search
found := false
for _, v := range items { if v == target { found = true; break } }
// After (Go 1.21+)
found := slices.Contains(items, target)// Before: manual clone
clone := append([]string(nil), original...)
// After (Go 1.21+)
clone := slices.Clone(original)Use maps package (Go 1.21+)
// Before
clone := make(map[string]int, len(original))
for k, v := range original { clone[k] = v }
// After (Go 1.21+)
clone := maps.Clone(original)Use cmp.Or for default values (Go 1.22+)
// Before
addr := os.Getenv("ADDR")
if addr == "" { addr = ":8080" }
// After (Go 1.22+)
addr := cmp.Or(os.Getenv("ADDR"), ":8080")Use sync.OnceFunc, sync.OnceValue, sync.OnceValues (Go 1.21+)
// Before
var (
once sync.Once
client *http.Client
)
func getClient() *http.Client {
once.Do(func() { client = &http.Client{Timeout: 10 * time.Second} })
return client
}
// After (Go 1.21+)
var getClient = sync.OnceValue(func() *http.Client {
return &http.Client{Timeout: 10 * time.Second}
})Use enhanced context functions (Go 1.21+)
ctx := context.WithoutCancel(parent) // detach from parent cancellation
ctx, cancel := context.WithTimeoutCause(parent, 5*time.Second, errTimeout)
ctx, cancel := context.WithDeadlineCause(parent, deadline, errDeadline)
stop := context.AfterFunc(ctx, func() { cleanup() })Go 1.22 Modernizations (February 2024)
Changelog: https://go.dev/doc/go1.22
SHOULD use range over integers (Go 1.22+)
// Before
for i := 0; i < n; i++ { process(i) }
// After (Go 1.22+)
for i := range n { process(i) }
// When index isn't needed
for range 10 { fmt.Println("hello") }Remove loop variable shadow copies (Go 1.22+)
Go 1.22 changed loop variable semantics: each iteration creates a new variable. Loop variable captures (v := v) SHOULD be removed in Go 1.22+ codebases.
Requirement: The go directive in go.mod must be go 1.22 or later for this behavior.
// Before (Go < 1.22)
for _, v := range items {
v := v // shadow copy to avoid closure bug
go func() { process(v) }()
}
// After (Go 1.22+): safe by default
for _, v := range items {
go func() { process(v) }()
}math/rand MUST be replaced with math/rand/v2 (Go 1.22+)
// Before
import "math/rand"
rand.Seed(time.Now().UnixNano()) // no longer needed
n := rand.Intn(100)
// After (Go 1.22+)
import "math/rand/v2"
n := rand.IntN(100) // IntN, not IntnKey math/rand/v2 changes:
No global seed needed — automatically seeded
Intn->IntN,Int63n->Int64N(renamed)rand.N[T]()generic function for any integer typeBetter algorithms (ChaCha8, PCG)
Readremoved — usecrypto/randfor random bytes
Use enhanced net/http routing (Go 1.22+)
// Before: gorilla/mux or chi
r := mux.NewRouter()
r.HandleFunc("/users/{id}", getUser).Methods("GET")
// After (Go 1.22+): stdlib
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
}Use strings.CutPrefix and strings.CutSuffix (Go 1.20+)
// Before
if strings.HasPrefix(s, "Bearer ") {
token := strings.TrimPrefix(s, "Bearer ")
}
// After (Go 1.20+)
if token, ok := strings.CutPrefix(s, "Bearer "); ok {
// use token
}Use reflect.TypeFor[T]() (Go 1.22+)
// Before
t := reflect.TypeOf((*MyInterface)(nil)).Elem()
// After (Go 1.22+)
t := reflect.TypeFor[MyInterface]()Use database/sql.Null[T] (Go 1.22+)
// Before
var name sql.NullString
var age sql.NullInt64
// After (Go 1.22+)
var name sql.Null[string]
var age sql.Null[int64]Go 1.23 Modernizations (August 2024)
Changelog: https://go.dev/doc/go1.23
Use iterators (range over functions) (Go 1.23+)
Go 1.23 introduced range-over-func with the iter package:
// Before: collect all results into a slice
func AllUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT ...")
if err != nil { return nil, err }
defer rows.Close()
var users []User
for rows.Next() {
var u User
rows.Scan(&u.ID, &u.Name)
users = append(users, u)
}
return users, rows.Err()
}
// After (Go 1.23+): lazy iteration
func AllUsers(db *sql.DB) iter.Seq2[User, error] {
return func(yield func(User, error) bool) {
rows, err := db.Query("SELECT ...")
if err != nil { yield(User{}, err); return }
defer rows.Close()
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
yield(User{}, err); return
}
if !yield(u, nil) { return }
}
if err := rows.Err(); err != nil { yield(User{}, err) }
}
}Use iterator-based slices and maps functions (Go 1.23+)
// Sorted keys via iterator
for k := range slices.Sorted(maps.Keys(m)) {
fmt.Println(k, m[k])
}
// Collect iterator into slice
users := slices.Collect(maps.Values(userMap))
// Chunk a slice into batches
for chunk := range slices.Chunk(items, 100) {
processBatch(chunk)
}Use unique package for value interning (Go 1.23+)
// Before: manual string interning
var mu sync.Mutex
var interned = make(map[string]string)
// After (Go 1.23+)
handle := unique.Make(s) // Handle[string], comparable, memory-efficient
s = handle.Value()Timer/Ticker behavior change (Go 1.23+)
With go 1.23 or later in go.mod:
time.Timerandtime.Tickerare garbage collected without callingStop()Timer channels are now unbuffered (capacity 0, was 1)
Remove unnecessary Stop() calls in defer patterns where the timer goes out of scope.
Go 1.24 Modernizations (February 2025)
Changelog: https://go.dev/doc/go1.24
Use generic type aliases (Go 1.24+)
// Now valid (Go 1.24+)
type Set[T comparable] = map[T]struct{}
type Result[T any] = struct { Value T; Err error }Use os.Root for directory-scoped file access (Go 1.24+)
Security-critical: os.Root prevents path traversal attacks (CWE-22) at the OS level. Replace all manual filepath.Clean + strings.HasPrefix validation with os.Root when handling user-supplied paths. It rejects symlinks resolving outside the root and supports Open, Create, Stat, OpenFile, Mkdir, Remove, and more.
// Before: manual path validation (risk of path traversal)
path := filepath.Join(baseDir, userInput)
data, err := os.ReadFile(path)
// After (Go 1.24+): safe directory-scoped access
root, err := os.OpenRoot("/opt/data")
if err != nil { return err }
defer root.Close()
f, err := root.Open(userInput) // cannot escape root directoryUse omitzero JSON tag (Go 1.24+)
omitzero is more correct than omitempty for time.Time, bool, and custom types:
// Before: omitempty doesn't work well for time.Time
type Event struct {
At time.Time `json:"at,omitempty"` // zero time.Time is NOT omitted
}
// After (Go 1.24+)
type Event struct {
At time.Time `json:"at,omitzero"` // zero time.Time IS omitted
}Use strings.SplitSeq, strings.FieldsSeq, strings.Lines (Go 1.24+)
Iterator-returning variants avoid allocating []string:
// Before: allocates a []string
parts := strings.Split(csv, ",")
for _, part := range parts { process(part) }
// After (Go 1.24+): lazy, zero-allocation iteration
for part := range strings.SplitSeq(csv, ",") { process(part) }t.Context() SHOULD replace manual context.Background() in tests (Go 1.24+)
// Before
func TestFoo(t *testing.T) {
ctx := context.Background()
}
// After (Go 1.24+): auto-cancelled when test ends
func TestFoo(t *testing.T) {
ctx := t.Context()
}b.Loop() MUST be used in benchmarks (Go 1.24+)
// Before
func BenchmarkFoo(b *testing.B) {
for i := 0; i < b.N; i++ { foo() }
}
// After (Go 1.24+)
func BenchmarkFoo(b *testing.B) {
for b.Loop() { foo() }
}Use runtime.AddCleanup instead of runtime.SetFinalizer (Go 1.24+)
// Before
runtime.SetFinalizer(obj, func(o *Object) { o.Close() })
// After (Go 1.24+): more flexible, no cycle issues
runtime.AddCleanup(obj, func(resource Resource) { resource.Close() }, obj.resource)Use weak package for weak references (Go 1.24+)
import "weak"
ptr := weak.Make(obj)
if v := ptr.Value(); v != nil {
// object still alive
}Use crypto/sha3, crypto/hkdf, crypto/pbkdf2 (Go 1.24+)
Replace golang.org/x/crypto sub-packages with standard library equivalents:
// Before
import "golang.org/x/crypto/sha3"
import "golang.org/x/crypto/hkdf"
import "golang.org/x/crypto/pbkdf2"
// After (Go 1.24+)
import "crypto/sha3"
import "crypto/hkdf"
import "crypto/pbkdf2"Use tool directives in go.mod (Go 1.24+)
Use tool directives instead of tools.go blank imports.
go get -tool golang.org/x/tools/cmd/stringer@latest
go get -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
go tool stringer -type=Kind
go tool golangci-lint run ./...go.mod shape for a module targeting Go 1.26 or newer. This is an example target, not a cap; keep the project's actual go directive and do not change it just to add tools.
module example.com/project
go 1.26
tool (
golang.org/x/tools/cmd/stringer
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
)Use go install tool to install all module-pinned tools when needed and go get -u tool to update them deliberately.
Use fmt.Appendf, fmt.Appendln (Go 1.19+, often overlooked)
// Before
buf = append(buf, fmt.Sprintf("count: %d", n)...)
// After (Go 1.19+)
buf = fmt.Appendf(buf, "count: %d", n)Go 1.25 Modernizations (August 2025)
Changelog: https://go.dev/doc/go1.25
Use sync.WaitGroup.Go (Go 1.25+)
// Before
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
process()
}()
wg.Wait()
// After (Go 1.25+)
var wg sync.WaitGroup
wg.Go(func() {
process()
})
wg.Wait()Use testing/synctest for concurrent code testing (Go 1.25+, experimental in 1.24)
// Before
func TestConcurrent(t *testing.T) {
var count atomic.Int32
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
count.Add(1)
}()
wg.Wait()
// Problem: Race conditions are hard to detect, timing-dependent,
// and flaky tests are common
if count.Load() != 1 {
t.Fatal("expected 1")
}
}
// After (Go 1.25+)
func TestConcurrent(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var count atomic.Int32
go func() { count.Add(1) }()
synctest.Wait() // wait for all goroutines to park
if count.Load() != 1 { t.Fatal("expected 1") }
})
}Note: Use synctest.Test in Go 1.25+ and Go 1.26+. Do not use the old Go 1.24 experimental synctest.Run API in Go 1.25+ code.
Use runtime/trace.FlightRecorder (Go 1.25+)
Lightweight always-on ring-buffer tracing for production:
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{})
if err := fr.Start(); err != nil {
return err
}
// ... later, on error:
fr.WriteTo(file) // captures recent trace dataContainer-aware GOMAXPROCS (Go 1.25+)
Go 1.25 automatically respects cgroup CPU limits on Linux. Remove manual workarounds:
// Before: using uber-go/automaxprocs
import _ "go.uber.org/automaxprocs"
// After (Go 1.25+): built-in, remove the import
// GOMAXPROCS is set automatically from cgroup CPU limitsencoding/json/v2 — introduced experimental (Go 1.25+, GOEXPERIMENT=jsonv2)
Major JSON revision, experimental via GOEXPERIMENT=jsonv2 in Go 1.25–1.26. Go 1.27 made it the default implementation — see the Go 1.27 section below for the stable API and migration hazards.
Go 1.25 additions to prefer when target allows
sync.WaitGroup.Go: simple fire-and-wait goroutines; function must not panic; no errors/cancellation.testing/synctest.Testandsynctest.Wait: stable deterministic concurrent/time tests. Do not use the Go 1.24 experimentalsynctest.Runin Go 1.25+.net/http.CrossOriginProtection: stdlib helper for cross-origin / CSRF-style protection in HTTP servers.reflect.TypeAssert[T](v): prefer overv.Interface().(T)in reflection code.os.Root.FSand additionalos.Rootmethods: use for confined filesystem APIs.New vet checks:
waitgroupmisuse and manual host:port formatting; prefernet.JoinHostPort.
Go 1.26 Modernizations (February 2026)
Changelog: https://go.dev/doc/go1.26
Use errors.AsType[T]() (Go 1.26+)
// Before
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println(pathErr.Path)
}
// After (Go 1.26+)
if pathErr, ok := errors.AsType[*os.PathError](err); ok {
fmt.Println(pathErr.Path)
}Use enhanced new() (Go 1.26+)
new(expr) now accepts a value expression and returns a pointer to it (not zero-initialized):
// Before: helper function needed
func ptr[T any](v T) *T { return &v }
cfg := Config{Timeout: ptr(30)}
// After (Go 1.26+): new(expr) initializes the value — equivalent to ptr(30)
cfg := Config{Timeout: new(30)} // *int pointing to 30, not 0Use crypto/hpke (Go 1.26+)
Hybrid Public Key Encryption (RFC 9180) is now in the standard library.
Use RSA-OAEP or HPKE instead of new PKCS#1 v1.5 encryption (Go 1.26+)
For new encryption use, avoid crypto/rsa.EncryptPKCS1v15. Prefer RSA-OAEP (rsa.EncryptOAEP / rsa.EncryptOAEPWithOptions) or a modern KEM/HPKE design.
Green Tea GC enabled by default (Go 1.26+)
Re-evaluate GC and allocation tuning under Go 1.26 Green Tea GC using profiles and benchmarks, removing legacy tuning only when data supports it. Keep GOMEMLIMIT when it represents a real container or service memory ceiling. Remove third-party automaxprocs workarounds unless the project has a measured reason, because Go 1.25+ makes GOMAXPROCS container-aware by default.
Go 1.26+ test artifacts
Use t.ArtifactDir(), b.ArtifactDir(), and f.ArtifactDir() for files created by tests, benchmarks, and fuzzers that should persist for inspection.
Go 1.26+ slog multi-handler
For simple fan-out to multiple slog handlers, prefer stdlib slog.NewMultiHandler before adding third-party handler-composition dependencies.
Go 1.26+ ReverseProxy
For new reverse proxy code, prefer httputil.ReverseProxy{Rewrite: ...}. Do not generate new Director-based proxy code unless preserving old compatibility.
proxy := &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(targetURL)
pr.SetXForwarded()
},
}Small Go 1.26+ API preferences
Use
bytes.Buffer.Peek(n)when you need to inspect upcoming bytes without consuming them.Use reflect iterators where they simplify code:
reflect.Type.Fields()reflect.Type.Methods()reflect.Type.Ins()reflect.Type.Outs()reflect.Value.Fields()reflect.Value.Methods()
Prefer these over manual
NumField/Field(i)orNumMethod/Method(i)loops when the iterator form is clearer.
Go 1.26+ goroutine leak profile
For Go 1.26 diagnostics, there is an experimental goroutine leak profile. It is useful for production-oriented leak investigation, but is gated by GOEXPERIMENT=goroutineleakprofile; do not rely on it as default stable behavior. Generally available since Go 1.27 without the experiment flag — see the Go 1.27 section.
Go 1.26+ documentation command
Use go doc, not go tool doc. Go 1.26 removed the old cmd/doc / go tool doc path.
Go 1.26+ module target note
When using a Go 1.26 or newer toolchain, go mod init may create a module with an older default go directive. If the project intentionally targets Go 1.26+ APIs, update the directive deliberately:
go mod edit -go=1.26
go mod tidyFor future Go versions, use the project's intended target version. Do not use APIs newer than the module's go directive until the project explicitly agrees to upgrade it.
Modernized go fix (Go 1.26+)
Go 1.26 rewrote go fix to apply a subset of modernize-style analyzers automatically. Check go tool fix help for exact coverage; some modernizations still require linting or manual review.
go fix ./... # applies the enabled safe transformationsGo 1.27 Modernizations (August 2026)
Changelog: https://go.dev/doc/go1.27
Use generic methods to scope generics to a type (Go 1.27+)
Go 1.27 lifted a restriction present since generics landed in Go 1.18: a method may now declare its own type parameters (go.dev/issue/77273, spec: Method declarations), so a helper that logically belongs to one type no longer needs a package-scope generic function. Interface methods still cannot declare type parameters, and a generic method cannot satisfy an interface — keep the package-level function when the operation must be part of an interface contract.
// Before: package-scope generic function, disconnected from the type it serves
func FilterInts[T any](s []T, pred func(T) bool) []T { ... }
// After (Go 1.27+): generic method, scoped to the receiver
func (s Set[T]) Filter[U comparable](pred func(T) U) Set[T] { ... }The standard library's own (*rand.Rand).N[Int intType](n Int) Int (math/rand/v2) is the reference example.
Use strings.CutLast and bytes.CutLast instead of LastIndex slicing (Go 1.27+)
// Before: manual index arithmetic — easy to get the offset wrong
if i := strings.LastIndex(path, "/"); i >= 0 {
dir, file := path[:i], path[i+1:]
}
// After (Go 1.27+)
if dir, file, ok := strings.CutLast(path, "/"); ok {
// dir, file
}bytes.CutLast(b, sep []byte) (before, after []byte, found bool) mirrors strings.CutLast(s, sep string) (before, after string, found bool).
Use net/url URL.Clone() and Values.Clone() (Go 1.27+)
// Before: Values is map[string][]string — a shallow copy shares the slices
clone := url.Values{}
for k, v := range original {
clone[k] = append([]string(nil), v...)
}
// After (Go 1.27+)
clone := original.Clone()
u2 := u.Clone()Use math/big.Int.Divide for rounding-mode division (Go 1.27+)
// Before: sign-correction dance for floor/ceil division
q, r := new(big.Int).QuoRem(x, y, new(big.Int))
if r.Sign() != 0 && (r.Sign() < 0) != (y.Sign() < 0) {
q.Sub(q, big.NewInt(1))
}
// After (Go 1.27+)
q, r := new(big.Int).Divide(x, y, new(big.Int), big.Floor)
// modes: big.Trunc, big.Floor, big.Round, big.CeilUse stdlib uuid instead of a UUID dependency (Go 1.27+)
// Before
import "github.com/google/uuid"
id := uuid.NewString()
// After (Go 1.27+): no external dependency
import "uuid"
id := uuid.New().String()The stdlib generators (uuid.New(), uuid.NewV4(), uuid.NewV7()) return values without errors, and v7 UUIDs are time-ordered — prefer uuid.NewV7() for database primary keys where index locality matters. Check go mod why -m github.com/google/uuid (or gofrs/uuid) before dropping the dependency — some codebases depend on v3/v5 namespace UUIDs, SQL Scanner/driver.Valuer integration, or other RFC-specific variants the stdlib package does not (yet) cover.
Migrate to encoding/json/v2 — default since Go 1.27 (Go 1.27+)
encoding/json/v2 and encoding/json/jsontext are now stable (experimental since Go 1.25 via GOEXPERIMENT=jsonv2) and encoding/json/v2 is the default JSON implementation; encoding/json becomes a thin wrapper over it, and unmarshal is significantly faster. For new code, prefer the v2 API directly:
// Before
data, err := json.Marshal(v)
err = json.Unmarshal(data, &v)
// After (Go 1.27+)
import "encoding/json/v2"
data, err := json.Marshal(v) // same names, v2 semantics
err = json.UnmarshalRead(r, &v) // stream from an io.Reader without a wrapper bufferUse encoding/json/jsontext (Encoder, Decoder, Token, Value) for syntactic, streaming-level JSON work instead of hand-rolled json.RawMessage juggling.
Migrate deliberately, not blindly — the default got stricter:
Duplicate object member names are now rejected; v1 silently kept the last one.
Invalid UTF-8 in JSON strings is now rejected; v1 replaced it silently.
The
formatandunknownstruct tags,DiscardUnknownMembers, andSkipFuncare gone.The
inlinetag is renamedembed.Roll back with
GOEXPERIMENT=nojsonv2only as a temporary compatibility bridge, not a permanent stance — it is documented as the escape hatch, not the intended steady state.
Use testing/synctest.Sleep() inside a synctest bubble (Go 1.27+)
synctest.Test(t, func(t *testing.T) {
go worker()
synctest.Sleep(time.Second) // advances the bubble's fake clock
})Use net/http/httptest.NewTestServer() for in-memory server tests (Go 1.27+)
// Before: httptest.Server binds a real socket, forcing real goroutines/timers
srv := httptest.NewServer(handler)
defer srv.Close()
// After (Go 1.27+): in-memory fake network, composes with testing/synctest
synctest.Test(t, func(t *testing.T) {
srv := httptest.NewTestServer(handler)
defer srv.Close()
})runtime/pprof goroutineleak profile is generally available (Go 1.27+)
The goroutine leak profile (previously experimental behind GOEXPERIMENT=goroutineleakprofile in Go 1.26) is now a standard runtime/pprof profile, also served at /debug/pprof/goroutineleak — no build flag required. It reports goroutines blocked on a concurrency primitive that can never be unblocked; leaks reachable from global variables or still-runnable goroutines are not detected. → See samber/cc-skills-golang@golang-concurrency and samber/cc-skills-golang@golang-troubleshooting skills for using it in a leak investigation.
go fix gains new modernizers (Go 1.27+)
New analyzers: atomictypes, embedlit, slicesbackward, unsafefuncs. The waitgroup analyzer was renamed to waitgroupgo, and fmtappendf was removed. Run go fix ./... after upgrading the toolchain. → See Tooling modernization for the full go fix/go doc/go mod tidy command reference.
go test runs the stdversion vet check (Go 1.27+)
go test now reports uses of standard library symbols that are too new for the file's effective Go version (the go directive in go.mod plus build tags). If CI starts failing after a toolchain upgrade, either bump the module's go directive or gate the newer API behind build tags — don't silence the check.
go mod tidy merges duplicate require blocks (Go 1.27+)
For modules with go 1.27 or later in go.mod, go mod tidy consolidates duplicate require blocks into the standard two-block layout (one direct, one indirect), preserving existing comments. Run it once after bumping the go directive to clean up blocks left by manual edits and merge conflicts.
Small Go 1.27+ API preferences
hash/maphash.Hasherandmaphash.ComparableHasher: contracts between a type and future hash-based data structures (hash tables, Bloom filters).database/sql.ConvertAssignanddriver.RowsColumnScanner: for database driver authors.runtime/secret.Do: goroutines started in secret mode now execute in secret mode themselves.
Go 1.27+ version-bump risk checklist (verify, don't rewrite)
These changes need review before or during a bump to go 1.27 — none of them require a code rewrite, but skipping the check risks a build failure or a silent behavior change:
Removed
GODEBUGsettings —asynctimerchan,tlsunsafeekm,tlsrsakex,tls3des,tls10server,x509keypairleaf,gotypesalias. Agodebugline ingo.modor a//go:debugcomment still pinning one of these to its old value now fails the build; pinning it to its current default value is accepted. Search withgrep -rn 'go:debug\|godebug' go.mod **/*.go.json/v2 default strictness — see above; re-run integration tests against real-world payloads, not just unit tests, before the bump ships.
Size-specialized allocator — up to 30% faster allocations under 80 bytes, roughly 1% faster overall, at the cost of ~60 KB binary size. Enabled by default; disable with
GOEXPERIMENT=nosizespecializedmallocif binary size is constrained, but treat that flag as scheduled for removal in Go 1.28, not a long-term setting.Darwin floor raised to macOS 13 (Ventura) — older macOS targets can no longer run binaries built with this toolchain.
linux/ppc64now builds ELFv2 binaries and requires Linux kernel 3.13+ (RHEL 7's 3.10 kernel with backports) — relevant only to ppc64 deployments.bzrversion control support removed from thegocommand — irrelevant unless a module still vendors from Bazaar.Tracebacks now include
runtime/pprofgoroutine labels forgo 1.27+modules by default; disable withGODEBUG=tracebacklabels=0if labels leak sensitive data into crash logs or panic output.
General Modernization (Any Version)
Code MUST use any instead of interface{} (Go 1.18+)
// Before
func process(data interface{}) interface{} { ... }
// After (Go 1.18+)
func process(data any) any { ... }Use generics instead of interface{} + type assertions (Go 1.18+)
// Before
func Contains(slice []interface{}, item interface{}) bool { ... }
// After (Go 1.18+)
func Contains[T comparable](slice []T, item T) bool { ... }
// Or better (Go 1.21+): slices.ContainsUse errors.Join instead of multi-error libraries (Go 1.20+)
// Before: hashicorp/go-multierror or uber-go/multierr
errs = multierror.Append(errs, err1)
return errs.ErrorOrNil()
// After (Go 1.20+)
return errors.Join(err1, err2)Use net.JoinHostPort instead of fmt.Sprintf (any version)
// Before (broken for IPv6)
addr := fmt.Sprintf("%s:%d", host, port)
// After (handles IPv6 correctly: [::1]:8080)
addr := net.JoinHostPort(host, strconv.Itoa(port))Requirements and compatibility
Written for: Claude Code, OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Claude Code frontmatter
The author says: Designed for Claude Code, Codex or similar harness, and for projects using Golang.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- samber
- Publisher
- Samuel Berthe (Community)
- Distributed by
- Samuel Berthe
- Source
- samber/cc-skills-golang · skills/golang-modernize · commit bb1d43d
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by samber or Samuel Berthe.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download golang-modernize.zip