Over the course of ~10 years of writing Go, my ratio of "embedding a struct" to "regretting embedding a struct" is nearly 1:1.
I do not embed structs anymore. It is almost always a mistake. I would confidently place it in the "you should be required to import 'unsafe' to use this feature" bin.
Using struct embedding for pure data to implement discriminated unions is fine, better than MarshalJSON() that is lost on a type definition. Using it to save typing, or going crazy with it (I consider embedding two things going crazy) is bad.
I think that using embedding for discriminating unions if a good idea. It would work, but it does not force the user to do the discrimination. I would say that explicit typecasting at the point of discrimination is safer. Without it, nothing prevents you from using one field from one variant of the union, and another from a different variant.
Introduction of proper discriminated unions would be great.
I'm not sure I understand you, or you understand me. I'm saying this is okay:
And yes you should convert to OrderTypeA or OrderTypeB at the first opportunity in domain code, and only convert from them at the latest opportunity.You seem to be under the impression that I'm advocating for something like
That's what I consider going crazy.> And yes you should convert to OrderTypeA or OrderTypeB at the first opportunity in domain code,
Go can only downcast through interfaces so there's something missing to your approach to unions, isn't there?
The missing something is manually creating the structs, not casting.
>And yes you should convert to OrderTypeA or OrderTypeB at the first opportunity
How would you convert Order to OrderTypeA? You would need some other source to fill TypeAAttr1 and TypeAAttr2 with.
Needing further external information to "convert" from one struct type to another is fairly common and completely normal. One I happen to have encountered in multiple places over the years is normalizing user names. Sometimes there is no mechanical process to normalize user names, such as simply lowercasing them; you may need access to an LDAP server to get a canonical name/account, or access to information about local email munging rules (like "which character do you use to allow users to specify multiple addresses for themselves, like gmail uses '+'?" - not all systems use +), or you may need DB access to verify the user name exists if you want a value of the given type to represent a user that is not only normalized but guaranteed to exist.
You said struct embedding could be used for discriminated unions, but there's no mechanism to discriminate between union variants here.
Go simply doesn't have discriminated unions, so a number of pattern can all be called "discriminated unions" in Go. I was simply emphasizing that sharing common fields between pure data structs with struct embedding (commonly seen in but not limited to discriminated unions) but now people are weirdly hung up on discriminated unions. I just showed a data model with a discriminator (.Type), there are a number of mechanisms to discriminate depending on your actual needs. You can make the types conform to an interface, pass an interface value around and cast to specific types. You can get a fat row with a bunch of left joins from your database then immediate create type-specific structs and call type-specific methods with them. You can get a discriminated union on the wire, unmarshal the type field first, then choose the type-specific struct to unmarshal to. Etc. These are largely irrelevant in a discussion about type embedding.
> so a number of pattern can all be called "discriminated unions"
Assuming they've got discriminators and some sense of type union, sure.
> I just showed a data model with a discriminator (.Type)
Which won't let you recover the additional fields from a pointer because you can't downcast, so that's insufficient for a union. AFAIK you need to combine this with interfaces, which I already know how to do.
> These are largely irrelevant in a discussion about type embedding.
Don't tell me, you brought it up.
It’s almost like I brought it up in passing because it’s a somewhat relevant concrete use case, rather than brought it up to have people who “already know how to do” to chastise me for not writing a full treatise on the use case.
I think there are a handful of cases where it is a nice-to-have and would be sad if it was removed in a hypothetical Go 2. Making a utility wrapper struct that overrides a method or adds new helper methods while keeping the rest of the interface is the most common example, though there are also some JSON spec type examples which are a little more esoteric. However, you need to be mentally prepared to switch to the boilerplate version as soon as things start getting hairy.
But yes, for anything more complicated I have generally regretted trying to embed structs. I think requiring "unsafe" is a bit too strong, but I think the syntax should've been uglier / more in-your-face to discourage its use.
(Fellow 10+ years Go user.)
yup, less than 24 hours after writing that comment, I found myself embedding a struct so that I could override one method in a test, haha
I think embedding structs would be way more useful if a) there would be properties on interfaces and b) there would be generic methods available.
As long as these two aren't there, embedding structs is literally identical to dispatching methods, and can't be used for anything else due to lack of state management through it. You have to manage the states externally anyways from a memory ownership perspective.