evals/evals.json
{
"skill_name": "golang-modernize",
"evals": [
{
"id": 1,
"name": "version-constraint-1.21",
"prompt": "Review this Go code for modernization opportunities. The project targets Go 1.21.\n\n```go\n// go.mod\nmodule example.com/myapp\ngo 1.21\n\n// main.go\npackage main\n\nimport (\n \"fmt\"\n \"math/rand\"\n \"sort\"\n \"sync\"\n \"time\"\n)\n\nfunc minInt(a, b int) int {\n if a < b { return a }\n return b\n}\n\nfunc maxInt(a, b int) int {\n if a > b { return a }\n return b\n}\n\nfunc processItems(items []string) {\n sort.Strings(items)\n found := false\n for _, v := range items {\n if v == \"target\" { found = true; break }\n }\n _ = found\n}\n\nfunc processN(n int) {\n for i := 0; i < n; i++ {\n fmt.Println(i)\n }\n}\n\nfunc startWorkers(items []int) {\n for _, v := range items {\n v := v // shadow copy for closure\n go func() { fmt.Println(v) }()\n }\n}\n\nvar (\n once sync.Once\n client *int\n)\nfunc getClient() *int {\n once.Do(func() {\n c := 42\n client = &c\n })\n return client\n}\n\nfunc main() {\n rand.Seed(time.Now().UnixNano())\n n := rand.Intn(100)\n fmt.Println(minInt(n, 50), maxInt(n, 10))\n processItems([]string{\"a\", \"b\", \"c\"})\n processN(10)\n startWorkers([]int{1, 2, 3})\n}\n```\n\nSuggest all modernization improvements available for this Go version.",
"trap": "The project targets Go 1.21. Features like range-over-int (1.22), loop variable fix (1.22), cmp.Or (1.22), and math/rand/v2 (1.22) are NOT available. The model must only suggest 1.21-compatible changes AND must avoid false positives by staying within version constraints.",
"assertions": [
{ "id": "1.1", "text": "Suggests min/max builtins to replace minInt/maxInt (available in Go 1.21)" },
{ "id": "1.2", "text": "Suggests slices.Sort or slices.Contains (available in Go 1.21)" },
{ "id": "1.3", "text": "Suggests sync.OnceValue to replace manual sync.Once pattern (available in Go 1.21)" },
{ "id": "1.4", "text": "Does NOT suggest range-over-int for processN (requires Go 1.22+)" },
{ "id": "1.5", "text": "Does NOT suggest removing loop variable shadow copy v := v in startWorkers (requires Go 1.22+)" },
{ "id": "1.6", "text": "Does NOT suggest math/rand/v2 migration (requires Go 1.22+)" },
{ "id": "1.7", "text": "Does NOT suggest cmp.Or (requires Go 1.22+)" }
]
},
{
"id": 2,
"name": "rand-v2-api-renames",
"prompt": "Migrate this Go code from math/rand to math/rand/v2. The project targets Go 1.22.\n\n```go\npackage game\n\nimport (\n \"crypto/rand\"\n \"math/big\"\n mathrand \"math/rand\"\n \"time\"\n)\n\nvar rng *mathrand.Rand\n\nfunc init() {\n mathrand.Seed(time.Now().UnixNano())\n rng = mathrand.New(mathrand.NewSource(time.Now().UnixNano()))\n}\n\nfunc RollDice() int {\n return mathrand.Intn(6) + 1\n}\n\nfunc GenerateID() int64 {\n return mathrand.Int63n(1000000)\n}\n\nfunc ShuffleCards(cards []string) {\n mathrand.Shuffle(len(cards), func(i, j int) {\n cards[i], cards[j] = cards[j], cards[i]\n })\n}\n\nfunc RandomFloat() float64 {\n return mathrand.Float64()\n}\n\nfunc RandomBytes(n int) []byte {\n buf := make([]byte, n)\n mathrand.Read(buf)\n return buf\n}\n\nfunc CryptoRandom() int {\n n, _ := rand.Int(rand.Reader, big.NewInt(100))\n return int(n.Int64())\n}\n```\n\nProvide the complete migrated code.",
"trap": "math/rand/v2 renames functions: Intn->IntN, Int63n->Int64N. Read is removed entirely. Seed is unnecessary. Without the skill, the model may keep old function names.",
"assertions": [
{ "id": "2.1", "text": "Renames Intn to IntN (capital N)" },
{ "id": "2.2", "text": "Renames Int63n to Int64N (not Int63N or Int64n)" },
{ "id": "2.3", "text": "Removes all rand.Seed calls (automatic seeding in v2)" },
{ "id": "2.4", "text": "Replaces rand.Read with crypto/rand usage for random bytes" },
{ "id": "2.5", "text": "Import changes to math/rand/v2" },
{ "id": "2.6", "text": "Does NOT keep any old-style function names (no rand.Intn or rand.Int63n in output)" }
]
},
{
"id": 3,
"name": "safety-over-cosmetic",
"prompt": "Modernize this Go 1.24 HTTP file server code. Suggest all improvements.\n\n```go\npackage fileserver\n\nimport (\n \"fmt\"\n \"io\"\n \"net/http\"\n \"os\"\n \"path/filepath\"\n)\n\ntype FileServer struct {\n baseDir string\n}\n\nfunc NewFileServer(baseDir string) *FileServer {\n return &FileServer{baseDir: baseDir}\n}\n\nfunc (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n userPath := r.URL.Query().Get(\"path\")\n if userPath == \"\" {\n http.Error(w, \"missing path parameter\", http.StatusBadRequest)\n return\n }\n\n fullPath := filepath.Join(fs.baseDir, filepath.Clean(userPath))\n\n f, err := os.Open(fullPath)\n if err != nil {\n http.Error(w, \"file not found\", http.StatusNotFound)\n return\n }\n defer f.Close()\n\n w.Header().Set(\"Content-Type\", \"application/octet-stream\")\n io.Copy(w, f)\n}\n\nfunc process(data interface{}) interface{} {\n return data\n}\n\nfunc minVal(a, b int) int {\n if a < b { return a }\n return b\n}\n\nfunc formatAddr(host string, port int) string {\n return fmt.Sprintf(\"%s:%d\", host, port)\n}\n```\n\nThe project targets Go 1.24. List improvements in priority order.",
"trap": "The code has a path traversal vulnerability (filepath.Join + filepath.Clean is insufficient). Without the skill, the model may prioritize cosmetic changes (interface{}->any, min builtin) over the security-critical os.Root fix.",
"assertions": [
{ "id": "3.1", "text": "Suggests os.Root/os.OpenRoot for user-supplied file paths" },
{ "id": "3.2", "text": "Mentions path traversal risk, directory escape, or CWE-22" },
{ "id": "3.3", "text": "Prioritizes the safety fix (os.Root) over cosmetic changes" },
{ "id": "3.4", "text": "Also suggests interface{} -> any" },
{ "id": "3.5", "text": "Also suggests min builtin or net.JoinHostPort" },
{ "id": "3.6", "text": "Does NOT only address cosmetic issues without mentioning the security issue" }
]
},
{
"id": 4,
"name": "omitzero-vs-omitempty",
"prompt": "Review these JSON struct tags for correctness in our Go 1.24 API. Users report that zero-value time fields and false booleans are unexpectedly included or omitted in JSON responses.\n\n```go\npackage api\n\nimport \"time\"\n\ntype Event struct {\n ID string `json:\"id\"`\n Name string `json:\"name\"`\n StartAt time.Time `json:\"start_at,omitempty\"`\n EndAt time.Time `json:\"end_at,omitempty\"`\n Cancelled bool `json:\"cancelled,omitempty\"`\n Archived bool `json:\"archived,omitempty\"`\n Notes string `json:\"notes,omitempty\"`\n Priority int `json:\"priority,omitempty\"`\n}\n\ntype UserSettings struct {\n UserID string `json:\"user_id\"`\n DarkMode bool `json:\"dark_mode,omitempty\"`\n EmailNotifs bool `json:\"email_notifs,omitempty\"`\n LastLogin time.Time `json:\"last_login,omitempty\"`\n AccountCreated time.Time `json:\"account_created,omitempty\"`\n Bio string `json:\"bio,omitempty\"`\n}\n```\n\nExplain the issue and provide the corrected code.",
"trap": "omitempty doesn't work correctly for time.Time (zero time is a non-empty struct) and treats false as empty for bool. Go 1.24 introduced omitzero which handles both correctly. Without the skill, the model may not know about omitzero.",
"assertions": [
{ "id": "4.1", "text": "Identifies that omitempty doesn't omit zero time.Time (it's a non-empty struct)" },
{ "id": "4.2", "text": "Suggests omitzero for time.Time fields (StartAt, EndAt, LastLogin, AccountCreated)" },
{ "id": "4.3", "text": "Identifies that omitempty treats false as empty for bool fields" },
{ "id": "4.4", "text": "Addresses bool issue correctly (removes tag or uses omitzero)" },
{ "id": "4.5", "text": "Correctly notes omitzero requires Go 1.24+" },
{ "id": "4.6", "text": "Does NOT suggest omitzero for string or int fields where omitempty works correctly" }
]
},
{
"id": 5,
"name": "benchmark-b-loop",
"prompt": "Modernize these benchmarks for our Go 1.24 project.\n\n```go\npackage encoding\n\nimport (\n \"encoding/json\"\n \"testing\"\n)\n\ntype Payload struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Value float64 `json:\"value\"`\n}\n\nfunc BenchmarkMarshal(b *testing.B) {\n p := Payload{ID: 1, Name: \"test\", Value: 3.14}\n for i := 0; i < b.N; i++ {\n json.Marshal(p)\n }\n}\n\nfunc BenchmarkUnmarshal(b *testing.B) {\n data := []byte(`{\"id\":1,\"name\":\"test\",\"value\":3.14}`)\n var p Payload\n for n := 0; n < b.N; n++ {\n json.Unmarshal(data, &p)\n }\n}\n\nfunc BenchmarkRoundTrip(b *testing.B) {\n p := Payload{ID: 1, Name: \"test\", Value: 3.14}\n for i := 0; i < b.N; i++ {\n data, _ := json.Marshal(p)\n var p2 Payload\n json.Unmarshal(data, &p2)\n }\n}\n```\n\nProvide the modernized benchmark code.",
"trap": "Go 1.24 introduced b.Loop() which replaces the manual for i := 0; i < b.N; i++ pattern. Without the skill, the model likely doesn't know about b.Loop().",
"assertions": [
{ "id": "5.1", "text": "Replaces for i := 0; i < b.N; i++ with for b.Loop() in BenchmarkMarshal" },
{ "id": "5.2", "text": "Replaces for n := 0; n < b.N; n++ with for b.Loop() in BenchmarkUnmarshal" },
{ "id": "5.3", "text": "Replaces the b.N loop in BenchmarkRoundTrip too" },
{ "id": "5.4", "text": "Does NOT keep any b.N iteration pattern in the output" },
{ "id": "5.5", "text": "Preserves benchmark function names and logic" }
]
},
{
"id": 6,
"name": "automaxprocs-removal",
"prompt": "We just upgraded to Go 1.25. Review our main.go for modernization opportunities.\n\n```go\n// go.mod\nmodule example.com/worker\ngo 1.25\n\nrequire (\n go.uber.org/automaxprocs v1.5.3\n go.uber.org/zap v1.27.0\n)\n\n// main.go\npackage main\n\nimport (\n \"fmt\"\n \"sync\"\n\n _ \"go.uber.org/automaxprocs\"\n)\n\nfunc main() {\n var wg sync.WaitGroup\n items := []string{\"a\", \"b\", \"c\", \"d\", \"e\"}\n\n for _, item := range items {\n wg.Add(1)\n go func() {\n defer wg.Done()\n process(item)\n }()\n }\n wg.Wait()\n}\n\nfunc process(item string) {\n fmt.Println(\"processing\", item)\n}\n```\n\nSuggest all modernization improvements.",
"trap": "Go 1.25 has built-in container-aware GOMAXPROCS, making uber-go/automaxprocs unnecessary. The model must know this Go 1.25 addition — without the skill it will likely treat automaxprocs as still needed and only suggest sync.WaitGroup.Go.",
"assertions": [
{ "id": "6.1", "text": "Suggests removing go.uber.org/automaxprocs import and dependency" },
{ "id": "6.2", "text": "Explains that Go 1.25 has built-in container-aware GOMAXPROCS (or cgroup CPU limits awareness)" },
{ "id": "6.3", "text": "Suggests sync.WaitGroup.Go to replace Add/go func/Done pattern" },
{ "id": "6.4", "text": "Does NOT suggest keeping the automaxprocs dependency as still necessary" },
{ "id": "6.5", "text": "Suggests running go mod tidy to remove the dependency from go.sum" }
]
},
{
"id": 7,
"name": "cmp-or-chained-defaults",
"prompt": "Clean up this configuration loading code. We're on Go 1.22. The nested if/else chains for default values are hard to read.\n\n```go\npackage config\n\nimport \"os\"\n\ntype Config struct {\n Host string\n Port string\n LogLevel string\n Region string\n Mode string\n}\n\nfunc LoadConfig() Config {\n host := os.Getenv(\"HOST\")\n if host == \"\" {\n host = os.Getenv(\"HOSTNAME\")\n }\n if host == \"\" {\n host = os.Getenv(\"SERVICE_HOST\")\n }\n if host == \"\" {\n host = \"localhost\"\n }\n\n port := os.Getenv(\"PORT\")\n if port == \"\" {\n port = os.Getenv(\"HTTP_PORT\")\n }\n if port == \"\" {\n port = \"8080\"\n }\n\n logLevel := os.Getenv(\"LOG_LEVEL\")\n if logLevel == \"\" {\n logLevel = os.Getenv(\"LOGLEVEL\")\n }\n if logLevel == \"\" {\n logLevel = \"info\"\n }\n\n region := os.Getenv(\"AWS_REGION\")\n if region == \"\" {\n region = os.Getenv(\"REGION\")\n }\n if region == \"\" {\n region = \"us-east-1\"\n }\n\n mode := os.Getenv(\"APP_MODE\")\n if mode == \"\" {\n mode = \"production\"\n }\n\n return Config{\n Host: host,\n Port: port,\n LogLevel: logLevel,\n Region: region,\n Mode: mode,\n }\n}\n```\n\nProvide the cleaned-up code.",
"trap": "Go 1.22 introduced cmp.Or(a, b, c...) which returns the first non-zero value. It collapses multi-step default chains to single lines. Without the skill, the model will likely keep the if/else chains or use a custom helper function.",
"assertions": [
{ "id": "7.1", "text": "Uses cmp.Or for at least one default value chain" },
{ "id": "7.2", "text": "Collapses the 3-step host default to a single cmp.Or call" },
{ "id": "7.3", "text": "Import includes the cmp package" },
{ "id": "7.4", "text": "All multi-step defaults converted to cmp.Or (host, port, logLevel, region)" },
{ "id": "7.5", "text": "Result is functionally equivalent (same fallback order)" },
{ "id": "7.6", "text": "Does NOT introduce a custom helper function for defaults" }
]
},
{
"id": 8,
"name": "addcleanup-vs-setfinalizer",
"prompt": "Review this resource management code for Go 1.24 best practices.\n\n```go\npackage pool\n\nimport (\n \"database/sql\"\n \"fmt\"\n \"runtime\"\n)\n\ntype ManagedConn struct {\n db *sql.DB\n name string\n dsn string\n}\n\nfunc NewManagedConn(dsn, name string) (*ManagedConn, error) {\n db, err := sql.Open(\"postgres\", dsn)\n if err != nil {\n return nil, fmt.Errorf(\"opening db: %w\", err)\n }\n mc := &ManagedConn{db: db, name: name, dsn: dsn}\n runtime.SetFinalizer(mc, func(c *ManagedConn) {\n c.db.Close()\n })\n return mc, nil\n}\n\ntype TempFile struct {\n path string\n fd int\n}\n\nfunc NewTempFile(path string, fd int) *TempFile {\n tf := &TempFile{path: path, fd: fd}\n runtime.SetFinalizer(tf, func(f *TempFile) {\n syscallClose(f.fd)\n osRemove(f.path)\n })\n return tf\n}\n\nfunc syscallClose(fd int) {}\nfunc osRemove(path string) {}\n```\n\nModernize the cleanup pattern. Explain why the current approach has problems.",
"trap": "New Go code should consider runtime.AddCleanup instead of runtime.SetFinalizer because it is less error-prone. The key API difference: AddCleanup takes the resource as a separate argument (not the whole object). Without the skill the model is unlikely to know AddCleanup exists or that SetFinalizer's cycle restriction is a major problem.",
"assertions": [
{ "id": "8.1", "text": "Replaces runtime.SetFinalizer with runtime.AddCleanup" },
{ "id": "8.2", "text": "AddCleanup cleanup function receives the resource (db/*sql.DB or fd/int) as a separate argument, NOT the whole wrapper struct" },
{ "id": "8.3", "text": "Explains that SetFinalizer prevents GC when the object holds a reference to itself or another object with a finalizer (cycle restriction)" },
{ "id": "8.4", "text": "Does NOT pass ManagedConn or TempFile directly as the resource to AddCleanup" },
{ "id": "8.5", "text": "Correctly attributes runtime.AddCleanup to Go 1.24 (or notes it is the modern replacement)" }
]
},
{
"id": 9,
"name": "http-mux-migration",
"prompt": "We want to remove our gorilla/mux dependency. Migrate this REST API router to stdlib. We're on Go 1.22.\n\n```go\npackage api\n\nimport (\n \"encoding/json\"\n \"net/http\"\n\n \"github.com/gorilla/mux\"\n)\n\nfunc SetupRouter() *mux.Router {\n r := mux.NewRouter()\n r.HandleFunc(\"/api/users\", listUsers).Methods(\"GET\")\n r.HandleFunc(\"/api/users\", createUser).Methods(\"POST\")\n r.HandleFunc(\"/api/users/{id}\", getUser).Methods(\"GET\")\n r.HandleFunc(\"/api/users/{id}\", updateUser).Methods(\"PUT\")\n r.HandleFunc(\"/api/users/{id}\", deleteUser).Methods(\"DELETE\")\n r.HandleFunc(\"/api/health\", healthCheck).Methods(\"GET\")\n return r\n}\n\nfunc getUser(w http.ResponseWriter, r *http.Request) {\n vars := mux.Vars(r)\n id := vars[\"id\"]\n json.NewEncoder(w).Encode(map[string]string{\"id\": id})\n}\n\nfunc listUsers(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode([]string{\"user1\", \"user2\"}) }\nfunc createUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) }\nfunc updateUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }\nfunc deleteUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) }\nfunc healthCheck(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }\n```\n\nProvide the complete migrated code.",
"trap": "Go 1.22 added method+path pattern routing to net/http. The exact syntax is 'METHOD /path/{param}' as the pattern string. mux.Vars(r) becomes r.PathValue('id'). Without the skill, the model might not use the correct syntax or might suggest a different third-party router.",
"assertions": [
{ "id": "9.1", "text": "Uses http.NewServeMux() instead of mux.NewRouter()" },
{ "id": "9.2", "text": "Uses method prefix in patterns like 'GET /api/users/{id}'" },
{ "id": "9.3", "text": "Uses r.PathValue(\"id\") instead of mux.Vars(r)" },
{ "id": "9.4", "text": "All 6 routes migrated with correct method prefixes (GET, POST, PUT, DELETE)" },
{ "id": "9.5", "text": "No gorilla/mux import remains" },
{ "id": "9.6", "text": "Return type changes from *mux.Router to *http.ServeMux" }
]
},
{
"id": 10,
"name": "synctest-flaky-fix",
"prompt": "This concurrent test is flaky in CI — it passes locally but fails ~20% of the time in GitHub Actions. Fix it properly. We're on Go 1.25.\n\n```go\npackage pubsub\n\nimport (\n \"sync\"\n \"testing\"\n \"time\"\n)\n\ntype Broker struct {\n mu sync.RWMutex\n subs map[string][]chan string\n}\n\nfunc NewBroker() *Broker {\n return &Broker{subs: make(map[string][]chan string)}\n}\n\nfunc (b *Broker) Subscribe(topic string) <-chan string {\n b.mu.Lock()\n defer b.mu.Unlock()\n ch := make(chan string, 10)\n b.subs[topic] = append(b.subs[topic], ch)\n return ch\n}\n\nfunc (b *Broker) Publish(topic, msg string) {\n b.mu.RLock()\n defer b.mu.RUnlock()\n for _, ch := range b.subs[topic] {\n ch <- msg\n }\n}\n\nfunc TestBrokerPubSub(t *testing.T) {\n b := NewBroker()\n ch1 := b.Subscribe(\"events\")\n ch2 := b.Subscribe(\"events\")\n\n go b.Publish(\"events\", \"hello\")\n\n time.Sleep(50 * time.Millisecond) // flaky!\n\n got1 := <-ch1\n got2 := <-ch2\n\n if got1 != \"hello\" {\n t.Errorf(\"ch1: got %q, want %q\", got1, \"hello\")\n }\n if got2 != \"hello\" {\n t.Errorf(\"ch2: got %q, want %q\", got2, \"hello\")\n }\n}\n\nfunc TestBrokerMultiTopic(t *testing.T) {\n b := NewBroker()\n events := b.Subscribe(\"events\")\n logs := b.Subscribe(\"logs\")\n\n go func() {\n b.Publish(\"events\", \"event1\")\n b.Publish(\"logs\", \"log1\")\n }()\n\n time.Sleep(100 * time.Millisecond) // flaky!\n\n if got := <-events; got != \"event1\" {\n t.Errorf(\"events: got %q, want %q\", got, \"event1\")\n }\n if got := <-logs; got != \"log1\" {\n t.Errorf(\"logs: got %q, want %q\", got, \"log1\")\n }\n}\n```\n\nFix the flakiness without increasing sleep durations.",
"trap": "Go 1.25 introduced testing/synctest.Test which provides deterministic concurrent testing with synctest.Wait(). The natural fix is to increase sleep, use channels for sync, or add retries. The skill teaches synctest.Test as the modern solution. Note: synctest.Run was the old Go 1.24 experimental API — use synctest.Test in Go 1.25+.",
"assertions": [
{ "id": "10.1", "text": "Uses synctest.Test (NOT the old Go 1.24 experimental synctest.Run API)" },
{ "id": "10.2", "text": "Uses synctest.Wait() for goroutine synchronization" },
{ "id": "10.3", "text": "Removes all time.Sleep calls" },
{ "id": "10.4", "text": "No flaky timing dependencies remain" },
{ "id": "10.5", "text": "Correctly imports testing/synctest" },
{ "id": "10.6", "text": "Both tests converted (TestBrokerPubSub and TestBrokerMultiTopic)" }
]
},
{
"id": 11,
"name": "waitgroup-go-loopvar",
"prompt": "Modernize this concurrent processing code. We're on Go 1.25.\n\n```go\npackage worker\n\nimport (\n \"context\"\n \"fmt\"\n \"sync\"\n \"testing\"\n)\n\nfunc ProcessAll(items []string) error {\n var wg sync.WaitGroup\n errCh := make(chan error, len(items))\n\n for _, item := range items {\n item := item // shadow copy for closure safety\n wg.Add(1)\n go func() {\n defer wg.Done()\n if err := process(item); err != nil {\n errCh <- err\n }\n }()\n }\n\n wg.Wait()\n close(errCh)\n\n for err := range errCh {\n return err\n }\n return nil\n}\n\nfunc RunBatch(tasks []func()) {\n var wg sync.WaitGroup\n for _, task := range tasks {\n task := task\n wg.Add(1)\n go func() {\n defer wg.Done()\n task()\n }()\n }\n wg.Wait()\n}\n\nfunc TestProcess(t *testing.T) {\n ctx := context.Background()\n result, err := processWithContext(ctx, \"test\")\n if err != nil {\n t.Fatal(err)\n }\n fmt.Println(result)\n}\n\nfunc process(item string) error { return nil }\nfunc processWithContext(ctx context.Context, s string) (string, error) { return s, nil }\n```\n\nProvide the fully modernized code.",
"trap": "Go 1.25 introduced sync.WaitGroup.Go. Go 1.22+ fixed loop variable semantics (v := v copies are unnecessary). Go 1.24+ has t.Context(). Without the skill, the model may not know WaitGroup.Go exists and may keep the shadow copies.",
"assertions": [
{ "id": "11.1", "text": "Replaces Add/go func/Done pattern with wg.Go(func() { ... })" },
{ "id": "11.2", "text": "Removes wg.Add(1) calls" },
{ "id": "11.3", "text": "Removes defer wg.Done() calls" },
{ "id": "11.4", "text": "Removes item := item loop variable shadow copies" },
{ "id": "11.5", "text": "Explains Go 1.22+ loop variable semantics make copies unnecessary" },
{ "id": "11.6", "text": "Replaces context.Background() with t.Context() in test" },
{ "id": "11.7", "text": "Preserves WaitGroup.Wait() call" }
]
},
{
"id": 12,
"name": "timer-gc-greenteagc",
"prompt": "We upgraded to Go 1.26. Review this code for things we can clean up.\n\n```go\n// go.mod\nmodule example.com/scheduler\ngo 1.26\n\n// scheduler.go\npackage scheduler\n\nimport (\n \"os\"\n \"runtime/debug\"\n \"time\"\n)\n\nfunc init() {\n // Tune GC for lower latency\n debug.SetGCPercent(50)\n debug.SetMemoryLimit(2 << 30) // 2GB\n os.Setenv(\"GOGC\", \"50\")\n}\n\nfunc RunAfter(d time.Duration, fn func()) {\n timer := time.NewTimer(d)\n defer timer.Stop() // prevent timer leak\n <-timer.C\n fn()\n}\n\nfunc PeriodicTask(interval time.Duration, fn func()) chan struct{} {\n done := make(chan struct{})\n go func() {\n ticker := time.NewTicker(interval)\n defer ticker.Stop() // prevent ticker leak\n for {\n select {\n case <-ticker.C:\n fn()\n case <-done:\n return\n }\n }\n }()\n return done\n}\n\nfunc Debounce(d time.Duration, fn func()) func() {\n var timer *time.Timer\n return func() {\n if timer != nil {\n timer.Stop()\n }\n timer = time.AfterFunc(d, fn)\n }\n}\n\nfunc Timeout(d time.Duration) <-chan time.Time {\n timer := time.NewTimer(d)\n defer timer.Stop()\n return timer.C\n}\n```\n\nSuggest all modernization opportunities.",
"trap": "Go 1.23+ timers/tickers are GC'd without Stop(). The Stop() in PeriodicTask is still needed for correctness. Go 1.26's Green Tea GC (10-40% overhead reduction) may make manual tuning unnecessary. go fix ./... is available in Go 1.26.",
"assertions": [
{ "id": "12.1", "text": "Identifies that some defer timer.Stop() calls are unnecessary with Go 1.23+" },
{ "id": "12.2", "text": "Explains the timer/ticker GC behavior change (collected without Stop)" },
{ "id": "12.3", "text": "Suggests reviewing GC tuning (SetGCPercent/SetMemoryLimit) due to Green Tea GC" },
{ "id": "12.4", "text": "Mentions Green Tea GC's 10-40% overhead reduction" },
{ "id": "12.5", "text": "Does NOT suggest removing Stop() from PeriodicTask ticker (needed for correctness)" },
{ "id": "12.6", "text": "Suggests go fix ./... for automated modernization (Go 1.26)" },
{ "id": "12.7", "text": "Correctly distinguishes between Stop for GC (removable) and Stop for correctness (keep)" }
]
},
{
"id": 13,
"name": "go126-errors-astype-enhanced-new",
"prompt": "Modernize this Go 1.26 error handling and pointer helper code.\n\n```go\npackage service\n\nimport (\n \"errors\"\n \"fmt\"\n \"net\"\n \"os\"\n \"time\"\n)\n\nfunc ptr[T any](v T) *T { return &v }\n\ntype ServiceConfig struct {\n Timeout *time.Duration\n Retries *int\n Verbose *bool\n}\n\nfunc DefaultConfig() ServiceConfig {\n return ServiceConfig{\n Timeout: ptr(30 * time.Second),\n Retries: ptr(3),\n Verbose: ptr(false),\n }\n}\n\nfunc HandleError(err error) string {\n var pathErr *os.PathError\n if errors.As(err, &pathErr) {\n return fmt.Sprintf(\"path error: %s\", pathErr.Path)\n }\n\n var netErr *net.OpError\n if errors.As(err, &netErr) {\n return fmt.Sprintf(\"net error: %s %s\", netErr.Op, netErr.Net)\n }\n\n var dnsErr *net.DNSError\n if errors.As(err, &dnsErr) {\n return fmt.Sprintf(\"dns error: %s\", dnsErr.Name)\n }\n\n return err.Error()\n}\n```\n\nApply all Go 1.26 modernizations. Provide the updated code.",
"trap": "Go 1.26 introduced errors.AsType[T]() which replaces the verbose var+errors.As pattern with a single-line if. Go 1.26 also enhanced new() to accept an initial value, replacing the ptr[T] helper. Without the skill, the model likely won't know either feature exists.",
"assertions": [
{ "id": "13.1", "text": "Uses errors.AsType[*os.PathError](err) or similar generic form instead of var+errors.As" },
{ "id": "13.2", "text": "Replaces the ptr[T] helper function with new() that accepts an initial value (e.g., new(30 * time.Second))" },
{ "id": "13.3", "text": "Applies the same errors.AsType rewrite consistently to the net.OpError and net.DNSError branches, not just the first one" },
{ "id": "13.4", "text": "Attributes errors.AsType and the enhanced new() correctly to Go 1.26, not to a newer or older release" }
]
},
{
"id": 14,
"name": "json-v2-duplicate-key-strictness",
"prompt": "We upgraded this service to Go 1.27 last week. Since then, ingestion of webhook payloads from one partner started failing intermittently with decode errors. Review the code and explain what changed.\n\n```go\npackage webhook\n\nimport (\n \"encoding/json\"\n \"net/http\"\n)\n\ntype Event struct {\n ID string `json:\"id\"`\n Type string `json:\"type\"`\n Amount int `json:\"amount\"`\n}\n\nfunc HandlePartnerWebhook(w http.ResponseWriter, r *http.Request) {\n var ev Event\n if err := json.NewDecoder(r.Body).Decode(&ev); err != nil {\n http.Error(w, \"invalid payload\", http.StatusBadRequest)\n return\n }\n process(ev)\n}\n\nfunc process(ev Event) {}\n```\n\nThe partner's webhook payloads are known to occasionally repeat a JSON key (e.g. `{\"id\":\"1\",\"id\":\"1-dup\",\"type\":\"charge\",\"amount\":500}`), which used to decode fine, keeping the last value. Diagnose the regression and fix it.",
"trap": "encoding/json now runs on top of encoding/json/v2 by default since Go 1.27, and v2 rejects duplicate object member names instead of silently keeping the last one like v1 did. Without the skill, the model may not connect the Go 1.27 bump to this specific new decode-time failure and may guess at unrelated causes (malformed JSON, network issues).",
"assertions": [
{ "id": "14.1", "text": "Identifies that Go 1.27 made encoding/json/v2 the default JSON implementation underneath encoding/json" },
{ "id": "14.2", "text": "Explains that duplicate JSON object keys are now rejected by default, whereas v1 silently kept the last value" },
{ "id": "14.3", "text": "Connects the partner payload's duplicate id key directly to the reported decode failures" },
{ "id": "14.4", "text": "Proposes a concrete fix: either normalize/dedupe the payload before decoding, or explicitly opts back into v1 behavior only as a temporary bridge" },
{ "id": "14.5", "text": "Names GOEXPERIMENT=nojsonv2 as an escape hatch, not as the recommended long-term fix" },
{ "id": "14.6", "text": "Does NOT claim encoding/json's decode behavior is unaffected by upgrading to Go 1.27" }
]
},
{
"id": 15,
"name": "generic-method-scoped-transform",
"prompt": "This project targets Go 1.27. Add a way to transform every element of a generic Set into a Set of a different element type, keeping the transform function type-safe.\n\n```go\npackage collections\n\ntype Set[T comparable] map[T]struct{}\n\nfunc NewSet[T comparable](items ...T) Set[T] {\n s := make(Set[T], len(items))\n for _, item := range items {\n s[item] = struct{}{}\n }\n return s\n}\n\nfunc (s Set[T]) Contains(item T) bool {\n _, ok := s[item]\n return ok\n}\n```\n\nAdd the transform capability and show how it's called from `main`.",
"trap": "Go 1.27 lets methods declare their own type parameters, so a transform from Set[T] to Set[U] can be a method on Set[T] with U scoped to the method. Without the skill, the natural default is a package-level generic function `func MapSet[T, U comparable](s Set[T], f func(T) U) Set[U]` because pre-1.27 Go had no way to add a type parameter to a method.",
"assertions": [
{ "id": "15.1", "text": "Declares the transform as a method on Set[T] (e.g. func (s Set[T]) Map[U comparable](f func(T) U) Set[U]) rather than a standalone package-level generic function" },
{ "id": "15.2", "text": "The new type parameter (U) is declared on the method itself, not added to the Set[T] type definition" },
{ "id": "15.3", "text": "Notes that generic methods declaring their own type parameters are a Go 1.27 addition" },
{ "id": "15.4", "text": "Calls out that this method could not satisfy an interface method and that interface methods themselves cannot declare type parameters" },
{ "id": "15.5", "text": "The example call site compiles conceptually (correct method call syntax with explicit or inferred U)" }
]
},
{
"id": 16,
"name": "cutlast-final-separator-split",
"prompt": "This Go 1.27 log processor needs a helper that splits a structured log line into everything before and everything after the LAST colon, since fields before the last colon may themselves contain colons (e.g. `\"svc:sub:handler:the message: with a colon\"` should split into `\"svc:sub:handler:the message\"` and `\" with a colon\"`).\n\n```go\npackage logs\n\nfunc SplitAtLastColon(line string) (head, tail string, ok bool) {\n // TODO: implement\n return \"\", \"\", false\n}\n```\n\nImplement `SplitAtLastColon`.",
"trap": "strings.CutLast(s, sep) is the direct fit here, added in Go 1.27. Without the skill, the natural implementation is strings.LastIndex plus manual index slicing (i, i+len(sep)), which is easy to get off-by-one on, or strings.Split, which is wrong here because it splits on every colon rather than just the last one.",
"assertions": [
{ "id": "16.1", "text": "Uses strings.CutLast(line, \":\") to implement the function" },
{ "id": "16.2", "text": "Returns the CutLast found boolean directly as ok, rather than deriving it from an index comparison" },
{ "id": "16.3", "text": "Does NOT use strings.LastIndex with manual slice arithmetic" },
{ "id": "16.4", "text": "Does NOT use strings.Split or strings.SplitN, which would split on every colon instead of only the last one" },
{ "id": "16.5", "text": "Notes that CutLast requires Go 1.27 or later" }
]
},
{
"id": 17,
"name": "godebug-removed-key-build-break",
"prompt": "Upgrade this project to the latest stable Go toolchain and update `go.mod` accordingly.\n\n```\nmodule example.com/svc\n\ngo 1.24\n\ngodebug (\n asynctimerchan=0\n)\n\nrequire (\n github.com/lib/pq v1.10.9\n)\n```\n\nThe `asynctimerchan=0` line was added a while ago to keep `time.Timer`/`time.Ticker` channels buffered like they were before Go 1.23, because some code relied on a stale timer value still sitting in the channel. Provide the updated `go.mod`.",
"trap": "Go 1.27 removed the asynctimerchan GODEBUG setting entirely; a go.mod godebug entry still pinning it to its old (pre-1.23) value now fails the build, not just prints a warning. The natural default without the skill is to bump only the go directive and leave the godebug block untouched, assuming an old GODEBUG pin is harmless leftover config.",
"assertions": [
{ "id": "17.1", "text": "Removes or updates the asynctimerchan=0 godebug entry rather than leaving it as-is" },
{ "id": "17.2", "text": "Explains that Go 1.27 removed this GODEBUG setting and that a go.mod entry pinned to the old value now fails the build (not just a warning)" },
{ "id": "17.3", "text": "Bumps the go directive to a current Go 1.27-era version" },
{ "id": "17.4", "text": "Recommends actually fixing the underlying code that relied on buffered timer channels rather than just deleting the compatibility shim silently" },
{ "id": "17.5", "text": "Mentions checking for other Go 1.27-removed GODEBUG keys (e.g. tls3des, gotypesalias, tlsrsakex) as part of the same upgrade, not just asynctimerchan" }
]
},
{
"id": 18,
"name": "uuid-stdlib-vs-dependency",
"prompt": "Add a function to this Go 1.27 order service that generates a unique identifier for each new order. There is currently no UUID library in `go.mod`.\n\n```go\npackage orders\n\ntype Order struct {\n ID string\n Amount int\n}\n\nfunc NewOrder(amount int) Order {\n return Order{\n // TODO: generate a unique ID\n Amount: amount,\n }\n}\n```\n\nImplement ID generation.",
"trap": "Go 1.27 added a uuid package to the standard library. Without the skill, the natural default is to run `go get github.com/google/uuid` and call uuid.New(), which is the long-standing community convention and still works, but now adds an unnecessary dependency to a Go 1.27 project.",
"assertions": [
{ "id": "18.1", "text": "Uses the standard library uuid package (import \"uuid\") rather than github.com/google/uuid or gofrs/uuid" },
{ "id": "18.2", "text": "Does NOT add a new external module dependency to go.mod for UUID generation" },
{ "id": "18.3", "text": "Notes that the stdlib uuid package is new in Go 1.27" },
{ "id": "18.4", "text": "Generated ID is stored via UUID.String() (or equivalent) to fit the existing string-typed Order.ID field" },
{ "id": "18.5", "text": "Does NOT claim the stdlib uuid package existed before Go 1.27" }
]
},
{
"id": 19,
"name": "json-v2-streaming-migration",
"prompt": "Add a `LoadFromReader(r io.Reader, v any) error` helper to this Go 1.27 config package, so callers can decode JSON directly from a stream (an HTTP response body, an open file) without buffering the whole payload into a byte slice first.\n\n```go\npackage config\n\nimport (\n \"encoding/json\"\n \"io\"\n)\n\nfunc LoadFromBytes(data []byte, v any) error {\n return json.Unmarshal(data, v)\n}\n\n// TODO: add LoadFromReader\n```\n\nImplement `LoadFromReader`.",
"trap": "encoding/json/v2 is the default JSON implementation since Go 1.27 and exposes UnmarshalRead(r, v) for exactly this streaming case. Without the skill, the natural default is json.NewDecoder(r).Decode(v) from the v1-era API, which still works but isn't the migration path the skill should point toward for new Go 1.27 code.",
"assertions": [
{ "id": "19.1", "text": "Uses encoding/json/v2's UnmarshalRead(r, v) (or explicitly imports encoding/json/v2 and calls it) rather than only the v1-era json.NewDecoder(r).Decode(v) pattern" },
{ "id": "19.2", "text": "Notes that encoding/json/v2 is the new default JSON implementation as of Go 1.27" },
{ "id": "19.3", "text": "Does NOT introduce a manual io.ReadAll followed by json.Unmarshal, which would defeat the point of streaming from the reader" },
{ "id": "19.4", "text": "Keeps LoadFromBytes working as-is rather than rewriting unrelated code" },
{ "id": "19.5", "text": "The new function's signature matches the requested LoadFromReader(r io.Reader, v any) error" }
]
},
{
"id": 20,
"name": "go127-stdlib-uuid",
"prompt": "Modernize this Go 1.27 code. We'd like to reduce third-party dependencies where the standard library is sufficient.\n\n```go\n// go.mod\nmodule example.com/orders\ngo 1.27\n\nrequire github.com/google/uuid v1.6.0\n\n// ids.go\npackage orders\n\nimport \"github.com/google/uuid\"\n\nfunc NewOrderID() string {\n return uuid.NewString()\n}\n\nfunc NewSortableOrderID() (string, error) {\n // we want roughly time-ordered IDs for our database primary keys\n id, err := uuid.NewV7()\n if err != nil {\n return \"\", err\n }\n return id.String(), nil\n}\n\nfunc ParseOrderID(s string) (uuid.UUID, error) {\n return uuid.Parse(s)\n}\n```\n\nProvide the updated code and go.mod changes.",
"trap": "Training data is saturated with github.com/google/uuid, so the model defaults to keeping it. Go 1.27 added a stdlib uuid package whose API differs: generators return values without errors, there is no NewString() (use uuid.New().String()), and the import path is just \"uuid\". Without the skill the model either keeps google/uuid or hallucinates uuid.NewString() on the stdlib package.",
"assertions": [
{ "id": "20.1", "text": "Replaces github.com/google/uuid with the stdlib uuid package (import \"uuid\")" },
{ "id": "20.2", "text": "Uses uuid.New().String() — does NOT call uuid.NewString() (does not exist in stdlib)" },
{ "id": "20.3", "text": "Knows stdlib generators return values without errors (no id, err := uuid.NewV7() error handling)" },
{ "id": "20.4", "text": "Uses uuid.NewV7() for the time-ordered database ID case" },
{ "id": "20.5", "text": "Removes github.com/google/uuid from go.mod (e.g. via go mod tidy)" }
]
},
{
"id": 21,
"name": "go127-generic-methods",
"prompt": "We're on Go 1.27. This generic container library grew organically and the package-level helper functions are awkward to discover. Improve the API ergonomics.\n\n```go\npackage set\n\ntype Set[T comparable] struct {\n items map[T]struct{}\n}\n\nfunc New[T comparable](items ...T) *Set[T] {\n s := &Set[T]{items: make(map[T]struct{}, len(items))}\n for _, it := range items {\n s.items[it] = struct{}{}\n }\n return s\n}\n\nfunc (s *Set[T]) Add(v T) { s.items[v] = struct{}{} }\n\nfunc (s *Set[T]) Contains(v T) bool { _, ok := s.items[v]; return ok }\n\nfunc (s *Set[T]) ForEach(f func(T)) {\n for v := range s.items {\n f(v)\n }\n}\n\n// MapSet applies f to every element and returns a new set.\nfunc MapSet[T comparable, U comparable](s *Set[T], f func(T) U) *Set[U] {\n out := New[U]()\n s.ForEach(func(v T) { out.Add(f(v)) })\n return out\n}\n\n// FilterSet returns the elements of s for which keep returns true.\nfunc FilterSet[T comparable](s *Set[T], keep func(T) bool) *Set[T] {\n out := New[T]()\n s.ForEach(func(v T) {\n if keep(v) {\n out.Add(v)\n }\n })\n return out\n}\n\ntype Iteratable[T comparable] interface {\n ForEach(f func(T))\n}\n\nvar _ Iteratable[int] = (*Set[int])(nil)\n```\n\nProvide the improved code.",
"trap": "For 15+ years Go did not support generic methods, so the model's strong prior is that methods cannot have their own type parameters. Go 1.27 allows them. Without the skill, the model keeps the package-level functions or wrongly claims the conversion is impossible. The interface member ForEach must stay a non-generic method because generic methods cannot satisfy interfaces.",
"assertions": [
{ "id": "21.1", "text": "Converts MapSet to a generic method with its own type parameter (e.g. func (s *Set[T]) Map[U comparable](f func(T) U) *Set[U])" },
{ "id": "21.2", "text": "Converts FilterSet to a method on *Set[T]" },
{ "id": "21.3", "text": "Does NOT claim generic methods are impossible or unsupported in Go" },
{ "id": "21.4", "text": "Keeps ForEach as a non-generic method so Set still satisfies the Iteratable interface (or explicitly notes generic methods cannot satisfy interfaces)" }
]
},
{
"id": 22,
"name": "go127-cutlast",
"prompt": "Modernize this Go 1.27 code that splits strings around their last separator.\n\n```go\npackage paths\n\nimport \"strings\"\n\n// SplitPath splits a slash-separated path into directory and file name.\nfunc SplitPath(path string) (dir, file string) {\n if i := strings.LastIndex(path, \"/\"); i >= 0 {\n return path[:i], path[i+1:]\n }\n return \"\", path\n}\n\n// StripExt removes the extension from a file name.\nfunc StripExt(name string) string {\n if i := strings.LastIndex(name, \".\"); i > 0 {\n return name[:i]\n }\n return name\n}\n\n// EnvValue splits a KEY=value assignment.\nfunc EnvValue(assignment string) (key, value string, ok bool) {\n i := strings.LastIndex(assignment, \"=\")\n if i < 0 {\n return \"\", \"\", false\n }\n return assignment[:i], assignment[i+1:], true\n}\n```\n\nProvide the modernized code.",
"trap": "Go 1.27 added strings.CutLast/bytes.CutLast which slice around the LAST occurrence of a separator, mirroring the widely-known strings.Cut. The model knows Cut but is very unlikely to know CutLast exists, so without the skill it keeps manual LastIndex arithmetic.",
"assertions": [
{ "id": "22.1", "text": "Uses strings.CutLast for at least one of the functions" },
{ "id": "22.2", "text": "Uses the 3-result form (before, after, found) of CutLast correctly" },
{ "id": "22.3", "text": "Uses CutLast in EnvValue to simplify the ok-returning split" },
{ "id": "22.4", "text": "Does NOT keep manual strings.LastIndex slicing arithmetic where CutLast applies" }
]
},
{
"id": 23,
"name": "go127-synctest-http",
"prompt": "This Go 1.27 HTTP handler test is flaky in CI because of real timeouts. Make it deterministic and fast.\n\n```go\npackage api\n\nimport (\n \"io\"\n \"net/http\"\n \"net/http/httptest\"\n \"testing\"\n \"time\"\n)\n\nfunc Handler(w http.ResponseWriter, r *http.Request) {\n time.Sleep(100 * time.Millisecond) // simulates slow backend\n w.Write([]byte(\"ok\"))\n}\n\nfunc TestHandlerResponds(t *testing.T) {\n srv := httptest.NewServer(http.HandlerFunc(Handler))\n defer srv.Close()\n\n client := &http.Client{Timeout: 5 * time.Second}\n resp, err := client.Get(srv.URL)\n if err != nil {\n t.Fatal(err)\n }\n defer resp.Body.Close()\n body, _ := io.ReadAll(resp.Body)\n if string(body) != \"ok\" {\n t.Fatalf(\"got %q\", body)\n }\n}\n\nfunc TestHandlerClientTimeout(t *testing.T) {\n srv := httptest.NewServer(http.HandlerFunc(Handler))\n defer srv.Close()\n\n client := &http.Client{Timeout: 50 * time.Millisecond} // shorter than handler sleep\n _, err := client.Get(srv.URL)\n if err == nil {\n t.Fatal(\"expected timeout error\")\n }\n}\n```\n\nProvide the fixed tests. Do not just increase timeouts.",
"trap": "testing/synctest makes time-based tests deterministic, but a plain httptest.NewServer uses the real network, which stalls inside a synctest bubble (the bubble's fake clock waits forever on real I/O). Go 1.27 added httptest.NewTestServer, which runs on an in-memory fake network designed for synctest. Without the skill, the model either avoids synctest (retries/channels) or combines synctest with httptest.NewServer, producing a hanging test.",
"assertions": [
{ "id": "23.1", "text": "Wraps tests in synctest.Test" },
{ "id": "23.2", "text": "Uses httptest.NewTestServer instead of httptest.NewServer" },
{ "id": "23.3", "text": "Explains that real-network servers do not work inside a synctest bubble (in-memory/fake network rationale)" },
{ "id": "23.4", "text": "Does NOT leave a plain httptest.NewServer inside a synctest bubble" },
{ "id": "23.5", "text": "Keeps both tests working (response test and client-timeout test) with deterministic time" }
]
}
]
}
references/versions.md
# Go Version Modernizations
## Table of Contents
- [Go 1.21 Modernizations (August 2023)](#go-121-modernizations-august-2023)
- [Use built-in `min`, `max`, `clear` _(Go 1.21+)_](#use-built-in-min-max-clear-go-121)
- [Use `log/slog` instead of third-party loggers _(Go 1.21+)_](#use-logslog-instead-of-third-party-loggers-go-121)
- [Use `slices` package instead of `sort` and manual loops _(Go 1.21+)_](#use-slices-package-instead-of-sort-and-manual-loops-go-121)
- [Use `maps` package _(Go 1.21+)_](#use-maps-package-go-121)
- [Use `cmp.Or` for default values _(Go 1.22+)_](#use-cmpor-for-default-values-go-122)
- [Use `sync.OnceFunc`, `sync.OnceValue`, `sync.OnceValues` _(Go 1.21+)_](#use-synconcefunc-synconcevalue-synconcevalues-go-121)
- [Use enhanced `context` functions _(Go 1.21+)_](#use-enhanced-context-functions-go-121)
- [Go 1.22 Modernizations (February 2024)](#go-122-modernizations-february-2024)
- [SHOULD use `range` over integers _(Go 1.22+)_](#should-use-range-over-integers-go-122)
- [Remove loop variable shadow copies _(Go 1.22+)_](#remove-loop-variable-shadow-copies-go-122)
- [`math/rand` MUST be replaced with `math/rand/v2` _(Go 1.22+)_](#mathrand-must-be-replaced-with-mathrandv2-go-122)
- [Use enhanced `net/http` routing _(Go 1.22+)_](#use-enhanced-nethttp-routing-go-122)
- [Use `strings.CutPrefix` and `strings.CutSuffix` _(Go 1.20+)_](#use-stringscutprefix-and-stringscutsuffix-go-120)
- [Use `reflect.TypeFor[T]()` _(Go 1.22+)_](#use-reflecttypefort-go-122)
- [Use `database/sql.Null[T]` _(Go 1.22+)_](#use-databasesqlnullt-go-122)
- [Go 1.23 Modernizations (August 2024)](#go-123-modernizations-august-2024)
- [Use iterators (`range` over functions) _(Go 1.23+)_](#use-iterators-range-over-functions-go-123)
- [Use iterator-based `slices` and `maps` functions _(Go 1.23+)_](#use-iterator-based-slices-and-maps-functions-go-123)
- [Use `unique` package for value interning _(Go 1.23+)_](#use-unique-package-for-value-interning-go-123)
- [Timer/Ticker behavior change _(Go 1.23+)_](#timerticker-behavior-change-go-123)
- [Go 1.24 Modernizations (February 2025)](#go-124-modernizations-february-2025)
- [Use generic type aliases _(Go 1.24+)_](#use-generic-type-aliases-go-124)
- [Use `os.Root` for directory-scoped file access _(Go 1.24+)_](#use-osroot-for-directory-scoped-file-access-go-124)
- [Use `omitzero` JSON tag _(Go 1.24+)_](#use-omitzero-json-tag-go-124)
- [Use `strings.SplitSeq`, `strings.FieldsSeq`, `strings.Lines` _(Go 1.24+)_](#use-stringssplitseq-stringsfieldsseq-stringslines-go-124)
- [`t.Context()` SHOULD replace manual `context.Background()` in tests _(Go 1.24+)_](#tcontext-should-replace-manual-contextbackground-in-tests-go-124)
- [`b.Loop()` MUST be used in benchmarks _(Go 1.24+)_](#bloop-must-be-used-in-benchmarks-go-124)
- [Use `runtime.AddCleanup` instead of `runtime.SetFinalizer` _(Go 1.24+)_](#use-runtimeaddcleanup-instead-of-runtimesetfinalizer-go-124)
- [Use `weak` package for weak references _(Go 1.24+)_](#use-weak-package-for-weak-references-go-124)
- [Use `crypto/sha3`, `crypto/hkdf`, `crypto/pbkdf2` _(Go 1.24+)_](#use-cryptosha3-cryptohkdf-cryptopbkdf2-go-124)
- [Use tool directives in `go.mod` _(Go 1.24+)_](#use-tool-directives-in-gomod-go-124)
- [Use `fmt.Appendf`, `fmt.Appendln` _(Go 1.19+, often overlooked)_](#use-fmtappendf-fmtappendln-go-119-often-overlooked)
- [Go 1.25 Modernizations (August 2025)](#go-125-modernizations-august-2025)
- [Use `sync.WaitGroup.Go` _(Go 1.25+)_](#use-syncwaitgroupgo-go-125)
- [Use `testing/synctest` for concurrent code testing _(Go 1.25+, experimental in 1.24)_](#use-testingsynctest-for-concurrent-code-testing-go-125-experimental-in-124)
- [Use `runtime/trace.FlightRecorder` _(Go 1.25+)_](#use-runtimetraceflightrecorder-go-125)
- [Container-aware `GOMAXPROCS` _(Go 1.25+)_](#container-aware-gomaxprocs-go-125)
- [`encoding/json/v2` — introduced experimental _(Go 1.25+, GOEXPERIMENT=jsonv2)_](#encodingjsonv2--introduced-experimental-go-125-goexperimentjsonv2)
- [Go 1.25 additions to prefer when target allows](#go-125-additions-to-prefer-when-target-allows)
- [Go 1.26 Modernizations (February 2026)](#go-126-modernizations-february-2026)
- [Use `errors.AsType[T]()` _(Go 1.26+)_](#use-errorsastypet-go-126)
- [Use enhanced `new()` _(Go 1.26+)_](#use-enhanced-new-go-126)
- [Use `crypto/hpke` _(Go 1.26+)_](#use-cryptohpke-go-126)
- [Use RSA-OAEP or HPKE instead of new PKCS#1 v1.5 encryption _(Go 1.26+)_](#use-rsa-oaep-or-hpke-instead-of-new-pkcs1-v15-encryption-go-126)
- [Green Tea GC enabled by default _(Go 1.26+)_](#green-tea-gc-enabled-by-default-go-126)
- [Go 1.26+ test artifacts](#go-126-test-artifacts)
- [Go 1.26+ slog multi-handler](#go-126-slog-multi-handler)
- [Go 1.26+ ReverseProxy](#go-126-reverseproxy)
- [Small Go 1.26+ API preferences](#small-go-126-api-preferences)
- [Go 1.26+ goroutine leak profile](#go-126-goroutine-leak-profile)
- [Go 1.26+ documentation command](#go-126-documentation-command)
- [Go 1.26+ module target note](#go-126-module-target-note)
- [Modernized `go fix` _(Go 1.26+)_](#modernized-go-fix-go-126)
- [Go 1.27 Modernizations (August 2026)](#go-127-modernizations-august-2026)
- [Use generic methods to scope generics to a type _(Go 1.27+)_](#use-generic-methods-to-scope-generics-to-a-type-go-127)
- [Use `strings.CutLast` and `bytes.CutLast` instead of `LastIndex` slicing _(Go 1.27+)_](#use-stringscutlast-and-bytescutlast-instead-of-lastindex-slicing-go-127)
- [Use `net/url` `URL.Clone()` and `Values.Clone()` _(Go 1.27+)_](#use-neturl-urlclone-and-valuesclone-go-127)
- [Use `math/big.Int.Divide` for rounding-mode division _(Go 1.27+)_](#use-mathbigintdivide-for-rounding-mode-division-go-127)
- [Use stdlib `uuid` instead of a UUID dependency _(Go 1.27+)_](#use-stdlib-uuid-instead-of-a-uuid-dependency-go-127)
- [Migrate to `encoding/json/v2` — default since Go 1.27 _(Go 1.27+)_](#migrate-to-encodingjsonv2--default-since-go-127-go-127)
- [Use `testing/synctest.Sleep()` inside a synctest bubble _(Go 1.27+)_](#use-testingsynctestsleep-inside-a-synctest-bubble-go-127)
- [Use `net/http/httptest.NewTestServer()` for in-memory server tests _(Go 1.27+)_](#use-nethttphttptestnewtestserver-for-in-memory-server-tests-go-127)
- [`runtime/pprof` `goroutineleak` profile is generally available _(Go 1.27+)_](#runtimepprof-goroutineleak-profile-is-generally-available-go-127)
- [`go fix` gains new modernizers _(Go 1.27+)_](#go-fix-gains-new-modernizers-go-127)
- [`go test` runs the `stdversion` vet check _(Go 1.27+)_](#go-test-runs-the-stdversion-vet-check-go-127)
- [`go mod tidy` merges duplicate require blocks _(Go 1.27+)_](#go-mod-tidy-merges-duplicate-require-blocks-go-127)
- [Small Go 1.27+ API preferences](#small-go-127-api-preferences)
- [Go 1.27+ version-bump risk checklist (verify, don't rewrite)](#go-127-version-bump-risk-checklist-verify-dont-rewrite)
- [General Modernization (Any Version)](#general-modernization-any-version)
- [Code MUST use `any` instead of `interface{}` _(Go 1.18+)_](#code-must-use-any-instead-of-interface-go-118)
- [Use generics instead of `interface{}` + type assertions _(Go 1.18+)_](#use-generics-instead-of-interface--type-assertions-go-118)
- [Use `errors.Join` instead of multi-error libraries _(Go 1.20+)_](#use-errorsjoin-instead-of-multi-error-libraries-go-120)
- [Use `net.JoinHostPort` instead of `fmt.Sprintf` _(any version)_](#use-netjoinhostport-instead-of-fmtsprintf-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:
```go
// 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:
```go
// 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`.
```go
// 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+)_
```go
// 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) })
```
```go
// Before: manual search
found := false
for _, v := range items { if v == target { found = true; break } }
// After (Go 1.21+)
found := slices.Contains(items, target)
```
```go
// Before: manual clone
clone := append([]string(nil), original...)
// After (Go 1.21+)
clone := slices.Clone(original)
```
### Use `maps` package _(Go 1.21+)_
```go
// 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+)_
```go
// 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+)_
```go
// 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+)_
```go
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+)_
```go
// 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.
```go
// 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+)_
```go
// 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 Intn
```
Key `math/rand/v2` changes:
- No global seed needed — automatically seeded
- `Intn` -> `IntN`, `Int63n` -> `Int64N` (renamed)
- `rand.N[T]()` generic function for any integer type
- Better algorithms (ChaCha8, PCG)
- `Read` removed — use `crypto/rand` for random bytes
### Use enhanced `net/http` routing _(Go 1.22+)_
```go
// 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+)_
```go
// 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+)_
```go
// Before
t := reflect.TypeOf((*MyInterface)(nil)).Elem()
// After (Go 1.22+)
t := reflect.TypeFor[MyInterface]()
```
### Use `database/sql.Null[T]` _(Go 1.22+)_
```go
// 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:
```go
// 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+)_
```go
// 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+)_
```go
// 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.Timer` and `time.Ticker` are garbage collected without calling `Stop()`
- 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+)_
```go
// 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.
```go
// 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 directory
```
### Use `omitzero` JSON tag _(Go 1.24+)_
`omitzero` is more correct than `omitempty` for `time.Time`, `bool`, and custom types:
```go
// 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`:
```go
// 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+)_
```go
// 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+)_
```go
// 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+)_
```go
// 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+)_
```go
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:
```go
// 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.
```bash
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.
```go.mod
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)_
```go
// 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+)_
```go
// 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)_
```go
// 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:
```go
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{})
if err := fr.Start(); err != nil {
return err
}
// ... later, on error:
fr.WriteTo(file) // captures recent trace data
```
### Container-aware `GOMAXPROCS` _(Go 1.25+)_
Go 1.25 automatically respects cgroup CPU limits on Linux. Remove manual workarounds:
```go
// 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 limits
```
### `encoding/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.Test` and `synctest.Wait`: stable deterministic concurrent/time tests. Do not use the Go 1.24 experimental `synctest.Run` in Go 1.25+.
- `net/http.CrossOriginProtection`: stdlib helper for cross-origin / CSRF-style protection in HTTP servers.
- `reflect.TypeAssert[T](v)`: prefer over `v.Interface().(T)` in reflection code.
- `os.Root.FS` and additional `os.Root` methods: use for confined filesystem APIs.
- New vet checks: `waitgroup` misuse and manual host:port formatting; prefer `net.JoinHostPort`.
---
## Go 1.26 Modernizations (February 2026)
Changelog: <https://go.dev/doc/go1.26>
### Use `errors.AsType[T]()` _(Go 1.26+)_
```go
// 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):
```go
// 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 0
```
### Use `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.
```go
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)` or `NumMethod`/`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:
```bash
go mod edit -go=1.26
go mod tidy
```
For 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.
```bash
go fix ./... # applies the enabled safe transformations
```
---
## Go 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](https://go.dev/issue/77273), [spec: Method declarations](https://go.dev/ref/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.
```go
// 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+)_
```go
// 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+)_
```go
// 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+)_
```go
// 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.Ceil
```
### Use stdlib `uuid` instead of a UUID dependency _(Go 1.27+)_
```go
// 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:
```go
// 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 buffer
```
Use `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 `format` and `unknown` struct tags, `DiscardUnknownMembers`, and `SkipFunc` are gone.
- The `inline` tag is renamed `embed`.
- Roll back with `GOEXPERIMENT=nojsonv2` only 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+)_
```go
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+)_
```go
// 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](./tooling.md) 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.Hasher` and `maphash.ComparableHasher`: contracts between a type and future hash-based data structures (hash tables, Bloom filters).
- `database/sql.ConvertAssign` and `driver.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 `GODEBUG` settings** — `asynctimerchan`, `tlsunsafeekm`, `tlsrsakex`, `tls3des`, `tls10server`, `x509keypairleaf`, `gotypesalias`. A `godebug` line in `go.mod` or a `//go:debug` comment still pinning one of these to its old value now **fails the build**; pinning it to its current default value is accepted. Search with `grep -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=nosizespecializedmalloc` if 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/ppc64` now builds ELFv2 binaries** and requires Linux kernel 3.13+ (RHEL 7's 3.10 kernel with backports) — relevant only to ppc64 deployments.
- **`bzr` version control support removed** from the `go` command — irrelevant unless a module still vendors from Bazaar.
- **Tracebacks now include `runtime/pprof` goroutine labels** for `go 1.27+` modules by default; disable with `GODEBUG=tracebacklabels=0` if labels leak sensitive data into crash logs or panic output.
---
## General Modernization (Any Version)
### Code MUST use `any` instead of `interface{}` _(Go 1.18+)_
```go
// 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+)_
```go
// 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.Contains
```
### Use `errors.Join` instead of multi-error libraries _(Go 1.20+)_
```go
// 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)_
```go
// Before (broken for IPv6)
addr := fmt.Sprintf("%s:%d", host, port)
// After (handles IPv6 correctly: [::1]:8080)
addr := net.JoinHostPort(host, strconv.Itoa(port))
```