DEV Community

stepinto.dev
stepinto.dev

Posted on Originally published at stepinto.dev

Forty-Eight Posts on Moving From C# to Go, in the Right Order

The C# to Go series is forty-eight posts long. It covers the move from twenty years of .NET to writing Go in production, from the first go build through to a six month retrospective, and it is the most-read thing on this site by a distance.

It was written as a sequence, but most readers do not arrive at the start of it. They arrive from a search for "Go equivalent of IOptions" and land on the configuration post, with no signpost to the twenty posts that explain the vocabulary it uses. The series page lists everything by date, which is correct and unhelpful.

So there is now a landing page at /go-for-csharp-developers/ that orders the whole thing as chapters. This post explains what is on it, who it is for, and what has changed in Go since.

Who it is for

You know C# well. You have shipped ASP.NET Core services, you have opinions about Entity Framework, and you can explain ConfigureAwait(false) to a junior without looking it up. You are either curious about Go or you have just been handed a Go codebase and need to be productive in it by the end of the month.

The series assumes all of that. It does not explain what a slice is from first principles. It explains what a slice is to someone who thinks in List<T>, including the shared backing array behaviour that catches every single one of us. Each post takes a thing you already know in .NET and shows the Go shape of it, where the shape is different, and where your muscle memory will hurt you.

If you do not know C#, the posts still work, but half the jokes will be lost on you.

How to read it

Start with the phrasebook. It maps C# vocabulary to Go vocabulary in one table and takes ten minutes. Bookmark it. Every other post uses the Go words without stopping to translate.

After that, read by chapter. The landing page groups the posts like this:

  1. Getting started. The toolchain, modules instead of csproj, and the phrasebook.
  2. Types. Structs instead of classes, visibility by capitalisation, the enum situation, generics, and nil.
  3. Control flow and errors. The switch statement, if err != nil, and why panic is an emergency exit rather than an exception.
  4. Memory. Pointers, slices, and escape analysis for people who have never thought about the stack.
  5. Concurrency. Goroutines against async/await, channels, select, context, and the sync package for when channels are the wrong tool.
  6. Interfaces and composition. Implicit interfaces, embedding instead of inheritance, and the empty interface.
  7. Testing and project layout. The built-in testing package, mocking without Moq, benchmarks, organising a real project, and multi-module repos.
  8. Building services. net/http, JSON, database/sql, GORM, configuration, and logging.
  9. Tooling. Linting, debugging with Delve, and code generation.
  10. Deployment. Single binaries, build tags, Docker, Kubernetes, health checks, Lambda, observability, GitHub Actions, and versioning.
  11. Looking back. What I miss, what I will never go back for, and the six month retrospective.

If you are in a hurry, chapters one, two, three and five are the ones that change how you think. The rest you can read when the relevant problem turns up.

Three things I would write differently now

The series was written against Go 1.22 and 1.23. Go is now at 1.27 and .NET is at 10, and three of the posts read differently against the current toolchain. The smaller version drift (image pins in Dockerfiles, the golangci-lint config format, b.N loops where b.Loop() now goes) gets a short post of its own at Christmas. These are the three that change the argument.

LINQ has a real answer now

The phrasebook's row for LINQ says "nothing built-in", and the "what I miss" post opens with "God, I miss LINQ" and shows the for loop you write instead. That was the honest position for production Go at the time. Range-over-func iterators had only just landed in 1.23 and the standard library had barely started using them.

It has caught up. The slices and maps packages have grown iterator-returning functions in every release since, strings.Lines and SplitSeq arrived in 1.24, and a lazy pipeline is now a thing you can write without a third-party package. Whether it reads as well as Where().Select().First() is a different question, and that one gets a full post in December with an honest side by side.

Meanwhile C# 14 shipped extension members, so the extension methods gap got wider from the other direction. Fair is fair.

Container-aware GOMAXPROCS is real now, and the Kubernetes post needs a date fixed

The Kubernetes post says Go has respected container CPU limits since 1.19. The 1.19 change was GOMEMLIMIT, which covers memory. CPU awareness arrived in Go 1.25 in August 2025. Before that, a Go process in a container with a two core limit on a sixty-four core node would set GOMAXPROCS to sixty-four and spend its life being throttled, and the fix was Uber's automaxprocs library.

On any toolchain from the last year the post's advice holds as written. If you are running something older in Kubernetes, that is the first thing to check. It is the kind of mistake that costs money quietly.

JSON got a new engine and the tag everyone wanted

The JSON post spends a whole section on the omitempty gotcha, where a zero is treated as empty and silently dropped from the output, and reaches for a pointer to work around it. Go 1.24 added omitzero, which does what everyone assumed omitempty did. The pointer trick still works. It is no longer the right answer.

Then Go 1.27 switched encoding/json over to the v2 implementation underneath. Decoding is a good deal faster and the API you call is the same, but the defaults are stricter: duplicate keys and invalid UTF-8 in strings are now errors rather than things that quietly passed. If you are moving a service that consumes JSON from systems you do not control, test that first after you bump the toolchain. Coming from System.Text.Json, which is also strict by default, you will probably find the new behaviour more familiar than the old.

The whole thing as a PDF

The forty-eight posts come to a little over two hundred pages when laid out as a book, with the chapters above as sections and the phrasebook as an appendix. It is the version I would have wanted on the train in week one.

It goes to newsletter subscribers. The signup form is at the bottom of this page, and the download link arrives in the welcome email. The newsletter is one email a week, on a Tuesday, when a post goes out, and nothing else.

If you have made the same move and want a post that is not here, say so. The Christmas post is the corrections and additions, and the comments on the series have written half of it already.

Top comments (0)