Known issues
This page lists specific bugs, each with a workaround. For the limits that come with running Go on .NET, and the rough spots real code hits today, see Limitations.
Writes through the address of another package’s pointer or interface variable are lost
Taking the address of a variable that another package exports gives a pointer to the variable itself when its
type is a value type. When its type is a pointer or an interface (&otherpkg.ErrMissing, for an
ErrMissing error), the pointer refers to a copy, and so does the address of runtime.MemProfileRate and of a
variable declared with a two-argument //go:linkname.
Limitations describes the cases and the
workarounds.
Ubuntu’s packaged .NET 10 can end a converted program early
On Ubuntu’s own build of .NET 10 (the dotnet installed with apt, runtime identifier ubuntu.*-x64), a
converted program that raises and recovers Go panics inside a hot loop can stop in one of two ways under the
default tiered compilation:
- a fatal
Internal CLR error. (0x80131506); - more often, a SILENT exit with code 0 and truncated output.
The fault needs the JIT’s on-stack replacement. Microsoft’s build of the same .NET version does not show it, and neither does Windows. No converted Go program has reproduced it so far, only a synthetic reproduction.
To avoid it, run on Microsoft’s .NET build, or turn off quick JIT for loops, which removes on-stack replacement. A build property does it without touching the converted project:
dotnet build -p:TieredCompilationQuickJitForLoops=false
The built program’s runtimeconfig.json then carries
"System.Runtime.TieredCompilation.QuickJitForLoops": false. Or set it in the converted executable’s project
file, though the next conversion rewrites that file:
<TieredCompilationQuickJitForLoops>false</TieredCompilationQuickJitForLoops>
Converted executable projects carry this line, commented out. It costs about 90 ms (16%) more startup time, and steady-state loop speed stays within measurement noise. It becomes the default if a converted Go program ever reproduces the fault.
A Native AOT publish of a program that references the packages is slow and large
A program that references the go.* packages and publishes with Native AOT (PublishAot) runs. The check
that gates each release publishes a C# consumer of the packages and runs it on linux-x64, win-x64,
osx-arm64 and osx-x64. A project that sets no trim mode of its own gets the packages’ default, partial,
which is the setting a converted project carries in its generated project file.
A full trim (TrimMode set to full) produces a program that runs as well. That is measured on win-x64, on
one development machine; the hosted check measures the default.
What it costs. With partial, Native AOT compiles every go.* assembly the program references in
full, not only the parts the program uses. For a small program that references about thirty of them, the
publish took under four minutes on a hosted CI runner for Linux and for Windows, about six minutes on a Mac
with Apple silicon and about eleven on an Intel Mac (measured 2026-10-14). Before golib’s generic-cycle fix
of that day, the same publish took about an hour on Linux and Windows and nearly five hours on an Intel Mac,
did not finish within six hours on Apple silicon, and produced an executable of about 250 MB. A converted
project pays the same cost, because it uses the same setting. A build, dotnet run and a publish without
Native AOT are not affected. The fix first shipped in the go.* 1.26.10.1 packages; for 1.24.13.7 and earlier,
see the section that follows.
With the go.* 1.24.13.7 packages or earlier, a Native AOT publish can take hours or not finish
Symptom. A Native AOT publish (PublishAot) of a program that references the go.* 1.24.13.7 packages, or an
earlier release, does not finish or takes hours. The ILC compiler holds about 14 GB of memory and prints tens of
thousands of warnings that a method will always throw because a type from golib failed to load. Measured on
win-x64, on one development machine.
Cause. These releases were built before the fix to the runtime library’s generic cycle (see the Native AOT section). Native AOT expands that cycle until it reaches its nesting limit, and compiles each method that reaches past the limit to throw, which is what those warnings report.
Fix. Move to the go.* 1.26.10.1 packages or later. They carry the fix, and the 1.24.13.7 packages’ deprecation
notice already points to them.
Workaround on 1.24.13.7. Turn off identical-method folding in the project file:
<IlcFoldIdenticalMethodBodies>false</IlcFoldIdenticalMethodBodies>
The publish then finishes, at a cost: ILC peaked at about 28 GB, and the executable was 3.3 times the size of the same program built against 1.26.10.1.
A single-file publish with ReadyToRun runs out of memory
A converted program’s project turns ReadyToRun on for dotnet publish. Adding -p:PublishSingleFile=true
to that publish ran the ReadyToRun compiler, crossgen2, to about 10 GB of memory on a linux-x64 machine,
where it failed with Insufficient memory and left the build hanging. The publish profiles go2cs writes
beside a converted program (-p:PublishProfile=linux-x64 and the others) turn ReadyToRun off, so a profile
publish is not affected. A program converted by a go2cs earlier than 1.26.10.1 keeps the profiles it already
has, since go2cs never overwrites one; add <PublishReadyToRun>false</PublishReadyToRun> to them. On the
command line, turn ReadyToRun off yourself:
dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true -p:PublishReadyToRun=false
The program then starts without ReadyToRun’s head start, as it does under dotnet run.
On Linux, a converted test suite can skip its unix-socket tests under a long TMPDIR
The converted test host keeps each run’s temporary files under go2cs-tests/<package>/<run id>/.tmp inside
TMPDIR, about 60 bytes deeper than Go’s own t.TempDir. Linux limits a unix socket’s path to 108 bytes, so a
test that binds a socket there can fail to bind under a long TMPDIR and is reported as skipped where Go passes
it (bufio’s TestCopyUnixpacket, for example). Run converted test suites with a short TMPDIR, such as
/tmp; 10 bytes or fewer leaves every socket path in range.