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 converted modules are published as NuGet packages, listed on nugetgo.net.
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 validation 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, each chip’s results compared with Go built for that same chip. Where the two chips’ results differ, the row records the stricter of the two and discloses the difference. 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.
Go releases
In short. The converted standard library is on Go 1.26.10, a release Go still supports. The next move is Go 1.27, before the 1.26 series leaves support. 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, and there is no Go 1.25 release. Fix releases on the Go 1.26.10 line continue; 1.26.10.3 is in preparation. The main line of development carries the current Go line, and the next Go minor is prepared separately until its packages ship. The next hop brings Go 1.27:
| Hop | When | What it involves |
|---|---|---|
| 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).
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 a converted helper process once per case inside Go’s
own 10-minute test deadline. Each launch costs about 1.2 s where Go’s costs about 85 ms, measured on
linux-x64 on one 4-core machine, and most of that is the converted process loading its packages. That
per-process start-up cost is what keeps the run outside the deadline, and the entry retires when the
start-up work under Performance brings it inside.
Native AOT and trimming
In short. You can publish converted Go as a native executable today, and it runs, but the publish compiles every converted package it references in full. 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. - In 1.26.10.3 (in preparation). Reading a slice element inlines under Native AOT, as it does under
the JIT, so slice-heavy code runs at its JIT speed: on
win-x64, on one development machine, a matrix-multiply benchmark takes about 36% of its earlier time and a prime sieve about 51%. A 64-bit multiply with a 128-bit result inlines too, which speeds up the TLS handshake. The first type assertion to an interface a type satisfies without declaring it takes about half its earlier time (linux-x64, one 4-core machine). A single-file publish compiles ReadyToRun one assembly at a time, which starts in a fraction of the time of the SDK’s compressed composite image. - Later stages cover method sets and stack frames.
Converted packages are built for the default x64 instruction set. Building for x86-64-v3 gave no gain
across the benchmark suite, and an image built that way does not start on a processor without AVX2.
The check that gates each release publishes a program with Native AOT and runs it on linux-x64,
win-x64 and osx-arm64. On osx-x64 the publish is read when it finishes inside the hosted
runner’s time limit, and reported as not measured when it does not.
The cost today is real. With the default trim, Native AOT compiles every go.* assembly a program
references in full, which costs publish time and executable size.
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. 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. A few Linux operational items remain open.
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, measured with Go 1.24.13, 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, each with the Go release and 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, and libraries come first: a converted module is most useful as Go code a .NET program calls, not as a stand-in for a command-line tool Go already runs well. 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).
- 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:
- 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.
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, on the
Go 1.24 standard library. Every call it confirmed reaching one now reaches a real body
instead. Clearing coverage
counters in a program built without coverage, and runtime.Breakpoint, answer exactly as Go does. On
macOS, verifying a certificate against the system’s root certificates and CPU profiling no longer
reach a stub.
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 all at once. 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, including the gaps the macOS validation
found: 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. A measured inventory lists the declarations that still compile against throwing stubs, per package and per operating system, taken on the Go 1.24 standard library. None of them was reached by a validated package’s tests.
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 that reach measurement: 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, and Tuning converted code says which deployment and runtime settings to choose. The follow-ups run off the critical path:
- The published comparison is measured on the 1.26.10.2 packages, the TLS-handshake benchmark included. It is the before picture for the .NET 11 measurement stage. The Native AOT work it pointed to is in 1.26.10.3 (Native AOT and trimming).
- Start-up cost of a converted process (in progress). A converted process pays .NET’s process
start and loads its converted packages each time it starts: a converted
crypto/tlshelper takes about 1.2 s to start and run one handshake, where Go’s takes about 85 ms (linux-x64, one 4-core machine). Loading the packages is most of it. Faster P-521 key setup and a cheaper first interface assertion are in 1.26.10.3; the remaining lever is the converted process’s own start-up. This cost is also what keeps thecrypto/tlshost-limit entry open. - Allocations (committed; 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. Its design, stack-allocation parity, is written.
-
Goroutine scheduling (planned next, after 1.26.10.3): each live goroutine runs on its own OS thread today, so a program can hold only as many goroutines alive at once as the system allows threads, a limit macOS reaches first (Limitations). The work moves toward Go’s model of many goroutines over a few threads.
- Building against local standard-library sources (planned for 1.26.10.3): a program converted against a
local go2cs source tree and built in its folder gets a Release standard library and publishes. Until
then, build the project file itself,
dotnet build MyProgram.csproj -c Release. - Interface conversions (unscheduled): the remaining interface-shell caching items, 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.
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:
- The address of another package’s pointer- or interface-typed variable. A variable another
package exports with a value type has its own storage, so
&otherpkg.V, a pointer-receiver method value such asf := otherpkg.V.Reset, andflag.BoolVar(&otherpkg.Debug, ...)reach the variable itself, as in Go. Two kinds still give a pointer to a copy, so writes through it are lost: a variable whose type is a pointer or an interface, such as&otherpkg.ErrMissing, and a variable whose storage lives somewhere else, such asruntime.MemProfileRate. Limitations and Known issues give the workarounds. - 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 | The 1.26.10.3 fix release. macOS validation of the standard library, package by package. The trimming work for Native AOT. The start-up cost of a converted process. Further converted modules as NuGet packages. |
| Next | The .NET 11 measurement stage (.NET 11 ships in November 2026). timefmt-go and gojq, as their prerequisites land. Goroutine scheduling, after 1.26.10.3. 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, whose design is written. |
| 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.