Go’s evolution is intentionally slow. By 1.24 in 2026, generics have matured, range-over-func brings real iterators, swiss tables make maps faster, and slog is the standard logging story. This post is the working summary.

Range-over-func — proper iterators

Before:

for i, v := range slice { ... }
for k, v := range mp { ... }

Now (1.23+, mature in 1.24):

func (db *DB) Iter() iter.Seq2[string, []byte] {
    return func(yield func(string, []byte) bool) {
        rows, _ := db.Query("SELECT k, v FROM kv")
        defer rows.Close()
        for rows.Next() {
            var k string; var v []byte
            rows.Scan(&k, &v)
            if !yield(k, v) { return }
        }
    }
}

for k, v := range db.Iter() {
    fmt.Println(k, string(v))
}

Lazy, composable iteration. Replaces channel-based fakery from before generics.

Generics in everyday Go

func MapKeys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

The standard library’s slices, maps, cmp packages give you slice / map / comparator helpers without rolling your own.

Swiss tables

The runtime now uses Swiss-table-based maps. Free 10–20% perf on map-heavy workloads. No code changes needed.

Weak pointers (1.24)

import "weak"

w := weak.Make(&heavyObject)
// ...later
if obj := w.Value(); obj != nil {
    use(obj)
}

Useful for caches that shouldn’t pin big objects in memory. Niche but powerful.

structured logging — slog

import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)

slog.Info("user created", "id", user.ID, "email", user.Email)
slog.Error("failed to process", "err", err, "request_id", reqID)

slog is the new standard. Structured, leveled, JSON-friendly, performant. Replace log everywhere.

For tracing-correlated logs see OpenTelemetry End-to-End in 2026 .

errors.Is/errors.As patterns

err := db.Get(ctx, id)
if errors.Is(err, ErrNotFound) {
    return notFoundResponse()
}
var dbErr *sqlx.DBError
if errors.As(err, &dbErr) && dbErr.Code == "23505" {
    return conflictResponse()
}

The 2026 way to handle errors. Combine with sentinel errors and typed wrappers.

cmp package for comparators

slices.SortFunc(orders, func(a, b Order) int {
    return cmp.Compare(a.Total, b.Total)
})

Cleaner than the old Less func interface.

Context patterns

Always pass ctx context.Context as the first argument:

func (s *Service) GetUser(ctx context.Context, id int64) (*User, error) {
    return s.db.QueryRowContext(ctx, "SELECT ...", id).Scan(...)
}

Deadline propagation, cancellation, request-scoped values all flow through context. Don’t store contexts in structs.

errgroup for parallel calls

g, ctx := errgroup.WithContext(ctx)

var user User; var posts []Post
g.Go(func() error {
    var err error
    user, err = s.GetUser(ctx, id)
    return err
})
g.Go(func() error {
    var err error
    posts, err = s.GetPosts(ctx, id)
    return err
})

if err := g.Wait(); err != nil {
    return err
}

Run two concurrent calls; first error cancels the group. The Go answer to async parallelism.

Build / runtime knobs

GOMAXPROCS=4 ./app                  # bound parallelism
GOMEMLIMIT=512MiB ./app             # GC target
GODEBUG=gctrace=1 ./app             # GC traces

For containerized runs, set GOMAXPROCS to your CPU limit and GOMEMLIMIT to your memory limit. The runtime will behave better under cgroup pressure.

Profiling

import _ "net/http/pprof"
// then run pprof at /debug/pprof

Free profiling endpoint. Production-safe behind auth.

Read this next

If you want a Go 1.24 service template (slog, ctx, errgroup, pgx, otel), it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .