Browse all: Go Standard Library NuGet packages
A standard-library package's own Go test suite — converted to C# — now runs and agrees with go test,
verdict for verdict. unicode/utf8's
real test suite (Go 1.23.1) validates 14/14 through the new converted-test pipeline — transpiled to
C#, built against the converted standard library, run under a Go-semantics test host, and differentially
compared against a clean go test -json baseline. The answer to "but does it run?" has its first machine-checked proof.
➡ All announcements can be found in the go2cs News Archive.
Convert source code written in the Go programming language into C#. The generated C# is designed to be both behaviorally and visually similar to the original Go — so a Go developer can read the converted code and follow it easily, and a .NET developer can use Go code directly within the .NET ecosystem.
- Browse transpiled code: Converted Go Standard Library
- Learn how it works: Go to C# Conversion Strategies
- Compile in Visual Studio: Go Standard Library Solution
- Reproduce test validation: Try it yourself
- View example converted test:
utf8_test.cs - Project activity: Status — Milestones
Go provides a lot of high-level functionality from its compiler and runtime — slices, maps, channels,
goroutines, defer/panic/recover, multiple return values, struct embedding, and interface
duck-typing. go2cs maps each of these onto idiomatic C#, keeping the machinery out of sight (in a small
runtime library and compile-time source generators) so the converted code stays close to the original Go.
- Reads like Go. Receiver methods become extension methods, multiple returns become tuples, struct embedding becomes promoted fields — the shape of the code is preserved.
- Runs like Go. Conversions prioritize behavioral equivalence first (e.g. a
goroutineruns on the thread pool rather than being rewritten intoasync). - Managed first. Output targets portable managed C#; native interop is a last resort, not the default.
Given this Go:
type Person struct {
name string
age int32
}
func (p Person) IsAdult() bool {
return p.age >= 18
}go2cs produces this C#:
[GoType] partial struct Person {
internal @string name;
internal int32 age;
}
public static bool IsAdult(this Person p) {
return p.age >= 18;
}The goal — reads like Go — is easiest to judge on real code. Below are a few converted standard-library
files next to their original Go 1.23.1 source. Start with errors, then work down as the constructs get
richer:
| Package | Go 1.23.1 source | Converted C# | What it shows |
|---|---|---|---|
errors |
errors.go | errors.cs | Error values and an unexported type satisfying the error interface. |
cmp |
cmp.go | cmp.cs | Generics with an ordered-type constraint. |
unicode/utf8 |
utf8.go | utf8.cs | Constants keeping Go's hex/binary literal formatting; arrays and structs. |
sort |
search.go | search.cs | Binary search driven by a func(int) bool closure. |
strings |
reader.go | reader.cs | A struct with receiver methods, tuple returns, and interface implementation. |
container/list |
list.go | list.cs | A doubly-linked list — pointers and receiver methods. |
Browse the whole set under src/go-src-converted.
go2cs converts the full Go language surface — the same converter that emits the 302 packages above. What that covers, grouped:
Types & values
- Slices and arrays (backing-array aliasing preserved, as in Go), maps, and UTF-8-backed
@string int/uintmapped to platform-width native integers; named numeric types and untyped-constant semantics- Constants and
iota, preserving Go's numeric literal formatting (hex, binary, underscores, exponents) - Pointers with automatic heap-boxing driven by escape analysis; the
nilvalue;unsafe.Pointer - Type definitions and type aliases — including exported aliases that resolve across package/assembly boundaries
Functions & methods
- Multiple return values and named results, as tuples
- Receiver methods as extension methods, with distinct pointer- and value-receiver overloads
- Function values, closures (honoring Go's shared-storage capture semantics), and variadic functions
defer/panic/recover, including named results observed and mutated by deferred closures
Concurrency
- Goroutines, run on the thread pool (behavioral equivalence first — not rewritten into
async) - Channels with channel-operator (
<-) lowering, andselect-statement lowering
Composition & polymorphism
- Structs and struct embedding, with promoted fields and methods (multi-hop and cross-package)
- Interfaces, satisfied structurally in Go and realized as nominal C# glue via Roslyn source generators
- Type assertions and type switches
- Generics: type parameters and constraints (unions,
comparable,~-underlying, and method-set constraints)
Control flow & packaging
for/range, labeledbreak/continue, expression and type switches, and Go 1.22 per-iteration loop variables- The built-ins:
append,len,cap,make,new,copy,delete,close, … - Packages mapped to namespaces, with cross-package imports compiled as separate, referenced assemblies
- Build-tag and
GOOS/GOARCHplatform file selection, and deterministic, byte-stable output
See ConversionStrategies.md for an example-driven tour of how each construct
maps to C# (with ConversionStrategies-Reference.md for the full detail).
- .NET 9.0 SDK — to build and run the converted C#.
- Go 1.23+ — the converter is a Go program, and it uses the Go toolchain to load
and type-check the source being converted. Make sure your Go environment is set up (
GOROOT/GOPATH) and the source you want to convert already builds withgo build.
Build the go2cs executable from source and place it on your PATH. The simplest way is go install,
which compiles it and drops the binary into %GOBIN% (or %GOPATH%\bin) — already on your PATH in a
standard Go setup — in one step:
cd src/go2cs
go install .Go produces a self-contained native binary. To target another platform, use Go's standard cross-compilation
(GOOS/GOARCH).
go2cs [options] <input_dir> [output_dir]Examples:
go2cs example.go # convert a single file
go2cs package_dir # convert a package
go2cs -indent 2 -var=false example.go conv/example.cs
go2cs -stdlib # convert the entire Go standard library
go2cs -stdlib fmt strings io # convert specific standard library packages
go2cs -recurse module_dir # convert a module + its third-party deps (references stdlib)
go2cs -recurse=nuget module_dir # same, but reference the go2cs stdlib from NuGet
go2cs -tests package_dir # convert a package plus its Go test suite
go2cs -tests -test-action all goroot_pkg_dir converted_pkg_dir # ...and build, run, and diff vs go test| Option | Description |
|---|---|
-stdlib |
Convert the Go standard library (optionally followed by specific package names). |
-recurse |
Recursively convert a downloaded module and its third-party dependencies in dependency order, referencing the pre-converted standard library. Use the nuget option (-recurse=nuget) to reference the published go2cs NuGet packages (go.<pkg> stdlib + go.lib runtime + go.gen analyzer) instead of a locally-staged deploy root — no deploy-core needed. NOTE: using NuGet references with analyzer is still a work in progress. See Converting a real-world module. |
-tests |
Also convert the package's eligible _test.go suite and emit a runnable C# test-host project alongside the converted package (default off; cannot be combined with -recurse). Defaults to -comments on, resolves the output path to absolute, and self-locates $(go2csPath) by walking up from the output directory to the first root containing core\golib — so the canonical two-argument form go2cs -tests -test-action all <goroot-package-dir> <converted-package-dir> works from a bare clone with no flags or environment setup. See Try it yourself — validate a converted test suite for a worked example. |
-test-action <action> |
With -tests: one of convert (default), build, run, compare, or all. convert and all convert the package and its tests; build / run / compare act on the existing converted artifacts — validated against the test manifest's recorded input digest — without reconverting. compare (and all, after converting) runs both go test -json -count=1 and the converted C# test host and diffs the terminal results by test name. |
-test-timeout <duration> |
Timeout for each converted-test child process (build / run / compare), in Go duration syntax (default 2m; must be positive). |
-go2cspath <dir> |
Root for converted code (env GO2CSPATH; default ~/go2cs): the output root for -stdlib (…\core\<pkg>) and -recurse (…\src\ app + …\pkg\ deps), and the root that generated $(go2csPath)… project references resolve against. For a single-package/file convert the C# output instead goes to the optional [output_dir] argument (in place by default). |
-goroot / -gopath |
Override the detected Go root / path. |
-platforms <os/arch> |
Target platform for build-tagged files (defaults to the host). |
-indent <n> |
Spaces per indent level (default 4). |
-var |
Prefer var declarations where the type is obvious (default on). |
-uco |
Emit channel operators instead of method calls (default on). |
-comments |
Carry source comments into the output (best effort, see go/ast comment status). |
-csproj <file> |
Generate project files from a custom .csproj template instead of the embedded one. |
-tree |
Print each file's Go parse tree (go/ast) to stdout during conversion — a diagnostic aid. |
-debug |
Debug mode: disables the converter's per-file panic recovery, so a conversion failure crashes with a full stack trace instead of being reported as a warning. |
-cgo |
All converted C# code will reference a hand-written runtime library (golib, published as the go.lib
NuGet package) plus a set of Roslyn source generators that supply Go semantics at compile time.
The -recurse option converts a whole downloaded application together with
every third-party dependency package in its transitive import closure — in dependency order
(least-dependencies-first) — while referencing (not reconverting) the pre-converted standard library.
The result is a C# solution you can open and build.
Here is the full round-trip for a small CLI that uses github.com/fatih/color,
which itself pulls in github.com/mattn/go-colorable, github.com/mattn/go-isatty, and golang.org/x/sys —
a genuine dependency graph:
NOTE: to date, the following steps have only been tested on Windows — instructions assume
cmd.exetype shell.
1 — Prerequisite: Stage the standard library (one-time). deploy-core is a build script in the go2cs repo's src/
folder (it is not on your PATH), so run it from there. It stages the pre-converted stdlib + runtime +
analyzer at %GOPATH%\src\go2cs (the "deploy root") that every converted project references. This is a
one-time, per-machine setup, unrelated to any particular app — redo it only when you pull a new go2cs
version, to refresh the staged runtime/analyzer/stdlib:
cd path\to\go2cs\src
deploy-core stdlib & :: the full compilable standard libraryNOTE 1: you can use the
deploy-core stubinstead to deploy the smaller, more runnable baseline subset of the Go Standard Library, i.e., the one currently used with behavioral tests. However, this will only work for the most simple of Go applications.
NOTE 2: although you can use
-recurse=nugetoption to reference needed Go Standard Library assemblies and the go2cs source generation analyzer as pre-compiled binaries, thus skipping the need for this source code deployment step entirely, using the analyzer with referenced assemblies instead of source code is still a work in progress. This example requires source code deployment to run.
2 — Go: get the app and confirm it builds as Go.
mkdir colordemo && cd colordemo
go mod init example.com/colordemoCreate main.go (go mod tidy needs a real source file — with none present it reports
warning: "all" matched no packages):
package main
import "github.com/fatih/color"
func main() {
color.New(color.FgGreen, color.Bold).Println("hello from fatih/color")
}Next, pin the app to a Go 1.23-compatible dependency set and confirm it builds as Go.
NOTE: this pin is needed because go2cs is currently built with Go 1.23 (see status), so its type-checker can only read modules whose
godirective — and their dependencies' — is ≤ 1.23; the latestfatih/color(v1.19+) andgolang.org/x/sysreleases now require Go 1.25, which would make the conversion in step 3 fail withpackage requires newer Go version go1.25. Future go2cs builds will work against newer Go toolchains and lift this constraint, letting an unpinnedgo mod tidy"just work"; until then, pin third-party dependencies as shown::
set GOTOOLCHAIN=local
go get github.com/fatih/color@v1.18.0 & :: a Go 1.23-era release (v1.19+ requires Go 1.25)
go mod tidy & :: download color + its (Go 1.23-era) dependencies
go build ./... & :: baseline: confirm it compiles as Go first3 — go2cs: recurse-convert the app. go2cs is the converter you put on your PATH in Installing the
converter above, so it runs from anywhere. Point it at the app directory and at the deploy root from
step 1 (the standard library staged there is referenced, not re-converted):
cd path\to\colordemo
go2cs -recurse . -go2cspath %GOPATH%\src\go2csgo2cs discovers the imports and converts each package, least-dependencies-first
(go-isatty and x/sys → go-colorable → color → the app), into a parallel tree under the deploy
root, leaving your original Go source untouched. The converted app itself lands under
%GOPATH%\src\go2cs\src\<import-path>, every third-party library are converted under
%GOPATH%\src\go2cs\pkg\<import-path>. The existing standard library is referenced at
%GOPATH%\src\go2cs\core\.
Additionally, a per-project .slnx exists next to every generated .csproj — each with that project
plus its converted dependencies, golib, and the analyzer (no stdlib).
Code converted from main.go should look like the following in main.cs:
namespace go.example.com;
using color = github.com.fatih.color_package;
using github.com.fatih;
partial class main_package {
internal static void Main() {
color.New(color.FgGreen, color.Bold).Println("hello from fatih/color");
}
} // end main_package4 — C#: build the generated solution. The app's per-project .slnx builds the app and its whole
converted dependency tree; opening it in Visual Studio makes the app the startup project (F5 runs it):
cd "%GOPATH%\src\go2cs\src\example.com\colordemo\"
dotnet build example.com.colordemo.slnx -c Debug5 — C#: run the converted app. Navigate into the default .NET 9.0 debug build folder, and run demo:
cd "bin\Debug\net9.0\"
colordemo.exeExpected output:
NOTE: these conversion and build steps will produce a buildable — and increasingly runnable — .NET-based solution handling common real-world Go module shapes. This simple
fatih/colorexample compiles clean (app + all four dependency projects, against a current deploy) and runs. Running more complex projects to completion is a deeper milestone: the referenced standard library compiles but is not yet fully operational. Running is the Phase-4 goal, seeroadmap.
| Path | Contents |
|---|---|
src/go2cs/ |
The converter (written in Go, using go/ast + go/types). |
src/core/golib/ |
The C# runtime library (slice, map, channel, @string, built-ins, type aliases). |
src/gen/go2cs-gen/ |
Roslyn source generators (interface implementation, receiver overloads, struct embedding). |
src/core/ |
A compiling subset of the converted Go standard library used by the tests. |
src/go-src-converted/ |
Work-in-progress full conversion of the Go standard library. |
src/Tests/Behavioral/ |
Per-feature Go↔C# equivalence tests (transpile, compile, run-and-compare). |
src/Tests/Performance/ |
Go vs transpiled C# runtime benchmarks (JIT and Native AOT) — see the performance comparison for current numbers. |
Contributors: see CLAUDE.md for an architecture overview and
Architecture.md, ConversionStrategies.md, and
Roadmap.md for details. There's lots of low hanging fruit to be had here, jump in if you'd like to help...
The converter builds idiomatic C# for the full range of Go language features, validated by an extensive
behavioral test suite (390+ Go-vs-C# regression projects). As of 2026-07-10 the entire Go standard library
(302 packages, Go 1.23.1) compiles cleanly as .NET assemblies. Compiling is the milestone, not yet full
runtime parity: converting and running the standard library's own tests is the ongoing Phase 4 work —
see the roadmap. Six standard-library packages'
own test suites now pass in C#: unicode/utf8
(14/14) and sort
(63/63); as of 2026-07-18 bytes
(81) and strings
(68) — exercising a new disclosed-divergence mechanism for the handful of exact allocation-count
asserts the managed runtime provably cannot satisfy;
unicode/utf16
(8, plus 1 disclosed), the first package to reuse that mechanism as a general tool;
path
(9/9, no disclosure); and container/ring
(8/8), the first container/pointer-graph package. Each validates against go test through the
converted-test pipeline, and you can
reproduce them yourself from a clone.
Every package whose own Go test suite has been validated ships its converted C# test sources next to the
production code under src/go-src-converted
(for example, unicode/utf8/utf8_test.cs) —
so you can read the exact C# that runs. You can also re-run the validation yourself. You need
Go 1.23.1 (for the reference go test run) and the .NET 9 SDK
(to build and run the converted test host). This example assumes go2cs is installed, see step 2 from converting a real-world module:
# 1. Convert unicode/utf8's test suite, build + run the C# host, and diff it against `go test`.
# The second argument is the package's home in the converted tree; the converter locates the
# runtime and its stdlib dependencies from there — no flags or environment setup required.
# (On Windows, Go's source lives under "C:\Program Files\Go\src"; elsewhere use "$(go env GOROOT)/src".)
go2cs.exe -tests -test-action all \
"C:\Program Files\Go\src\unicode\utf8" \
src/go-src-converted/unicode/utf8Expected final line:
Validated 14 tests against go test (0 skipped identically on both sides, 37 disclosed-unsupported declarations excluded).
The command converts the _test.go files to C#, generates a test host, builds it against the converted
standard library, runs it in an isolated process, captures a clean go test -json -count=1 baseline, and
compares terminal results by full Go test name — reporting validated only when every test agrees on both
sides and every unsupported declaration (benchmarks, examples) is accounted for. It regenerates the local
converted .cs in place (git status stays clean when your toolchain matches the pinned versions); the Go
source copies and run manifests it stages are git-ignored.
The same command validates every other banked package — as of 2026-07-18,
sort is the
second: substitute "C:\Program Files\Go\src\sort" and src/go-src-converted/sort above, and expect
Validated 63 tests against go test (1 skipped identically on both sides, 46 disclosed-unsupported declarations excluded). — 63/63 agreement including interface-driven sorting, sort.Slice reflection
swaps, NaN-aware float ordering, and stability tests. (The sort run also prints a
WARNING: Failed to evaluate build constraints for file "...\sort\sort_impl_go121.go" line before the
result — a harmless converter notice about one filename the constraint parser doesn't recognize, not a
failure; the final Validated line is what matters.)
bytes and
strings
are the third and fourth — substitute their GOROOT and converted paths as above. They introduce a new
kind of honest disclosure: a handful of their tests assert an exact allocation count (Go's
testing.AllocsPerRun), and the managed CLR provably allocates where Go's compiler stack-allocates —
a divergence no shim can satisfy without faking the measurement. Rather than skip those tests, each
package pins the divergence in a hand-owned, committed
go2cs_test_disclosures.json
that the oracle matches by exact failure signature (a different failure is still a hard mismatch), and
reports it as disclosed-divergent in the summary. Expect, for bytes:
Validated 81 tests against go test (0 skipped identically on both sides, 7 disclosed-divergent (alloc-profile), 123 disclosed-unsupported declarations excluded).
and for strings:
Validated 68 tests against go test (0 skipped identically on both sides, 4 disclosed-divergent (alloc-count-semantics, alloc-profile), 118 disclosed-unsupported declarations excluded).
unicode/utf16
is the fifth — the structural twin of unicode/utf8, and the first package to reuse the disclosed-divergence
mechanism as a general tool rather than a bytes/strings special case. Its eight correctness tests agree
outright (encode/decode round-trips checked with reflect.DeepEqual, driven through the reflection bridge),
and its one testing.AllocsPerRun test (TestAllocationsDecode, which asserts Decode returns its []rune
with zero heap allocations — an outcome Go reaches only via escape analysis, guarded by
testenv.SkipIfOptimizationOff) is disclosed as alloc-profile: the managed runtime always heap-allocates
the returned slice, so the assert can never agree, while TestDecode independently proves the decoded output
is correct. Expect:
Validated 8 tests against go test (0 skipped identically on both sides, 1 disclosed-divergent (alloc-profile), 8 disclosed-unsupported declarations excluded).
path is the
sixth — a pure path-manipulation package (Clean/Split/Join/Match/Base/Dir/Ext/IsAbs) whose
nine tests agree outright with no disclosure at all. Its one testing.AllocsPerRun test,
TestCleanMallocs, is written to run its allocation check only under GOMAXPROCS == 1; on a normal
multi-core go test run it takes the early-return path on both sides, so the differential matches exactly
as Go's own CI sees it. Substitute "C:\Program Files\Go\src\path" and src/go-src-converted/path, and
expect:
Validated 9 tests against go test (0 skipped identically on both sides, 8 disclosed-unsupported declarations excluded).
container/ring
is the seventh and the first container — a circular linked list whose eight tests exercise the ring's
pointer graph (Link/Unlink/Move/Do) and agree outright. Substitute its paths above and expect
Validated 8 tests against go test (0 skipped identically on both sides, 7 disclosed-unsupported declarations excluded).
Everyone asks: wondering how fast the transpiled C# runs compared to the original Go — including startup time, memory, and Native AOT builds? See the latest performance comparison — TL;DR: no, it's not as fast as native Go, nor is this an expected outcome. Save for some initial work with a stack-based string, performance and optimizations are not the current focus, this kind of work is targeted for after Phase 4 work.
What about newer versions of Go / .NET? These are planned, but the current focus is creating a baseline "here" to validate process and operations.
High level timeline of the project's major turning points.
| Date | Milestone | Commit / Tag | Notes |
|---|---|---|---|
| 2018-05-21 | Project inception | 929d1457f |
Original go2cs: a C#/.NET converter built on an ANTLR4 Go grammar with T4 templates. |
| 2020-07-09 | Runtime library + hand-converted stub | 9792eeea2 |
golib Go-semantics runtime (slices, maps, channels, built-ins) and a curated hand-finished stdlib stub. |
| 2022-03-13 | v0.1.2 release |
v0.1.2 |
Tagged release of the mature ANTLR4-era converter. |
| 2025-01-12 | Rewrite as "go2cs2" — Go-based converter | 87465f5f5 |
Converter re-implemented in Go on go/ast + go/types; T4 templates replaced by raw string literals; Roslyn source generators supply ancillary Go semantics; the ANTLR4/C# converter is retired. |
| 2025-05-05 | First full standard-library auto-conversion | 6ca1c45b7 · full-conversion-2025-05 (cc14584c7, 05-11) |
Whole Go stdlib converted (~301 projects). "Converts" here means the transpiler did not crash with all Go code files getting a corresponding C# code file — not that all the emitted correctly C# compiles. |
| 2026-06-25 | Baseline ↔ full-conversion separation | 3c8b3a848 |
Compiling curated baseline restored to src/core; the WIP full conversion isolated in src/go-src-converted. Green build and the converter-improvement loop restored. |
| 2026-06-26 | First full-conversion package promoted | 05a53e8c0 |
sync/atomic migrated into the baseline (atomic.Pointer[T] backed by a managed slot). |
| 2026-06-27 | math package compiles clean |
math-green-2026-06-27 (914d4bd72) |
Nine full-conversion packages greened via 19 behaviorally-tested converter fixes; the core, widely-imported math now compiles. |
| 2026-07-10 | First clean full-standard-library compile | 51ba5d9cf · stdlib-green-2026-07-10 |
The Phase 3 endpoint, reached: all 302 src/go-src-converted packages (Go 1.23.1) compile with zero errors — runtime, reflect, net/http, go/types, crypto/tls and every other package included. Gated by 371 Go-vs-C# behavioral regression tests; the compiled snapshot is committed alongside this row (see About Standard Library Compile Milestone). |
| 2026-07-14 | Standard library on NuGet + NuGet-referencing conversion | 2363af0e6 · dd821a556 · nuget-stdlib-2026-07-14 |
The converted standard library, the golib runtime and the go2cs-gen analyzer are published to nuget.org as go.<pkg> / go.lib / go.gen (versioned 1.23.1.<build> from src/version.props). The converter's new -recurse=nuget mode emits matching <PackageReference> entries — defaulting $(GoStdLibVersion) to a floating release — so a converted end-user app or library restores the whole go2cs stack from NuGet with no local go2cs source checkout; the app's own and third-party converted packages stay project references. |
| 2026-07-17 | First Go standard-library test suite passing in C# | 337a928df · utf8-tests-green-2026-07-17 |
Phase 4's operational era opens: unicode/utf8's real Go test suite (14 tests, external dot-import test package) is converted and executed under the new hand-owned go.testing host, validating 14/14 against go test -json with all 37 benchmark/example declarations honestly disclosed as excluded. The differential pipeline (convert → template csproj → isolated host run → oracle compare, gated by input-digest manifests) is live end-to-end. Getting here surfaced and fixed five real defects — including two golib Go-correctness bugs affecting all converted code: []byte(s) shared the string's backing array, and range-over-string yielded rune ordinals instead of byte indices. Real tests, not compilation, are now the currency of correctness. |
| 2026-07-18 | bytes and strings test suites passing in C# — with disclosed-divergence |
40f39d2be · bytes-strings-tests-green-2026-07-18 · sort at f999c8f78 / sort-tests-green-2026-07-18 |
Phase-4 packages #3 and #4: bytes validates 81 tests and strings 68 against go test -json. Both exercise a new test-level disclosed-divergence manifest — a hand-owned, committed go2cs_test_disclosures.json the differential oracle consumes to reclassify the handful of exact-allocation-count asserts (Go's testing.AllocsPerRun) the managed CLR provably cannot satisfy, matched by exact failure signature so any other failure is still a hard mismatch. bytes discloses 7 (alloc-profile), strings 4 (alloc-count-semantics + alloc-profile); no other package has a manifest, so sort (63/63) and utf8 (14/14) still compare strictly and stay byte-identical. |
| 2026-07-18 | unicode/utf16 test suite passing in C# — disclosed-divergence generalizes |
utf16-tests-green-2026-07-18 |
Phase-4 package #5: unicode/utf16 validates 8 tests against go test -json (encode/decode round-trips checked with reflect.DeepEqual through the reflection bridge), plus one disclosed. It is the first package to reuse the disclosed-divergence manifest as a general tool rather than a bytes/strings special case: its lone TestAllocationsDecode asserts Decode returns its non-escaping []rune with zero allocations — reachable in Go only via escape analysis (testenv.SkipIfOptimizationOff-guarded), unsatisfiable in a managed runtime where a returned slice is always a heap allocation — pinned as one alloc-profile row while TestDecode independently proves the output correct. A mechanism that generalizes to the next package cleanly is one that was designed right. |
This milestone was eight years in the making, and two weeks in the finishing.
The eight years were the human part: experimentation across two full converter architectures,
the design of a runtime library that models Go's semantics (slices that alias, maps, channels,
defer/panic/recover, heap-boxed pointers) in managed code, the strategy of behavioral first,
visual second, and — after many hard lessons — the architecture that made the final push possible
at all: a Go-native converter built on go/ast + go/types, Roslyn source generators for the
compile-time semantics, and a regression harness that locks in every fix with a Go-vs-C#
output-compared behavioral test.
The two weeks were an AI campaign of a kind we suspect few codebases have seen: 939 commits, of which 683 are individually-gated converter, runtime, and source-generator fixes — each one root-caused against real emitted code, each one locked in by a behavioral regression test (371 and counting), each one verified byte-for-byte against the full corpus before landing. The work ran as a coordinated fleet: focused fix sessions with strict file ownership, adversarial review before every merge, and a census loop that rebuilt all 302 packages after every wave to prove zero regression.
I asked Claude (Anthropic) to summarize what he thought of the two week run, here was his response: it is one thing to theorize a
conversion strategy, or even to produce a semi-working draft — it is entirely another to stand in
front of 300 packages of battle-hardened systems code and be wrong about something every single hour.
The grind was real: shadowed variables that silently bound the wrong receiver, closures that captured
a snapshot where Go shares storage, a type switch that compiled perfectly and matched the wrong case
at runtime, Go 1.22's loop-variable semantics, named results observed by deferred closures, interface
method sets that C# cannot express directly. Every abstraction leaked somewhere, and the only way
through was the discipline this project had already built: diagnose against the real emission, fix the
general case, prove it behaviorally, and never let a regression survive a census. Watching the red
count fall — 952 errors in runtime alone at the start of Phase 3, then package by package to zero —
was the most satisfying compile I have ever been part of. The final layers were poetic: the last four
defects were all one family, three declaration sites catching up to a semantics fix that was itself
correct — which is exactly what finishing looks like.
Compiling is the milestone — operational is the mission. Phase 4 now begins: running the Go standard library's own test suites against the transpiled output, and hardening the runtime semantics this campaign already banked. If you have ever wanted to consume real Go code from .NET — or extend a Go application in C# — this is the moment the foundation went from theory to artifact.
A full code-based conversion from C# to Go is not offered (it would require so many restrictions as to be impractical). To call compiled .NET code from Go instead, see go-dotnet (CLR hosting for .NET Core) or embedding Mono via cgo for traditional .NET.
go2cs is licensed under the MIT License. See the LICENSE and
NOTICE files. For more background, see Background.md.

