Goroutines and channels make it very straightforward to set up an ECS engine hence you can actually run a lot of the engine in parallel and not have to stick to the "game loop" structure. Build your systems similar to the actor model with a mailbox, have an event/messaging bus, and you basically have a very performant engine.
I've yet to run into issues with the GC. The only bit of tuning I had to do was be careful with interfaces in hot paths and batch all graphics calls.
I am a huge Go fan and I figured this out working on the raytracer in a weekend project using Go.
It took me a week or so.
Now that I know what is going on I could do it in a couple days with additional libraries. I didnt use the popular Go vector library.
For any game that doesn't require smooth frames for the experience, the best language is the one you're most comfortable with (modulo some base level of game dev community)
Edit: on the other hand, Go is a superb candidate for a back-end game server. I'd say the same goes for the network client layer, but the language interop story is not ideal.
Check their developer channel on YouTube.
The garbage collector has not been an issue, because I use memory the same way as if I were doing it in the C language: Allocate all memory used by the game at program launch. I find this scheme easier to work with in Go than in C, because Go's slice type is a natural fit for partitioning these launch-allocated blocks of memory (as they get used and reused during the game).
The garbage collector doesn't get called if you don't allocate. And for the most part¹, it's easy to reason about where Go allocates (if you know C). So my rule is: Always know where your memory is; never allocate.
This may sound like it's going against the grain of Go, but I find Go to be handily amenable to this style. I had my concerns going in, but it just hasn't been a problem. My game runs silky smooth.
At this point, my only concern about using Go to make a game is that bringing the game to consoles does not have a well-trodden path.
¹ So far, the only thing that has surprised me is that assigning a value type (like an int) to an interface allocates! (So another rule is to only assign pointers to interfaces [pointers to memory allocated at launch].) And while this was a surprise to me, I was pleased with how quick the debugging went: I noticed frames were dropping, so I ran go's pprof tool, which led me directly to the (freshly coded) interface assignment that was stealth allocating. (Also, this allocation becomes less surprising the more I think about how interfaces are implemented.)
One neat thing that Go has is built-in benchmarking (part of the built-in test package) which shows you allocations per-invocation making it easy to spot the functions which need more work.
I’ve heard this a lot and even parroted it myself, but in all honesty I would be surprised if this is true today. First, GC does not mean “no control at all” over GC pauses. Secondly, GC pauses are incredibly short in some languages, unless you have to deallocate a lot.
Without first-hand experience, I would assume that GC is not an issue for games today, given that you are aware of how allocations work, and you have access to a good profiler if necessary.
As usual, the best language is most likely the one with the best ecosystem (libs, tooling, docs) around the domain that you’re working in.
Also, you might be able to toggle the GC off, and invoke manually with runtime.GC() at opportune moments.