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:

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.