Settings

Theme

Be careful with Go struct embedding

mattjhall.co.uk

134 points by mattjhall 7 months ago · 100 comments

Reader

divan 7 months ago

Some people in comments jump to the conclusion that Go allows conflicting names in embedded structs. It doesn't not – for the embedded structs of the same depth.

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.

  • ActionHank 7 months ago

    I don't really use golang all that much and even I was confused about OP's point because of the different depths.

    I feel like sometimes people just want to complain.

    • divan 7 months ago

      I think author just used to having consistency in Go. If you learned behaviour of one aspect of the language (i.e. compile-time error for conflicting field names), you would expect to have it in all cases. And that's not what's happened in the given example.

  • NooneAtAll3 7 months 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.

    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?

    • zimpenfish 7 months ago

      > 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".

nemo1618 7 months ago

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.

  • oefrha 7 months ago

    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.

    • nine_k 7 months ago

      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.

      • oefrha 7 months ago

        I'm not sure I understand you, or you understand me. I'm saying this is okay:

          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.
        • debugnik 7 months ago

          > 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?

          • oefrha 7 months ago

            The missing something is manually creating the structs, not casting.

            • Thorrez 7 months ago

              >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.

              • jerf 7 months ago

                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.

            • debugnik 7 months ago

              You said struct embedding could be used for discriminated unions, but there's no mechanism to discriminate between union variants here.

              • oefrha 7 months ago

                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.

                • debugnik 7 months ago

                  > 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.

                  • oefrha 7 months ago

                    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.

  • cyphar 7 months ago

    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.)

    • nemo1618 7 months ago

      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

  • h4ck_th3_pl4n3t 7 months ago

    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.

Olreich 7 months ago

If you need to grab a particular struct's version of the data, you can via `opts.BarService.URL` or `opts.FooService.URL`: https://go.dev/play/p/MUSYJhmoC2D

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".

tymscar 7 months ago

So I got curious and I looked at the compiler source code, and it does a depth-first search.

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.

  • dgl 7 months ago

    > 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.

  • divan 7 months ago

    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).

  • ankraft 7 months ago

    It performs a breadth-first search, not a depth-first search.

joahnn_s 7 months ago

Who's actually expecting `xyz.com` here?

Spec: 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).

  • Someone 7 months ago

    That something is clearly specced doesn’t imply all developers actively know it.

    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.

digianarchist 7 months ago

I'm surprised this wasn't in the recent post submitted here: https://blog.habets.se/2025/07/Go-is-still-not-good.html

It's a one of a few rough edges in Go.

  • eru 7 months ago

    You could add it as a comment there, or on the HN discussion?

HelloNurse 7 months ago

This is a "you had one job" level language defect. If there are excuses that allow Go to pretend that structs with duplicate member names are well formed, good programming language designers would reject them in horror and specify how the compiler should actively look for this type of mistake, without wasting effort to look for such excuses.

Fire-Dragon-DoL 7 months ago

Maybe I see it differently, but it made sense: embeding works only at 0 depth, it's like a macro to access rapidly the fields of the embedded struct, it doesn't go beyond that,there is no inheritance.

When embedding BarService, the field being embedded is BarConnectionOptions

b_e_n_t_o_n 7 months ago

Struct embedding is sugar for

   type Foo struct {
      MyType MyType
   }

   myFoo.MyType.url
So it would resolve myFoo.MyType1.url over myFoo.MyType2.NestedType.url
bilbo-b-baggins 7 months ago

Huh my IDE linter spits out warnings about this. Not sure which extension does it.

desumeku 7 months ago

Why would you ever do opts.URL instead of opts.FooService.URL or opts.BarService.URL? What does ambiguity and imprecision gain you here when you can just write out from which struct you want it from? I don't even know why opts.URL would compile, it's completely unstated that you're not grabbing it from opts but grabbing it from some other structure contained within opts. Shouldn't even compile IMO, but at least I found something I disagree with Go's designers on.

alpb 7 months ago

It's my common code review comment to the beginners to not embed structs. There's rarely anything to be gained by doing so. The only use case I found to be useful is to embed something like:

    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.
fishywang 7 months ago

I don't understand how this got so many upvotes. Embedded struct fields are never "promoted", you always need to access them via the embedded type's name, so there's nothing to conflict.

The only thing "promoted" are the functions associated with the embedded types, and when those actually conflicts, the compiler will tell you, as expected.

  • jlokier 7 months ago

    > you always need to access them via the embedded type's name, so there's nothing to conflict.

    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.

