Be careful with Go struct embedding

(mattjhall.co.uk)

79 points | by mattjhall 4 hours ago ago

53 comments

  • nemo1618 2 hours 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 an hour 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 26 minutes 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 15 minutes 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.
  • joahnn_s 14 minutes 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).

  • tymscar 3 hours 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 3 hours 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.

  • Olreich 3 hours 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".

  • digianarchist 3 hours 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 an hour ago

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

  • Fire-Dragon-DoL 42 minutes 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 19 minutes 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
  • alpb 44 minutes 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.
  • bilbo-b-baggins 3 hours ago

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

  • noisy_boy 2 hours 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 19 minutes 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.

      • tayo42 8 minutes 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?

    • creddit an hour 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 26 minutes ago

      Yeah it makes sense to me as well.

    • adastra22 an hour ago

      What is the intuition for that?

      • binary132 17 minutes ago

        I think it’s something like:

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

        • adastra22 15 minutes ago

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

  • preommr an hour 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.

    • nine_k 14 minutes 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.

    • b_e_n_t_o_n 25 minutes 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 minutes 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.

    • shadowgovt an hour 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 4 hours ago

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

    • amiga386 2 hours 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.

    • typ 2 hours 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....

    • jrockway 3 hours ago

      Because

         type Foo struct { 
             sync.Mutex
             whatever string
         }
         var foo Foo
         foo.Lock()
         foo.whatever = 42
         foo.Unlock()
      
      is convenient.
      • resonious 2 hours 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 3 hours 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 an hour 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 3 hours 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 3 hours 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 an hour 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!)

    • mananaysiempre 3 hours 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.

    • dgl 3 hours 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 2 hours 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 3 hours 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 2 hours ago

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

    • bilbo-b-baggins 2 hours 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.

    • mikepurvis 3 hours 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 3 hours 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.

    • metadat 3 hours ago

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

      • ShroudedNight 3 hours ago

        A wheel is generous. This seems more like inviting the computing equivalent of spilling twenty thousand tons of crude into the sea, which then promptly catch fire.

        • catlifeonmars 42 minutes ago

          Eh it’s about the same level of footgun you might see in C99. It’s not great but you’re being hyperbolic if you ask me.

  • MH15 an hour ago

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

  • linhan_dot_dev 3 hours 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 an hour 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.