Consuming converted Go from C#

go2cs turns a Go package into an ordinary .NET assembly, so C# can call it like any other library. This page shows what that surface looks like from the C# side: how Go’s strings, slices, multiple results, errors, pointers, maps, channels and goroutines appear to a C# caller.

The examples use two converted modules, github.com/ritchiecarroll/hashset (a generic set built on a Go map) and github.com/google/uuid. Every snippet comes from a small console app, src/tests/CSharpConsumer, which converts both modules, builds against them, and checks that each example still behaves as shown here.

Getting a converted package

There are two ways to get one:

The C# you write is the same either way.

Names: packages, classes and namespaces

A Go package becomes a static class named <package>_package, in a namespace built from the rest of its import path. github.com/google/uuid is the class uuid_package in the namespace go.github.com.google. An exported Go function is a public static method on that class, and a Go method is a C# extension method on the type it belongs to. A Go type is nested in the package class, so uuid.UUID is uuid_package.UUID.

These using directives cover the examples on this page:

using go;                                   // golib: @string, slice<T>, map<K,V>, channel<T>, ж<T>, error
using go.github.com.google;                 // uuid_package (and its extension methods)
using go.github.com.ritchiecarroll;         // hashset_package (and its extension methods)
using static go.builtin;                    // nil, len, goǃ, Ꮡ
using errors = go.errors_package;

go is the namespace of golib, the runtime library that carries Go’s semantics. go.builtin holds Go’s built-in functions and nil. When the converted code comes from NuGet packages, those two arrive with the packages as global usings, so you write only the package namespaces; set the MSBuild property GoConsumerUsings to false to turn them off. A local conversion that you reference as projects does not bring them, so add them yourself, as here.

A Go name that would clash with another name in the same C# class gets a Δ prefix: uuid’s Version type is uuid_package.ΔVersion, because the package class also holds the Version method. Go’s int is a native-sized integer, so it appears in C# as nint.

Calling a function, and strings

uuid_package.UUID id = uuid_package.New();
@string text = id.String();         // a Go string comes back as @string
string s = text;                    // and converts to System.String implicitly

A Go string is @string, a UTF-8 string like Go’s. It converts to and from System.String implicitly, so a C# string can be passed wherever Go takes a string:

@string fromCSharp = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";   // System.String converts the other way

Multiple results and errors

A Go function with several results returns a C# tuple. Go reports failure with an error value rather than an exception, so check it against nil:

var (parsed, err) = uuid_package.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
var (_, bad) = uuid_package.Parse("not-a-uuid");
// err == nil, and bad != nil with bad.Error() describing the problem

C# can make Go errors too, for Go code that takes one:

error mine = errors.New("made in C#");

Arrays

A Go array is a fixed-size value. uuid.UUID is a [16]byte, and it indexes like an array:

// parsed[0] == 0x6b, and len(parsed) == 16
Span<byte> view = parsed.ToSpan();

Slices

A Go slice is slice<T>. A C# array converts to one implicitly, and the slice shares the array, as a Go slice shares its backing array: a write through either one shows in the other. Converting a slice back to T[] makes a copy. ToSpan() gives a Span<T> over the slice’s elements without copying.

int[] numbers = [1, 2, 3];
slice<int> shared = numbers;        // wraps the same array: no copy
shared[0] = 10;                     // numbers[0] is now 10
int[] copy = shared;                // converting back to T[] copies
Span<int> span = shared.ToSpan();   // a view, no copy

Maps and generics

hashset’s HashSet[T] is a Go map type, and its generic functions and methods keep their type parameters:

var set = hashset_package.NewHashSet<int>(new[] { 1, 2, 3 });
set.Add(4);                                 // true: 4 is new
var sum = 0;
foreach (var (key, _) in set) sum += key;   // range over a Go map: (key, value) pairs
slice<int> keys = set.Keys();

len works on a map, a slice, an array or a string, as in Go.

Pointers

A Go *T is ж<T>, a reference to a value on the heap. A method with a pointer receiver takes either a C# ref to a local or a ж<T>:

var target = new uuid_package.UUID();
error uerr = target.UnmarshalText("6ba7b810-9dad-11d1-80b4-00c04fd430c8"u8.ToArray());

ж<uuid_package.UUID> boxed = Ꮡ(new uuid_package.UUID());
boxed.UnmarshalText("6ba7b810-9dad-11d1-80b4-00c04fd430c8"u8.ToArray());

Ꮡ(value) makes a ж<T> holding a copy of the value, like Go’s &value on a new variable. boxed.Value reads it.

Channels and goroutines

A Go channel is channel<T>, with Send and Receive. goǃ starts a goroutine, from C# as from converted Go:

var results = new channel<int>(1);
goǃ(() => results.Send(42));        // a Go goroutine, started from C#
// results.Receive() == 42

An unbuffered channel (new channel<T>(0)) makes the sender wait for a receiver, as in Go.

Panics

A Go panic that is not recovered reaches C# as a PanicException:

try
{
    uuid_package.MustParse("not-a-uuid");
}
catch (PanicException panic)
{
    // panic.Message describes it
}

Rough edges

For how each Go construct is converted, and why, see Conversion Strategies.