lowmagnet 7 months ago

I have code that wouldn't work without embedding. Basically, I have a type that annotates any AWS paginate and is capable of streaming all the results as a sequence. It embeds the original client so you get all the functionality of the client, but it also wraps the functions that support a pagination. I can't think of an easier or clearer way to do it.

flowerthoughts 7 months ago

Making the compiler fail that would indeed be nice. A clear example of a language feature that makes sense when there's a single developer, but in a code base with hundreds of developers, this could easily break something without anyone noticing.

noisy_boy 7 months ago

Am I the only one who found the described behavior to be intuitively correct? I did expect it to print "abc.com".

  • nine_k 7 months ago

    This may be intuitively correct, but to my mind it is architecturally wrong. A good language should not tolerate ambiguity and offer to guess which behavior is correct.

    • wild_egg 7 months ago

      It's not ambiguous though. The behaviour is very clearly defined in the language spec.

      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.

    • tayo42 7 months ago

      Why is it ambiguous though? The second URL is nested

      Are thy not accessed like

      opts.URL == abc.com

      and

      opts.BarConnectionOptions.URL == xyz.com

      what leads you think otherwise?

      • tsimionescu 7 months ago

        If there were no duplicate URL field, say they were called FooURL and BarcoURL, you could access them as `opts.FooURL` and `opts.BarcoURL`. They are both fields of `opts`, via embedding. It's just that FooURL is embedded directly, while BarcoURL is coming from two levels of embedding.

  • nvlled 7 months ago

    I'm confused at the responses saying this intuitive. It's like saying:

      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.

    • graynk 7 months ago

      It's not at all like saying that.

      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"},
        }
      • nvlled 7 months ago

        I'm making an analogy using simple assignments to show more clearly that it's actually counter-intuitive, I'm not claiming what you seem to be refuting, which at best seems to be beside the point.

        • graynk 7 months ago

          And the analogy is invalid, because it completely replaces the intuitive thing that's happening with an unintuitive thing that isn't happening. There are no "latter operations or (re)definitions" here.

          • nvlled 7 months ago

            > There are no "latter operations or (re)definitions" here.

            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?

            • graynk 7 months ago

              > Yes, in the literal narrow sense, there is no such thing in the submitted article

              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)
              • nvlled 7 months ago

                > The unintuitive part is that this works in the first place and doesn't throw an error

                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.

                • graynk 7 months ago

                  > you do agree that it's unintuitive

                  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.

  • _2d30 7 months ago

    I don’t write Go at all but given the first example, also expected this.

    I was very surprised that either example compiled, though.

  • b_e_n_t_o_n 7 months ago

    Yeah it makes sense to me as well.

  • adastra22 7 months ago

    What is the intuition for that?

    • binary132 7 months ago

      I think it’s something like:

      “The general rule is that I may access the direct embeds of my type anonymously.”

      • adastra22 7 months ago

        Ok I don’t think anyone disagrees with that. But there are two embedded structs, both with a URL field.

        • noisy_boy 7 months ago

          But the level of nesting isn't the same; only one of them has a direct URL field.

          • masfuerte 7 months ago

            You would get the same result if both URL fields were nested one level deeper. Being directly nested isn't the point.

okibry 7 months ago

The same issue with embbedding of field had reflect tag like json or bson ...

  • okibry 7 months ago

    After that time, I did not use json or bson Unmarshal to cast anymore. Any transform/converter function of me is manual directly. And thanks for LLM, with the their help, I only need to review the function, so no more typing.

preommr 7 months ago

My personal conspiracy is that Golang is an epic prank.

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.

  • b_e_n_t_o_n 7 months ago

    Is it possible that it's like every other language, with flaws and tradeoffs that don't always make sense to everyone? Why make it more complicated than that?

    • hakunin 7 months ago

      This might sound bad, but having worked in a few different languages, I find it kind of cute how some parts of Go seem to promote simplicity while others are simply what the authors were used to growing up. Sometimes it frustrates me when I write Go, when I see a tradeoff due to the latter. But yeah. Languages tend to have a lot of subjectivity of their author in them. The more "strict" the language is, the more confined you are to the author's opinions.

  • jerf 7 months ago

    I've been programming in Go for ten years.

    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?

  • nine_k 7 months ago

    > just horrible in other ways for no reason whatsoever

    I bet the reasons were very mundane: initial project scope, deadlines, performance review cycle. "This simplest thing that could possibly work", etc.

  • shadowgovt 7 months ago

    As with most small-team languages, it was built mostly to solve the problems that its initial author had in front of them.

