(Disclaimer, formerly worked at Google and used proto/grpc/go there and now in my own startup in github.com/accretional/collector which tries to address this problem with a type registry and fully reflective API. Not privy to the full history, just reasoning.)
Proto is designed so that messages can be deserialized into older/previous proto definitions by clients even if the server is responding with messages of a more recent version. Field numbers Re what let you to start serializing new fields (add a new field with an unused/the next number) or safely stop setting fields in proto responses (reserve a field) without risking older clients misinterpreting the data as belonging to some existing field they know about. This requires you to encode the field numbers alongside the field data in the proto wire format.
Two major problems: nothing in proto itself enforces that field numbers are assigned sequentially, because there is no single source of truth for the proto schema (you can still have one of your own, but it’s not a “thing” in proto). Also, the whole point of field numbers is that they can be selectively missing/reserved/ignored and allow you to deserialize messages without special handling for version changes in your code at runtime.
So, field numbers aren’t a dense, easily enumerable range of numbers, they’re basically tags that can be any number between 1 and 536,870,911 except for the reserved 19,000-19,999. This smells like serious tech debt/ a design flaw that completely closes the door for even fixing this at Google or anywhere else, because it’s arbitrarily in the middle of the range of field numbers and a leaked implementation detail from the internals. You couldn’t build your own dense field number management/sequential enforcement system on top of proto without ripping that part out, but your existing proto usage relies on that part and changing it would break existing clients because you’re removing field numbers, which is the whole fucking point of proto, and makes it difficult to roll out even if you did fix it yourself.
So, representing union/enumerable types in proto is impossible. For proto enums to have forward compatibility, they have to handle adding new enum values over time, or need to remove and reserve old ones. So, proto enums end up being basically just field numbers. That’s exactly what you see in Golang enums and I don’t think it’s a coincidence: Google has no good way to serialize/deserialize/operate on enumerable enums or union types anywhere they use proto/grpc. Golang inherits this “enums” implementation from protobuf because it’s the context in which it was created.