1. 29
    1. 14

      This is how it is in C and C++, which Go is closely related to. I think this is only surprising when compared to languages where literally everything is a reference (e.g. Java, Python, JS, or Ruby).

      1. 5

        This is how it is in C++

        Debatable, as copy constructors are usually deep.

        1. 3

          That is irrelevant. When you do a = b in C++, in all cases where the type of a and b is a struct (or class), you will get either copy of b in a, or you will get a compile failure. Whether that’s a deep or shallow copy depends on the copy-assignment operator for the type of a, but it will always be a copy of the struct.

          1. 1

            I don’t think default ones are.

            1. 2

              They are… sort of. As in, the copy constructor of members will get executed. However, the copy constructor of a pointer just copies the pointer.

              So they’re “deep” as in “members also get copies”, but not “deep” as in “members which are pointer types will allocate a new object and point to the new object”.

              1. 4

                This is exactly what’s described in the article, and isn’t considered a deep copy.

                1. 4

                  In C++ copied strings and vectors will result in deep copies with new allocations, as they are NOT pointer types. Go strings and slices are copied shallowly as they ARE pointer types.

                  Also the original problem was Go ranges iterating by value. C++ can iterate by reference.

                  1. 2

                    “Members are copied recursively” does sound like a deep copy, in a way, though. And you could totally make a CopiedPtr class which is sorta like a std::unique_ptr except it has a copy constructor which allocates and copy-constructs a new pointed-to object with new.

                  2. 1

                    members which are pointer types will allocate a new object and point to the new object

                    This is generally what’s referred to as a deep copy. A shallow copy is memcpy(dst, src, sizeof(*src))

                    1. 4

                      It’s not a memcpy though. It’s recursively running copy constructors. Strings and vectors will allocate a new buffer and copy their contents over; that doesn’t sound totally “shallow” to me.

                      And in fact, you could make a CopiedPtr class which allocates and copy-constructs a new pointed-to object with new. An object tree where all pointers are CopiedPtr rather than the other pointer/reference types would be deeply copied.

                      C++ is too complicated for simple statements like “copies are shallow” or “copies are deep”. Copies are customizable. And the language’s and standard library’s pointer types are copied shallowly (or refuse to be copied altogether, as is the case with unique_ptr), while the standard library’s container types are copied deeply.

                      1. 1

                        That’s true. It is in between a bit.

                        But given the original scenario of pointers, where the default copy constructor is basically memcpy, I feel like it worked for that example.

                        I still feel like the phrase “copies are usually deep” is overlooking a lot.

                        And really, things like strings don’t use the default constructor (the thing I specified), they use a custom one

            2. 7

              With all the other languages using the term ‘struct’ as a value type, I would assume it copied until looking it up.

              1. 6

                If you want to make sure the method doesn’t mutate the struct t, use a value receiver.

                I think the biggest footguns with value receivers is that, if you forget that you have one (and it’s just a one character difference in the signature), and modifications you make to the receiver are silently discarded. If I remember correctly there are no warnings for this, even if there’s no code that ever reads back your writes.

                1. 6

                  I wish that the Go team hadn’t copied C’s mistake of using *T for the type “pointer to T”. It should be &T, so & always means “take a pointer” and * always means “dereference a pointer”. func (s &MyStruct) Frobnicate() would at least have a big & instead of a tiny * in it. Or they could have used @ or something. Maybe a use a keyword for deref because you don’t actually do it that often.

                    1. 4

                      Doesn’t Carlana’s comment already contain her reasoning? Here’s a rephrasing.

                      1. The symbol * in types is inconsistent with the symbol & in values, though both have the same general meaning of “take a pointer”.
                        • Consistency is a good property for programming language syntax.
                      2. The symbol * is easy to overlook in function signatures, as compared to &, @, or a keyword.
                        • As oconnor663 described, overlooking the absence of that symbol can lead to bugs.
                      1. 6

                        The symbol * in types is inconsistent with the symbol & in values.

                        This is stated as fact but no proof is provided. I don’t see how it could even be a valid proposition. Values use both * and &, but in Go there is only one annotation used for types. Any choice would be arbitrary.

                        Mind you, this choice is not only consistent with C, but consistent with Rust as well. Rust has both *T and &T, and Go’s pointers are clearly analogous to Rust’s raw pointers and not to Rust references since Go pointers are nullable.

                        though both have the same general meaning of “take a pointer”.

                        The meaning of *T is a pointer to T, no more and no less. No “taking of” action is implied. And in Rust *T and &T are both some sort of reference with the notational choice simply indicating which type of reference it is and not indicating some sort of asymmetry, unlike * and & when applied to values.

                        The symbol * is easy to overlook in function signatures, as compared to &, @, or a keyword.

                        Citation needed.

                        Notating types by &T is a perfectly sensible choice, but I object to claims that *T is inconsistent or less readable than &T.

                        1. 9

                          This is stated as fact but no proof is provided.

                          I think the reasoning is quite simple: on values, &thing gives you a pointer to thing; it would be nice if on types, &Thing would give you the type of pointers to Things. Or at least that’s how i understood it.

                          Aside: i think C has a good rationale for using * as the symbol for pointers types, even if it is the dereference operator. The declaration int foo says “foo is an int”, and the declaration of a function int foo(char) says “calling foo with a char is a an int”. Similarly, int *foo says “dereferencing foo is an int”, which i find pretty elegant and consistent TBH (and also a nice argument for putting the * besides the variable name rather than the type).

                          1. 2

                            Regarding why * would be easier to overlook than &, @, or a keyword, I thought it was visually apparent, so no citation was needed. The asterisk * (or, in another font, *) is a visually smaller symbol – it has fewer black pixels than the other symbols. Also, it cannot be seen when the upper half of the line is scrolled offscreen. Just as the symbol * is easier to see than ' (straight quote), the symbol @ or keyword pointer would be easier to see than *.

                            Of course, the difference in difficulty between noticing * and noticing other symbols depends on the viewer’s font, font size, color scheme, screen DPI, and so on. Also, I haven’t personally had a problem noticing * symbols in the programs I write (though those programs usually aren’t in languages that use * as part of types). But I can easily believe that those programmers who do overlook * symbols would, on average, be less likely to overlook a larger symbol.

                            1. 3

                              As discussed on lobste.rs a few weeks ago IIRC, real men use non-breaking space as their sigil symbol.

                              1. 2

                                Transition saved my life, ROFL.

                    2. 5

                      this is one thing I like about languages like Rust that use keywords instead of characters for this, it makes it much more obvious when you see a “mut” (highlighted too, since it’s a keyword) compared to “*” (which is not highlighted and easily missed sometimes) it still catches me even after a decade of Go!

                      1. 1

                        I almost never want to use a value receiver unless it’s something like a Point type that is two int64s.

                        A compiler inserted unsynchronized shallow copy is, to a first approximation, never the behavior I want when calling a method.

                      2. 4

                        A good way to understand it is that everything in Go is copy on assignment / call by value, but some types have internal pointers, like slices and maps. A pointer is best understood as “don’t copy this! just use the original, over there.”

                        Like takes this Python:

                        >>> x = 1
                        >>> y = x
                        >>> x = 2
                        >>> y
                        1
                        >>> x = [1]
                        >>> y = x
                        >>> x.append(2)
                        >>> y
                        [1, 2]
                        

                        If you wanted y to update to 2 when x updates to 2 as happens to list y when you append to list x, there’s no proper way to do it (of course, you could hack locals() or something).

                        But with Go, you can decide if you want a copy or the original:

                        x := 1
                        y := x // copies x
                        x = 2 // doesn't change y
                        
                        // vs.
                        x := 1
                        y := &x // now x is the original and y just shows its value instead of being a copy of it
                        x = 2 // now *y is 2 also
                        
                        1. 8

                          Even pointers are copy by value, but the value is a location, instead of the thing at the location. It’s like writing down a quote and giving it to someone, versus writing down the name of the book and page number where they could find the quote.

                        2. 3

                          For example, this code prints [1 2 3 555 5] (code on play.go.dev)

                          x := []int{1, 2, 3, 4, 5}
                          y := x[2:3]
                          y = append(y, 555)
                          fmt.Println(x)
                          

                          I did not know this. This is kind of insane to me. The existence of this footgun seems like a fundamental flaw in the design of Go slices.

                          I suppose it’s good for performance, but it seems like clone-on-write behavior (or something similar) would be preferable.

                          1. 1

                            I wanted to buy the mentioned book, but paying 50 bucks for a paperback just isnt in my budget as a university student, are all technical books that expensive?

                            1. 6

                              No argument that the prices are high for students. That said, my sense is that it is expensive (and challenging) to produce good technical books. Authors, editors, etc. deserve to be paid well, and publishers (especially those with small overall sales numbers) operate with tiny profit margins. It’s a tough business. (My point being that I’m not sure that I agree with liquidev that the books are truly overpriced. It’s not clear to me what a fair price is for such books. Mass market paperbacks can be far cheaper for all sorts of reasons that don’t apply to technical books.)

                              In any case, if you are willing to give an email to the publisher (Manning), they offer daily discounts of one type or another. I buy books from them reasonably often and pretty much always at 40-50% off. You just have to be willing to receive regular emails and buy when you get a good offer for the format (or format and item) that you want. (The sales tend to be by format, but sometimes by topic or language.)

                              1. 3

                                My point being that I’m not sure that I agree with liquidev that the books are truly overpriced.

                                I think you’re right, my choice of words doesn’t feel like it tells the whole story. What I should’ve said is that they are really expensive compared to popular books (pick your favorite piece of fiction), but that lower demand is probably an aspect that contributes to the price.

                              2. 2

                                From what I’ve seen yeah, technical books tend to be way overpriced, because publishers know companies will buy them anyways.

                                Aside, but this seems to pretty much apply to anything companies may buy. Corporate IT hardware is like that too.

                              3. 1

                                I read a whole book before writing anysignificant amount of Go, so I’m happy to report I knew all of these :)