go2cs Roadmap
This page is the plan for what comes next in go2cs: the Go and .NET releases the converted standard library moves to, the platforms it runs on, the real-world Go code it converts beyond the standard library, and the runtime and converter work still ahead. It states what is planned. The completed phases, with the git commits and tags that anchor them, are in Roadmap history. Dates are month or quarter targets.
Where things stand. Every implementable package of the converted standard library validates
against Go’s own tests on Windows and Linux, and the same validation is under way on macOS, package
by package. go2cs -tests -recurse validates real third-party modules against their own tests the
same way, and the first converted module is published as a NuGet package.
Validated Test Packages, the validation roster, carries the current state
of the standard library, and Validated Modules that of third-party modules.
What that rests on. The converter is guarded by hundreds of Go-versus-C# behavioral test projects.
Each one is transpiled, compiled and compared against a committed expected output, and each runnable
one is executed with its output compared against Go’s. The whole Go standard library compiles cleanly
as .NET assemblies. It reproduces Go built with -tags purego: a managed runtime cannot execute Go’s
hand-written assembly, so the portable pure-Go variants of the assembly-backed crypto and hash
functions are the faithful target
(Conversion Strategies).
Compiling is not the same as running correctly, which is what Phase 4 below is for.
Phase 4: validation against Go’s own tests
In short. Phase 4 checks the converted standard library against Go’s own tests, package by package, so that “it works” is measured and not claimed. It is done on Windows and Linux, under way on macOS, and has one planned extension.
Phase 4 converts each package’s _test.go suite and compares it, verdict for verdict, with go test.
It is not finished, and it stands in three parts:
- Phases 4A to 4C are complete. The test pipeline, subtests and parallelism, and the package-by-package campaign are done: every implementable package validates on Windows and Linux. Their record is in Roadmap history.
- macOS parity is in progress. Each roster row gains a macOS count when the package validates on both Apple silicon and Intel with the two agreeing test for test. The roster rows carry the live state; see Platforms.
- Phase 4D is planned. It widens validation to examples, fuzz seed corpora and benchmarks; see below.
How the project moves: release hops
In short. go2cs follows Go and .NET one release at a time, and every move proves every package again. For a user that means each go2cs release names exactly one Go release and one .NET release.
The project advances by release migrations, called hops: moving the converted
standard library (the corpus) to a new Go release or to a new .NET runtime, one variable at a time
and never both in one hop. Each Go hop re-validates every package against that release’s own tests.
This page states which releases come next. Three documents carry the work itself, and this roadmap
links to them rather than restating them: PLAN-corpus-upgrade.md, which
frames how hops are run, and the two standing runbooks that lead on procedure,
GoCorpusMigration.md for a Go release migration and
DotNetMigration.md for a .NET one. What each document type is for, and which
one wins when two disagree, is in Glossary.md.
Go releases
In short. The converted standard library is on Go 1.24.13, a release Go no longer patches, so it lacks the security fixes Go has issued since. Two planned moves bring it to a supported Go release and keep it there: Go 1.26, straight from 1.24, and then Go 1.27. 1.24.13.7 is the last 1.24 release.
Go 1.24 left upstream support in February 2026, and the 1.25 series left it in August 2026, so packages built from either carry none of the Go security fixes issued since. The Go 1.24 line is closed: 1.24.13.7 is its last release. The next hop goes straight to Go 1.26, with no Go 1.25 release, and the hop after it brings Go 1.27:
| Hop | When | What it involves |
|---|---|---|
| Go 1.26 (1.26.9), straight from 1.24.13 | After 1.24.13.7, the last 1.24 release | One hop across two Go releases. Go 1.25’s changes are behavioral (its nil-check fix, updated unicode tables, the AllocsPerRun change) plus testing-API additions in the converted test host; Go 1.26 adds new(expr) and self-referential generic constraints. Every failure is first attributed to the release that caused it, by converting at 1.25.14 as well, without publishing it. Then every package is re-validated at 1.26. |
| Go 1.27 | Q1 2027, by about February | Generic methods, which need a conversion design first. The 1.26 series leaves support when Go 1.28 ships, around February 2027, so the corpus reaches 1.27 before then. |
How often later hops run is decided ahead of the Go 1.27 hop.
.NET
In short. Converted code targets .NET 10, a long-term-support release, and stays there until .NET 12. You need the .NET 10 SDK, and a newer SDK on your machine does not change what you get.
The published target stays net10.0. .NET 10 is a long-term-support release, supported until
November 2028, and converted packages stay on it until net12.0, the next long-term-support release.
The SDK and the C# language version are pinned. A repository global.json pins the .NET SDK,
and every project, including the template the converter emits, fixes LangVersion instead of
following latest, so a machine whose default SDK moves to a newer .NET does not silently compile
converted code under a newer C#. The converter also escapes Go identifiers that newer C# gives new
meaning, such as closed, union and safe when they name a type.
A .NET 11 measurement stage follows the Go 1.26 hop, once .NET 11 has shipped (November 2026), and
never overlaps a Go hop. It compares JIT and allocation behavior on .NET 11 over identical code, and
reviews the deployment shape, including the full-trim publish floor, which depends on the runtime
library’s trim safety (Native AOT and trimming;
PLAN-bflat-perf-exploration.md). Its findings inform the move
to net12.0; net10.0 stays the published target until then.
Declared host limits and their retirement path
The roster’s host-limit disclosure class, defined in
ValidatedTestPackages.md, covers tests whose premise rests on a property
of the test binary that the converted host’s deployment shape structurally lacks. An entry must
name that structural property, never a fixable defect, and it is written to retire itself when the
shape gains the property: disclosed tests keep running in every validation run, so an entry that
starts passing breaks the roster’s disclosure arithmetic loudly and must be removed. The converted
test host publishes as a self-contained, relocatable single-file executable, so Go tests that copy
and relaunch their own binary pass.
Entries remain in crypto/tls, os, os/exec and syscall, and not every one retires through the
deployment shape; the roster’s class text names what each one needs. In crypto/tls’s
TestBogoSuite, BoringSSL’s test runner starts the host once per case inside Go’s own 10-minute test
deadline. The interop itself passes, and the run is bound by throughput rather than start-up time, so
no start-up work alone retires it. The entry retires when managed steady-state TLS throughput brings
the run inside the runner’s deadline, which is what the TLS work under Performance targets.
Native AOT and trimming
In short. You can publish converted Go as a native executable today, and it runs, but the publish can take hours and the file is large. This work makes that publish smaller, quicker and free of warnings, in stages.
A converted program, and a C# program that references the go.* packages, can be published with Native
AOT. Making that publish smaller, faster and free of trim warnings is staged work on the runtime
library and the source generators:
- Shipped in 1.24.13.5. Every package registers the Go types it defines, and the runtime library’s
trim warnings are more than halved. A Native AOT publish runs on
linux-x64,win-x64andosx-x64, which the check that gates each release measures with the default trim. A fully trimmed build (TrimModeset tofull) runs for the first time, measured onwin-x64on one development machine. - Shipped in 1.24.13.6.
reflectworks under Native AOT without generating code at run time: the source generators write the operations it needs at build time. - Shipped in 1.24.13.7. Under a fully trimmed publish,
fmtfinds a value’s ownStringandErrormethods again, and reflection reaches a struct’s fields through a pointer. Both cost size, and a Native AOT publish now takes longer: theosx-x64publish no longer fits the hosted runner the release check uses, so 1.24.13.7 was measured onlinux-x64andwin-x64only. - Later stages cover method sets and stack frames.
The cost today is real. With the default trim, Native AOT compiles every go.* assembly a program
references in full, so a publish takes hours and the executable is large.
Known issues
has the measurements. A build, dotnet run and a publish without Native AOT are not affected.
Platforms
In short. go2cs is validated on x64 Windows and x64 Linux. macOS, on Intel and on Apple silicon, has packages and is being validated package by package now. Other platforms are not supported yet.
Windows and Linux. The converter runs on both, and the standard library is multi-target on disk:
platform-shared files sit flat, and a package whose Go source varies by platform keeps one folder per
target OS (design). Each platform-varying package ships a
win-x64 and a linux-x64 flavor, and roster rows record their Linux verdict counts beside the
Windows record. Remaining Linux operational items are tracked in
PLAN-linux-operation.md and the multi-target design.
macOS (Intel and Apple silicon): packages ship for both chips, the behavioral suite passes on both,
and the README walkthrough runs. The standard library is being validated package by package on macOS
now. A roster row gains its macOS count only when both chips agree with each other and with Go, and
the roster rows show which packages have one. Until a package has its
count, don’t expect it to be fully operational there. The work also finds macOS-only gaps, each fixed
as it is found. The compile is checked daily in CI on macOS arm64 and x64 runners
(CIMatrix.md).
Other architectures receive the Windows flavor until a flavor ships for them: a platform-varying
package’s Windows flavor is what compiles and loads, and its platform-entangled behavior (local time
zones, sockets, the syscall surface) follows Windows semantics or fails.
Real-world Go modules
In short. Beyond the standard library, go2cs converts third-party Go modules and holds each to the same bar: its own tests must match Go’s. Five modules pass today, and a larger one is next.
Third-party modules are the proving ground after the standard library. Each module is converted,
validated against its own Go test suite exactly as a standard-library package is, and given a proof
page beside the conversion rather than on the standard-library roster. go2cs -tests -recurse runs a
module’s tests across all of its packages, and a validated module packs as one NuGet package.
Five modules validate today, each on the platforms Validated Modules names:
golang.org/x/sync, google/uuid,
golang-jwt/jwt v5, ritchiecarroll/hashset
and spf13/pflag. uuid is one package with no dependencies; it
brings Go’s v1, v3, v5 and v6 UUIDs to .NET, where System.Guid generates v4 and v7. jwt is the first
module whose tests span several of its own packages, and it exercises the converted crypto stack in
real code. The next candidate:
| Module | Why it comes here | When |
|---|---|---|
itchyny/gojq v0.12.19, with its dependency itchyny/timefmt-go v0.1.8 |
The first conversion with a dependency. It gives .NET an in-process, fully managed jq, with no native binary per platform. | Q4 2026 |
gojq’s full command-line conformance suite is a later second stage, once its command-line package’s own dependencies convert. Further modules follow; the go2cs Target Atlas is one input to their selection.
NuGet packages of converted modules
In short. A Go module that someone has already converted can arrive as a NuGet package, so your conversion restores it and does not convert it again. Converted modules are published this way today, and nugetgo.net, the registry that maps Go modules to their packages, lists them.
With -recurse=nuget, a converted program references the standard library, the runtime and the
source generators as published NuGet packages, and the same substitution reaches third-party modules:
the nugetgo.net community registry maps Go module paths to published NuGet
packages of their conversions, so a dependency someone has already converted becomes a package restore
instead of a local transpile. The converter side is built and on by default (see
Mapping modules to NuGet packages). The design is
PLAN-nugetgo.md.
- Package IDs: a converted module publishes as
nugetgo.<dotted module path>; thego.prefix is the converted standard library’s alone. - A package rehearses first, on a local feed, before it reaches nuget.org.
- Converted modules are published. The nugetgo.net registry lists each
one, with the Go module and version it was converted from. A program that imports a listed module
converts with
-recurse=nugetto a project that restores the package instead of converting the module again. - The package-ID prefixes are not reserved yet. The reservation requests are with NuGet, which has not decided them.
- Packages built from an unsupported Go release are labelled as proofs, and are rebuilt once the converted standard library is on a supported release.
cgo
In short. Go code that calls C through cgo cannot be converted yet. The converter says so and stops, naming the file, and a bridge to native code is planned from 2027.
A Go file that imports "C" is not converted, and the standard library is converted with
CGO_ENABLED=0. The plan is PLAN-cgo-interop.md:
- It fails loudly today. When a file the build selects imports
"C", the conversion stops with errorGO2CS2001, which names the file and the cause, instead of skipping the file silently (Limitations). Test-only and platform-deselected files are skipped as usual. - Next, a phased P/Invoke bridge (from Q1 2027, as capacity allows): recognize and extract the C declarations, generate P/Invoke calls and blittable structs, then marshal pointers, strings and slices, starting with a slice limited to the C library. Platform order: Linux, then Windows, then macOS. Each target needs a C toolchain on the converting machine.
Phase 4D: examples, fuzz seed corpora and benchmarks
In short. Validation counts Go’s Test functions today. Phase 4D adds the other kinds of test a
Go package carries: examples, fuzz seeds and benchmarks. It is planned and not yet started.
Validation counts a package’s Test functions. Phase 4D, the next validation slice, widens what
“validated” covers:
- runnable
Examplefunctions, with their declared output compared; - fuzz seed corpora, run as deterministic tests before any active fuzzing is considered;
- benchmarks, compiled and executed for compatibility, with their speed kept outside the correctness gate.
Active fuzzing, race-detector equivalence and coverage-percentage equivalence stay outside the
correctness gate. The design kickoff is planned for Q1 2027, and the work proceeds without delaying
Phase 5. Detail:
TestingInfrastructureRequirements.md.
Phase 5 — Implement assembly-backed declarations in C#
In short. Phase 5 is about completeness, not speed. A small set of standard-library declarations
has no portable Go body, because Go implements them in assembly, in cgo or inside its runtime. Today
each of those compiles to a stub that throws a NotImplementedException, naming the declaration, if
it is ever called. Phase 5 gives each one a real C# body. Making portable code that already works
run faster is a different subject, under Performance.
What it means for you. No validated package’s tests reach a stub, and a call that does reach one
fails loudly and by name, never silently. Which exported functions can reach one is measured, in the
exported-reach record. On Windows and Linux the only exported
calls that do are ones Go itself refuses to carry out in an ordinary program: clearing coverage
counters in a program built without coverage, and runtime.Breakpoint. They are being changed to
answer exactly as Go does. On macOS, verifying a certificate against the system’s root certificates
and CPU profiling also reach a stub. Those are part of the macOS work
(Platforms).
The converted library reproduces Go built with -tags purego, so most assembly-backed code takes its
portable Go path and needs nothing from Phase 5. The declarations it is about are the ones with no Go
body at all: they are backed by assembler, cgo, runtime or compiler intrinsics, or platform services.
The PartialStubGenerator writes the throwing stub for each until a C# implementation exists.
It proceeds package by package, as validation demands, not as one campaign. When a package’s own
tests need a declaration, it gets a hand-written C# body and the package validates with it. That is how
most of the surface that matters has been implemented so far, and it is how the macOS validation is
closing the gaps it finds now: os.Executable, the system page size, and an arm64 path for
hash/crc32. A declaration no validated test reaches can still be a throwing stub, and a program that
calls one stops with a NotImplementedException that names it.
The remaining stubs are measured. The Phase 5 stub inventory is a measured inventory of the declarations that still compile against throwing stubs, per package and per operating system. None of them is reached by a validated package’s tests. The record carries the counts, so this page quotes none.
How a declaration gets its implementation.
- The converter keeps the bodyless declaration as a
partialmethod, and a hand-owned companion such as*_impl.cssupplies the body; where the Go source uses a declaration-oriented file, the converted*_decl.csis paired with a*_impl.cs.sync/atomic’sdoc.csanddoc_impl.csare the model. Regenerable declaration output is never hand-edited, and the generated stub disappears for a member once its real implementation is present. - Behaviorally equivalent managed code (
Interlocked,Volatile,BitOperations, spans, BCL crypto) is preferred over instruction-by-instruction translation, preserving Go’s overflow, alignment, memory-ordering, pointer and GC, and platform semantics. - Platform implementations are isolated by target and fail clearly on unsupported targets, and a generic throwing stub never passes for completed platform support. Focused Go/C# behavioral fixtures cover declarations that upstream tests do not reach.
- Compiling alone is not completion. An implementation counts when the package’s converted tests run against it and match Go, with no silent exclusions, and stress and edge tests are added where unit tests are not enough (concurrency, atomics, unsafe pointers, cryptography, platform calls).
What closing Phase 5 means: for every supported target, no unexplained throwing stub remains; each implemented member has a real companion or a documented target exclusion; and every applicable package is validated.
What is closed first. Priority follows the exported-reach record: a stub that an ordinary program can easily reach is closed ahead of everything else. The stubs no exported call reaches stay as they are, by name, until a real use needs one; closing those has no date.
Phase 5 is not the “stage 5” of the trimming work under Native AOT and trimming; the two numberings are unrelated.
Performance
In short. Converted code is usually slower than Go and uses more memory, and the published comparison says by how much. This section lists the work planned to narrow the gap. None of it holds up anything else on this page.
The published performance comparison measures converted C# against Go on the JIT and under Native AOT. The follow-ups run off the critical path:
- A re-baseline at the last Go 1.24 package release, which is also the before
picture for the Go 1.26 hop and the .NET 11 measurement stage. It brings the TLS-handshake
benchmark, already in the suite, into the published table, and profiles the crypto paths behind the
handshake gap; TLS throughput is also what retires the
crypto/tlshost-limit entry. - Allocations (committed; design kickoff planned for Q1 2027): the retirement plan named by each deferred allocation-count disclosure on the roster. Go meets those assertions through compiler optimizations, chiefly escape analysis keeping values off the heap, that the converted code does not yet reproduce. The work closes the gap by construction (storage for Go array values, frame-local allocations kept off the heap, and ref-receiver method emission), and each disclosure retires when its assertion matches.
- Interface conversions (unscheduled, after the last Go 1.24 package release): the remaining items from the interface-shell caching design, a per-value shell-cache experiment and a base-class marker. A converter-emitted per-call-site inline cache stays deferred.
- Stack strings: the two remaining increments below, unscheduled.
Expand the sstring stack-string MVP: the remaining increments
A non-escaping string([]byte) conversion emits a zero-copy sstring view instead of a heap copy:
for a named local, a comparison or switch operand, a concatenation operand, and a repeated conversion
hoisted to one view
(strings reference;
the completed increments are in
Roadmap history).
Two increments remain:
- Non-retaining callee arguments.
string(x)passed to a callee that never retains it, such asstrconv.ParseUint(string(s), …). A@stringparameter copies the view back at the boundary, so this pays only where the callee accepts a view (ansstringtwin gives a registered function that overload), and it needs a curated list of non-retaining callees. - A positional, loop-carried liveness guard. Relax “the source is never written” to “the source is
not written between the conversion and the local’s last read, and is not loop-shared”. This is the
only increment that reaches the
PerfStringhot loop and the one with the highest silent risk, so it gets the heaviest adversarial pass.
Why widening is safe to try. sstring is a ref struct, so the .NET compiler rejects almost
every over-reach (storing it in a field, array or map, boxing it, sending it on a channel, capturing
it in a closure) at build time. The only silently-wrong vectors are escape through return and
mutation of the source buffer during the view’s lifetime, and both stay explicitly guarded. Each
increment extends the SStringElision behavioral test, inspects every newly emitted site in a full
standard-library reconvert, and re-runs the performance suite.
Not pursued: for range string(x), which has no standard-library sites; and m[string(x)] map
keys, where a zero-allocation lookup would need a custom comparer that slows every @string-keyed map.
IDE mode (an experiment)
A gated experiment in writing Go in an editor and running and debugging it as .NET, with the debugger showing Go values the way a Go developer expects. Its feasibility is tested first, and nothing in it is a commitment yet. The plan: IDE mode.
Open converter items
In short. Known, smaller gaps in the converter, none of them scheduled. The validated standard library and modules convert despite them; one matters to you only if your code uses that construct.
Smaller converter items, unscheduled. The first is recorded in the
struct-types reference;
the rest are tracked in src/go2cs/ToDo.md:
- Cross-package address of a global.
&otherpkg.Vardoes not box the global, because the addressed-global scan covers only the package under conversion. - The remaining dynamic-struct implicit-cast checks in the
AssignStmt,CompositeLit,IndexExpr,BinaryExpr,UnaryExpr,SelectorExpr,TypeSwitchStmtandValueSpecvisitors. - Complete comment conversion, free-floating comments included, which depends on golang/go#20744 or the DST package.
- Go assembler (
.s) targets. One idea is to compile them to object code and wrap it as a native library; the standard library’s pure-Go paths cover the need in the meantime. - Accessibility “publicizing” as a source-generator pass, a cosmetic change.
Timeline
In short. The order the work on this page is planned in. Few rows carry a date; most wait on something else finishing first.
| When | Planned |
|---|---|
| Now | macOS validation of the standard library, package by package. The trimming work for Native AOT. Further converted modules as NuGet packages. |
| Next | The Go 1.26 hop, straight from 1.24.13.7, the last 1.24 release. The performance re-baseline at 1.24.13.7. |
| After the Go 1.26 hop | The .NET 11 measurement stage (.NET 11 ships in November 2026). timefmt-go and gojq, as their prerequisites land. The generic-methods design for Go 1.27. The decision on how often later hops run. |
| Q1 2027 | Go 1.27 hop, by about February. The cgo bridge’s first phases (C library only; Linux, then Windows, then macOS). The Phase 4D design kickoff. The allocation work’s design kickoff. |
| Unscheduled | The Phase 5 stubs that no exported call reaches, each closed when a real use needs it. Interface-conversion performance work. The remaining stack-string increments. Open converter items. |
How progress is tracked
In short. Where to check each claim on this page for yourself.
| What | Where |
|---|---|
| Validation | Validated Test Packages, the authoritative roster: each package’s verdict counts on Windows and Linux, its macOS count once it has one, its disclosures with their stated reasons, and its proof page. Its header carries the current totals. |
| Third-party modules | Validated Modules: each validated module, its version, the platforms it was measured on, and its proof pages. |
| Compilation | The whole converted standard library builds with zero errors (src/go2cs-stdlib.slnx) for Windows and Linux; the macOS compile runs daily in CI (CIMatrix.md). |
| The converter | Hundreds of Go-vs-C# behavioral projects under src/tests/Behavioral, each transpiled, compiled, compared against a committed golden and, where it is a runnable program, output-compared against Go. |
| Performance | The performance comparison. |
| Releases | News. |
Every converter change runs the same loop: edit src/go2cs/*.go → go build (with the Go release
src/go2cs/go.mod names) →
re-transpile → dotnet build, and a whole-corpus reconvert shows no unintended drift before the change
lands.