I could have forgiven nil checks, but nil checks on interfaces elevated nils to a whole new level, which is annoying, but I do get where they were going with this: you should never nil check an interface. After all,an interface could be valid for a nil value.

There are ways to decently write go and not deal with nil, but as usual, linters defaults makes it impossible and you have to fight with your team before they will understand (we did this at some point and it was a huge improvement).

Don't use pointers at all, always allocate structs on the stack, pass them by value.

You pay the copy price, even with large structs, and that's fine. When there are exceptions, be very explicit about the reason: performance must be critical,not just an optimization.

Don't ever check interfaces for nil, if you need some sort of optional parameter, make a separate function and make it pass an valid object for that interface that's a null object.

These two did improve things substantially

This suggestion fails for values that can be null, need to be mutable or need references from multiple places etc – it's not "just performance penalty".

Go has a problem, "just remember to always do X, never Y" patterns can't be guaranteed across all libraries you use, can't be enforced, can be violated for good reasons, other patterns and as a mistake etc etc.

Shame because otherwise it's a great language, but some mistakes are just no-go.

So close indeed.

They need Go 2 with *T and ?*T - that would be nice language to use.

The best approach is to use other programming languages with more open minded approach to modern type systems, and leave Go to the use cases where there is no alternative due to existing adoption.

Go 2 will never happen, they will keep incrementing 1.x until end of current computing model.

> They need Go 2 with T and ?T - that would be nice language to use.

Zig has basically been this to me. As a developer, writing Zig feels a lot like writing Go, except with basically all of the pain points addressed.

- Zig has different pointer types for different things. The default pointer type points to exactly one value. It also has an optional pointer type as described. Arrays and slices are also pointers of sorts, and a graph in this post[0] does a good job describing the relationships between N element pointer types.

- Zig has a built-in error type that is able to carry stack trace information.

- Zig has a syntactic shorthand for the common `if err != nil { return err}` pattern: the `try` keyword.

- Zig doesn't impose a garbage collected runtime.

- defer can handle arbitrary expressions in Zig. They do not need to be wrapped in a closure or a function call.

- The frankly weird interface system in Go is replaced with one of the more sane metaprogramming systems (I would argue that comptime is the most sane metaprogramming system in any C-like language).

Other than that, Zig and Go are very similar. They both use repos for modules. They have roughly the same concurrency semantics. They have the same allocate+defer deallocate pattern (though more flexible in Zig, also due to scope bound vs function bound). They both treat errors as values. They both disallow things like operator overloading. They both have built in testing systems.

Zig makes breaking changes in ways that Go doesn't anymore, but the breaking changes are always very thoughtful, and are clearly converging on something that is more flexible than Go (both in expressiveness, and in portability) but with a very similar mental model for developers.

[0] https://ziggit.dev/t/array-and-slice-address-relationship/14...

Honest question, what is the zig version of goroutines these days? They removed async from the spec.

Honestly the only reason why I stick to golang for some projects are goroutines.

Zig 0.16.0 was the big, recent async update. Go channels are handled as std.Io.Queue, the select keyword is handled with std.Io.Select, and goroutines are handled with std.Io.async and std.Io.concurrent. The difference between async and concurrent calls are essentially: async calls may be called concurrently, or they may not (the execution of the async function is not dependent on the caller continuing to execute - like reading a set of files), and concurrent must execute concurrently (the execution of the concurrent function depends on the caller continuing to execute - like a client+server interaction, and will crash if the environment cannot spawn concurrent processes).

The fully userspace implementation is a bit more syntactically clunky than the concurrency primitives in Go, but it is very similar semantically.

I really like Zig, but the lack of easy to use interfaces is kind of a dealbreaker for me. One issue but I can't look past it when building anything of substantial size.

I've written a couple of fairly large projects in Zig, and never really felt like I needed anything more than what comptime provides. What kind of pattern do you think Zig would benefit from supporting?

You are approaching the issue but from the other direction :) It is indeed subtle. It could be solvable though if someone wanted to change the current behavior, in a forward compatible way.

Hint: "Don't use pointers at all" is the requirement that you would have to relax. That means that you cannot know whether a pointer is safe or not to be used from within an interface.

Conservatively, that means that every nil pointer in interfaces are unsafe.

Which means we should either have a way to check for nil pointers (typed or untyped) in interfaces or assert that an interface value cannot contain a nil pointer. (requires definite assignment analysis)

Actually have implemented the nil checking migration part as a POC but seems that it requires the assertion part to be tractable... That is a bit more work.

That would be interesting.

> Don't use pointers at all, always allocate structs on the stack

Unless one makes the rookie mistake of passing these structs to pkg log (which box to any/interface{}) instead of slog [0]... then they escape to heap. If a project relies on avoiding heap allocs, prudent to 'go build -gcflags="-m"' on every check-in, and review the diff from that too.

[0] https://go.dev/blog/slog

Good to know, but wouldn't that be something that pops up when profiling?