1. 16
  1. 7

    The lack of what this proposal describes as package level enforcement at construction time has been a source of much verbosity and a frustrating need to write obtuse validation code in my own go projects (or rather, to remember the need to).

    I am completely unqualified to comment on this solution as it pertains to programming language design, but I can affirm that the motivation reflects something I’ve found wanting. So, thank you for sharing.

    1. 6

      I feel the pain here, but am not convinced of the solution. Isn’t this just an enforced constructor?

      I can think of lots of different ways I’d deal with this sort of problem (a isValid sort of method used liberally, or having a non-exported bool isValid in the struct itself), but I can’t think of anything as elegant as your suggestion that works with the existing language. I think @bfiedler ’s suggestion of a go vet is probably the best bet.

      1. 3

        I agree that the problem described occurs often in practice, however I am against defining new keywords (or combinations of keywords) for this, as this unneccessarily complicates the language, and zero values for package-exported types should be valid.

        I would much rather see go vet support for this, i.e. a warning when not using the package’s constructor if present (e.g. “manual struct initialization of XXX, use NewXXX instead”), maybe using some opt-in methods such as //vet:checkConstructor on the constructor(s)

        1. 1

          I’d be very open to any non-keyword approach to the syntax - can’t think of anything off the top of my head though.

          re zero-values: just to check I understand what you mean. Zero-values are valid for package-created-types, they just can’t be created outside the defining package. I guess you could have the same thing as this proposal that only restricts explicit initialisation, or partial initialisation (i.e initialize with a subset of fields), but the benefits seem much narrower. Also you would lose the proposal’s benefit re enums - all enums would gain zero in their range of values, so you’d still need to validate most of the time.

          1. 1

            We use capitalisation for exported/not-exported, so type names are already significant.

            Would using a leading underscore (a la C-style “ths is internal”) to mean “may only be instantiated in this package” be too magic?

            Then you can be sure that any instances which escape the package were constructed in it, presumably allowing invariants to be honoured.

            Edit:

            I’ve now read the likely-decline reason:

            As discussed above, this proposal does not support zero values, and does not provide any special mechanism for copying a value (or avoiding copying a value). Without those features this functionality is not a good fit for the language and would be quite difficult to use in practice. Therefore, this is a likely decline. Leaving open for four weeks for final comments.

            Which I don’t address, ah well.

            1. 1

              Oh indeed, I meant partial initialization in the above, whoops.

              My argument is that the most extreme partial initialization would be not specifying anything - i.e. the zero value of your package type.

          2. 2

            Not an expert in any sense here, but I’ve always felt like Go could solve some of these encapsulation and visibility problems by allowing interfaces to specify read-only “fields” instead of just methods.

            package interval
            
            type Interval interface {
            	Lower int // note: no "()" here
            	Upper int
            }
            
            type privateInterval struct {
            	Lower int
            	Upper int
            }
            
            func NewInterval(lower, upper int) Interval {
            	// validate invariant
            	return privateInterval{lower, upper}
            }
            

            This would, presumably, allow the compiler to resolve those fields more efficiently than requiring a method call since the only way to implement those fields is with struct fields. It might be a little slower because the memory layout of the implementing structs could be different. This would also avoid having to implement trivial getter methods as would currently be required for doing this with an interface.

            1. 1

              I am not sure why this would be more efficient than inlined getters.

              1. 1

                Yeah, as long as the compiler will inline your methods then it’s going to be the same thing. But with Go as it currently is you still have to write the getters. I suppose one could actually do what I proposed above with code generation, assuming the compiler is currently smart enough to inline simple methods (which I assume it is).