tymscar 7 months ago

That’s actually crazy. Why is this even a feature?

  • amiga386 7 months ago

    Because it's useful.

    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.

  • dgl 7 months ago

    See how it's used in the standard library io types, it makes for quite nice composition: https://go.googlesource.com/go/+/refs/heads/master/src/io/io...

    • whatevertrevor 7 months ago

      Unioning interfaces like this does seem convenient for composition/mixin patterns, I'm not sure if extending it to structs in general seems worth the cost of potential footguns though, especially external libraries and such where you probably don't want to think about the full potential tree of embedding conflicts.

    • mananaysiempre 7 months ago

      I’m sympathetic to parts of the Go design philosophy, but the only thing that comes to mind looking at this is “damn, that’s some awkward (nominal-looking) syntax for (structural) intersection types”.

      (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 7 months ago

        You can’t have ambiguous methods so the problem illustrated here fails at compile time for interfaces.

  • mananaysiempre 7 months ago

    At the very least, the Go authors have been convinced this should be a feature since the Plan 9 C dialect[1].

    [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.

  • jrockway 7 months ago

    Because

       type Foo struct { 
           sync.Mutex
           whatever string
       }
       var foo Foo
       foo.Lock()
       foo.whatever = 42
       foo.Unlock()
    
    is convenient.
    • resonious 7 months ago

      I thought Go was all about being "simple" at the cost of convenience.

      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).

    • sethammons 7 months ago

      almost always, the recommendation is to not embed your mutex; give it a name.

      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 7 months ago

        Alas, locks don't compose, ie often your users will have to know about the internals when you are using locks.

        But it's good advice when it works.

    • thrill 7 months ago

      Hmm, never realized the convenience came this way. Seems the compiler could emit a warning if two equal depth names might cause confusion, which could be ignored if acceptable.

    • echelon 7 months ago

      It's dangerous. This is awful.

      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 7 months ago

        I mostly agree.

        > 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!)

  • mikepurvis 7 months ago

    At risk of being excessively sassy this looks like a case of wanting the ergonomics of multiple inheritance without fully grappling with the complexities or implications of it.

    • gdbsjjdn 7 months ago

      In most cases people just want any inheritance, this is the backwards way the Golang devs decided to implement it based on their 80s view of programming languages.

  • typ 7 months ago

    It seems to come from a Plan 9 C idiom that GCC even has an extension for it.

    https://gcc.gnu.org/onlinedocs/gcc-5.3.0/gcc/Unnamed-Fields....

  • bilbo-b-baggins 7 months ago

    Normally you wouldn’t contrive to use embedded struct fields in this way. And you can’t have the same kind of composition with methods - it’s a compiler error:

    https://go.dev/play/p/r04tPta1xZo

    So the whole article is basically about using the language in a way you normally would ever do.

  • metadat 7 months ago

    IMHO it should be a compiler error. This is just so loose... a wheel fell off.

  • divan 7 months ago

    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 have 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.

    Example in the OP article is a corner case where this behavior is creating ambiguity, indeed. Yet, it's documented and intentional.

MH15 7 months ago

Hello from another Matt Hall! Interesting post, although I don't do much Golang.

linhan_dot_dev 7 months ago

I like the Go language because it's straightforward and clear, even if it looks a bit plain.

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.

  • eru 7 months ago

    > I hope the feature mentioned in the article will cause a compiler error.

    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.

    • linhan_dot_dev 7 months ago

      I know, so this is a wish. Looking at other comment sections, there are actually linters that can warn about such behavior.

  • linhan_dot_dev 7 months ago

    I tested the tool more thoroughly using a strict method to review the code, but it couldn't find the problems mentioned in the article.

    golangci-lint run --enable-all --max-issues-per-linter=0 --max-same-issues=0

  • linhan_dot_dev 7 months ago

    This comment unexpectedly received a few downvotes, which might be due to some misunderstanding. My point is as follows: 1. People choose Go for various reasons. 2. Beginners will encounter the issues mentioned in the article, so I hope there could be a feature that provides hints for newcomers. While the compilation errors might not be reasonable, at least the LSP/linter could provide some guidance.

    • tsimionescu 7 months ago

      This may be a language issue, but saying "I hope this causes a compilation failure" implies that you don't know if it does - this is probably why you got down voted, since the article very explicitly says that this doesn't cause compilation failure. You may have meant that you hoped it wouldn't do this - in that case you should have used a construction like "I'd hoped this would have caused a compilation failure".

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection