Still worth being careful, but it can be useful when you have a set of common fields that everything of a certain group will have (such as a response object with basic status, debug info, etc. and then additional data based on the particular struct). I don't know why they let you embed multiple layers and multiple objects though. I've never gotten value out of anything but a "here's a single set of common fields struct embedding".
The fascinating bit to me is that there is a consolidateMultiples function in go/src/go/types/lookup.go (lines 286-304) that detects when multiple embedded types at the same depth provide the same field name. I wonder why they don’t do this for all levels. How deep could this even be in practice for it to matter? You could just have a hashmap with them all.
While it may seem questionable for fields; it applies to methods too and is potentially more useful as a way to override them when doing struct embedding but wanting to preserve an interface.
I hope the feature mentioned in the article will cause a compiler error.
However, I wouldn't use this approach when writing my own code.
Read the article. It won't.
At best you can perhaps find a linter that'll report it?
> However, I wouldn't use this approach when writing my own code.
You might use it by accident.
golangci-lint run --enable-all --max-issues-per-linter=0 --max-same-issues=0
It's a one of a few rough edges in Go.
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.
Introduction of proper discriminated unions would be great.
type Order struct {
Type OrderType
CommonAttr1 int
CommonAttr2 string
}
type OrderTypeA struct {
Order
TypeAAttr1 int
TypeAAttr2 string
}
type OrderTypeB struct {
Order
TypeBAttr1 int
TypeBAttr2 string
}
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
type OrderUnion struct {
CommonAttr1 int
CommonAttr2 string
TypeAAttrs
TypeBAttrs
}
That's what I consider going crazy.Go can only downcast through interfaces so there's something missing to your approach to unions, isn't there?
How would you convert Order to OrderTypeA? You would need some other source to fill TypeAAttr1 and TypeAAttr2 with.
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.
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.)
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.
I was very surprised that either example compiled, though.
“The general rule is that I may access the direct embeds of my type anonymously.”
Are thy not accessed like
opts.URL == abc.com
and
opts.BarConnectionOptions.URL == xyz.com
what leads you think otherwise?
https://go.dev/ref/spec#Selectors
As far as language specs go, Go's is really quite concise and I strongly encourage everyone I onboard to spend an afternoon reading it end to end at some point in their first couple weeks.
var x string
x = "abc.com"
x = "xyz.com"
fmt.Println(x)
will print abc.com and that's totally expected.The normal intuition would be that the latter operations or (re)definitions override the preceding ones.
The order of operations has nothing to do with it.
opts := Options{
FooService: FooService{URL: "abc.com"},
BarService: BarService{
BarConnectionOptions: BarConnectionOptions{
URL: "xyz.com",
},
},
}
is equivalent to opts := Options{
BarService: BarService{
BarConnectionOptions: BarConnectionOptions{
URL: "xyz.com",
},
},
FooService: FooService{URL: "abc.com"},
}Yes, in the literal narrow sense, there is no such thing in the submitted article (if it isn't already clear, I'm referring to my own example). That's why it's an analogy. I don't know the precise term that go uses for this, closest is probably "shadowing", but again it doesn't matter, it is besides the point. The point is that the exhibited behaviour is unintuitive, in contrast to what the others are saying.
> it completely replaces the intuitive thing that's happening with an unintuitive thing that isn't happening
What is the intuitive thing are you referring to here? If it's my example, then you are in total agreement with me, but you seem to think otherwise. If you are referring to the linked article, then you are just merely invoking tautology, to disagree with me. It's intuitive because you said so, therefore my analogy is invalid. Did I get that right?
Therefore your analogy is invalid, because your example is doing something entirely different and throws away nested structs that the whole thing is about.
> The point is that the exhibited behaviour is unintuitive, in contrast to what the others are saying.
Why?
> Did I get that right?
No. Let's stick to the original example and add the order of operations from your example.
type A struct {
X string
}
type Nested struct {
X string
}
type B struct {
Nested
}
type Combined struct {
A
B
}
c := Combined{}
c.X = "example.com"
c.Nested.X = "something completely different"
fmt.Println(c.X)
Do you still expect this to print "something completely different" or does this look intuitive now?The unintuitive part is that this works in the first place and doesn't throw an error:
type Combined struct {
//A
B
}
c := Combined{}
//c.X = "example.com"
c.Nested.X = "something completely different"
fmt.Println(c.X)
But if you know about this unintuitive feature and are relying on it instead of accessing the fields by their fully qualified names, then you should already have a gnawing feeling that asks you "what happens when there are conflicts?" (and the answer is - it does the intuitive thing)What the hell? So you do agree that it's unintuitive but the supporting points you keep giving are completely, utterly tangential. That's what I have been saying all this time, that it's unintuitive, what you are even disagreeing with me for? The analogy?
I repeat this once again, I made the analogy to simplify and make it clear because some responses seems to miss it. I've already addressed your points, but you keep giving back the same supposed rebuttal, different words but same meaning. Nothing about what you say invalidates the analogy.
> But if you know about this unintuitive feature and are relying on it instead of accessing the fields by their fully qualified names, then you should already have a gnawing feeling that asks you "what happens when there are conflicts?" (and the answer is - it does the intuitive thing)
If you are deeply aware of the quirks, intuition no longer applies. You rely on intuition when you are in an unfamiliar situation. So again, nothing what you said just now supports any of your argument, whatever it is.
You should really pay more attention to precision of wording and the meaning that it carries, because you keep mixing up different things into one big pile of hand-waving and equating things that are not equal :)
There is no single "it" here. There are two different behaviors here. One is intuitive. One is not. We were discussing the former one. The analogy was about the former one. And the analogy was imprecise and misleading, completely losing different levels of nesting which is the whole point - that's what makes this behavior (short-hand selection when names conflicts - NOT short-hand selection in general) intuitive.
> If you are deeply aware of the quirks, intuition no longer applies. You rely on intuition when you are in an unfamiliar situation
I agree! And what I'm saying is: if you rely on intuition and you don't know about the selector mechanism at all - you will explicitly write out this nested level to access the second-level nested variable (c.Nested.X) and side-step the issue altogether, and accessing the first-level nested variable (c.X) will get you first-level nested variable exactly as you would expect. That's the behavior. That's what we're discussing here. That's what the article is about. That's what everyone calls intuitive - it's not about the short-hand selection itself (which, as I said - is unintuitive, but will not shoot you in the foot, unless you really try).
I don't think I'm getting my point across though, so I suggest we wrap it up here.
Make a language that's really good in some ways and just horrible in other ways for no reason whatsoever.
So that when it's critics point out contradictory features like embedding, it's defenders can be the ultimate troll and say things like "but, actually, it's a simple language because it doesn't have while loops".
It's the best explanation I have for some of the cognitive dissonance surrounding the language design.
I bet the reasons were very mundane: initial project scope, deadlines, performance review cycle. "This simplest thing that could possibly work", etc.
This problem has happened to me once.
Would you care to make a list of all the problems your favorite language has served up to you at a rate of once in ten years, so I can also write a post making your language sound horrible as a result?
type MockHandlers struct {
UnimplementedHandlers
}
func (m MockServer) LoginHandler{ /* ... */ }
where I get to override only a part of a bigger interface at a time and have the embedding take care of the rest by saying panic("unimplemented") to satisfy the interface.When embedding BarService, the field being embedded is BarConnectionOptions
type Foo struct {
MyType MyType
}
myFoo.MyType.url
So it would resolve myFoo.MyType1.url over myFoo.MyType2.NestedType.urlSpec: https://go.dev/ref/spec#Selectors > x.f resolves to the field/method at the shallowest depth in T. If there isn’t exactly one at that depth, it’s illegal.
Embedding promotes fields; on name collisions the shallowest wins. So `opts.URL` is `FooService.URL` (depth 1), not `BarConnectionOptions.URL` (depth 2).
Even given that it compiles, I wouldn’t exclude it being a runtime error.
But the big problem isn’t that it behaves as advertised, it’s that it is way too easy to write opts.URL where you mean opts.Bar.URL. Auto-complete will happily compete the wrong thing for you.
These won't compile:
type Bad struct {
Name string
Name string
}
type A struct{ Name string }
type B struct{ Name string }
type C struct {
A
B
}
bad.Name() // compile error: other declaration of Name
c.Name() // compile error: ambiguous selector c.Name
The case in article is about field names of the different depth. Spec is very clear about this behavior, and it was intentional.One of the reasons why handling same field names is different at different nesting levels is to protect against changes in structs coming from external libraries. Or, better phrased, external structs should not dictate what names you're allowed to use in your own structs (so they have priority).
I.e. when you create a struct with another embedded struct (possibly from other package):
type Foo struct {
somepackage.Bar
URL string
}
you don't want to depend on whether Bar already has URL. Your depth level has higher priority.Even more, imagine in the future authors of `somepackage` decided to add URL to their struct and it suddendly started to break your code from being compiled.
I agree that behavior in the OP article example is confusing (and so is the code - do you want URL of Foo service or Bar service?). Yet, this behavior is intentional and documented.
As usual, it's a subtle tradeoff here. If this feature would be implemented differently (say, compile time error for all depth levels), we would see an article with rant on how external structure changes breaks compilation.
I feel like sometimes people just want to complain.
doesn't this just shove the problem down a level?
e.g. if somepackage.Bar suddenly gets a member with same name as one of your URL members?
I think nothing happens there. Your fields "win" on depth and you'd have to access their field with `thing.Bar.Conflicted` (whereas yours would be `thing.Conflicted`).
It could only be a problem if someone embeds both `somepackage.Bar` and `mypackage.Cheese` into `T` with a shared field `X` but then you can't access `T.X` without a runtime error of "ambiguous selector".
The only thing "promoted" are the functions associated with the embedded types, and when those actually conflicts, the compiler will tell you, as expected.
The article talks about "opts.URL" in its example being accepted by the compiler, which accesses "opts.FooService.URL" without using the embedded type's name.
tymscar•4mo ago
jrockway•4mo ago
echelon•4mo ago
Any coding construct that can cause defects is an antipattern. Your language should discourage defects by design. Especially if the faults crop up at runtime.
This struct field dereferencing is like NULLs and "goto".
Language design that is anti-defect yet ergonomic include the modern Option<T> and Result<T, E> as seen in languages such as Swift and Rust, with first class destructuring that doesn't make it painful to use. They're almost impossible to misuse, yet feel convenient instead of frictionful. Rust's sum types and matching are another set of examples. Hopefully these patterns spread to more languages, because they're safe and convenient.
eru•4mo ago
> Language design that is anti-defect yet ergonomic include the modern Option<T> and Result<T, E> as seen in languages such as Swift and Rust, with first class destructuring that doesn't make it painful to use.
Funny enough, this is only 'modern' in imperative languages. It's been a staple in the ML family since approximately forever. (But hey, I do appreciate progress when we get it!)
thrill•4mo ago
sethammons•4mo ago
foo.mu.Lock()
This way you don't expose your primitives, preventing poor usage from causing a deadlock. Generally you don't want the user of your struct to have to know when or when to not lock.
eru•4mo ago
But it's good advice when it works.
resonious•4mo ago
It is a bit ironic that this language that was was designed around "all of these features of other languages cause trouble, we will omit them" also has a bunch of features that cause trouble and get avoided.
Just to make my own stance clear: I like language features. I think this struct embedding feature looks pretty cool. But I also like interfaces and polymorphism. I think it's OK for a programming language to be powerful, and to put the onus on developers to not go too crazy with that power. And for that reason, I've always gravitated away from Go, and always jump on an opportunity to make fun of it (as I have here).
mikepurvis•4mo ago
gdbsjjdn•4mo ago
metadat•4mo ago
ShroudedNight•4mo ago
catlifeonmars•4mo ago
ShroudedNight•4mo ago
https://en.wikipedia.org/wiki/Unreachable_code#goto_fail_bug
dgl•4mo ago
mananaysiempre•4mo ago
(It also feels to me that this sort of anonymous embedding is materially different for interfaces vs structs, though I admit that from a type-theoretic perspective it’s not.)
bilbo-b-baggins•4mo ago
whatevertrevor•4mo ago
mananaysiempre•4mo ago
[1] http://doc.cat-v.org/plan_9/4th_edition/papers/comp, look for “anonymous structure or union” and note that a (different) part of that extension has since been standardized.
bilbo-b-baggins•4mo ago
https://go.dev/play/p/r04tPta1xZo
So the whole article is basically about using the language in a way you normally would ever do.
whatevertrevor•4mo ago
https://go.dev/play/p/D3eFi9_can8
Conflicting functions at nested levels also compile:
https://go.dev/play/p/xXXDZCjQJOh
It's not about method vs field, it's about the nesting level of the conflicting identifier, if it's at the same level there's an error, if it's at different levels, the higher level hides the lower level identifier:
https://go.dev/doc/effective_go#embedding
amiga386•4mo ago
https://go.dev/doc/effective_go#embedding
> Embedding types introduces the problem of name conflicts but the rules to resolve them are simple. First, a field or method X hides any other item X in a more deeply nested part of the type. If log.Logger contained a field or method called Command, the Command field of Job would dominate it.
> Second, if the same name appears at the same nesting level, it is usually an error; it would be erroneous to embed log.Logger if the Job struct contained another field or method called Logger. However, if the duplicate name is never mentioned in the program outside the type definition, it is OK. This qualification provides some protection against changes made to types embedded from outside; there is no problem if a field is added that conflicts with another field in another subtype if neither field is ever used.
typ•4mo ago
https://gcc.gnu.org/onlinedocs/gcc-5.3.0/gcc/Unnamed-Fields....
divan•4mo ago
Even more, imagine in the future authors of `somepackage` decided to add URL to their struct and it suddendly started to break your code from being compiled.
Example in the OP article is a corner case where this behavior is creating ambiguity, indeed. Yet, it's documented and intentional.