Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> Go's confusion between "type aliases" and "newtypes". The only way to make a newtype in go is to make a separate package with an opaque type and use interfaces for indirection, which is costly and awkward.

It's very unclear what this is supposed to mean. 'type Foo int' in Go creates a new 'Foo' type that can't be used as an int without casting (a̶l̶t̶h̶o̶u̶g̶h̶ ̶y̶o̶u̶ ̶c̶a̶n̶ ̶a̶s̶s̶i̶g̶n̶ ̶a̶n̶ ̶i̶n̶t̶ ̶t̶o̶ ̶i̶t̶). If you don't want to allow that, you can just wrap it in a single member struct.



> 'type Foo int' in Go creates a new 'Foo' type that can't be used as an int without casting (although you can assign an int to it).

You can't, in fact:

    type Foo int

    func main() {
        var foo Foo = 1
        var bar int = 2

        foo = bar // cannot use bar (variable of type int) as Foo value in assignment
    }
The reason you can do things like:

    var foo Foo = 1
is that because numeric literals in Go are not ints, they are untyped [0].

[0] https://go.dev/blog/constants


Ah yes, good point. Thanks for the correction.


I moved some words around to make the nit less confused - thanks for the feedback!


Still not seeing it. You don't have to use interfaces to make an opaque type. A package boundary is sufficient.

Not sure why you would want a fully opaque type internally to a package anyway. A regular `type Foo Bar` declaration is sufficient to give you a `Foo` that can't be accidentally interchanged with a `Bar`.


One impediment is that Go cannot prevent the construction of invalid values, because every type has an implicit zero value:

  type NonZeroInt struct {
    v int
  }

  func (n NonZeroInt) Get() int {
    return n.v
  }

  var n NonZeroInt  // invalid!
You could of course validate on read:

  func (n NonZeroInt) Get() (int, bool) {
    if n.v == 0 {
      return 0, false
    }
    return n.v
  }
…but that's hardly good design.

You can improve on this marginally with an interface, because an interface's zero value is at least nil, which is, at least, explicit about whether there is a value or not. But it's not very elegant:

  type NonZeroInt interface {
    Get() int
  }

  type nonZeroInt struct {
    v int
  }

  func (n nonZeroInt) Get() { return n.v }

  func ToNonZeroInt(v int) (NonZeroInt, bool) {
    if v == 0 {
      return nil, false
    }
    return nonZeroInt{v}, true
  }
The biggest problem with this approach is that it adds overhead. It can also force values to be heap-allocated, though I believe Go will still optimize single-word interface values to avoid this.

Note that since Go interfaces use structural typing, naming the method Get() can cause issues:

  type AnyInt struct {
    v int
  }

  func (n AnyInt) Get() int {
    return n.v
  }
Now AnyInt fulfills the NonZeroInt interface, which is of course not something we want. So for types like these, it's a good idea to name the method explicitly:

  type NonZeroInt interface {
    GetNonZero() int
  }


Using interfaces here seems unnecessarily complex. If the default zero value were an issue in practice, I would just define

    type NonZeroInt *int

    OR

    type NonZeroInt struct { v *int } // (if you want it to be fully opaque)
so that the program would panic on an attempt to do arithmetic on the nil value. That achieves the same effect as your code, but without the unnecessary interface definition and the resulting naming issues. (Though note that Go does make it possible to export an interface from a package that no-one outside the package can implement. All you need to do is add a dummy private method to the interface.)

It's true of course that the pointer adds runtime overhead, but two points:

(1) Go is not really advertised as a zero cost abstraction language (in contrast to e.g. C++, Rust). So yes, building nice abstractions in Go will sometimes have a runtime cost. If that is unacceptable in a given application, then Go is not the right tool for the job. I'm not a Go zealot. If you need to do this kind of thing all over the place in your code base, then sure, don't use Go.

(2) If you are dealing with large arrays of non-zero ints (such that the overhead of boxing would be significant) then you could always define NonZeroIntArray.


That's not the only/main reason to want a newtype though - it's also about restricting what the value can be, see something like NonZeroU64: https://doc.rust-lang.org/stable/std/num/struct.NonZeroU64.h...

numeric literals (mentioned by paskozdilar in a parent comment) being untyped is another footgun I hadn't even thought of including - it makes it impossible to achieve in TypeScript/Java-level enums.


You can define NonZeroU64 in Go exactly the same way it's defined in Rust – as a struct with a private field. No interfaces required.

But I was wondering why you would want an opaque type internally to a package. Presumably not to ensure that the value has passed some validation, since in that case you would want to encapsulate that validation logic in its own package. It's not like Go imposes a tax on packages. Packages are the unit of encapsulation in Go, just as modules are the unit of encapsulation in Rust.


> But I was wondering why you would want an opaque type internally to a package.

You can have a public function (a constructor) returning an instance of a private type. This way you ensure it's not zero-initialised by default (you can't do "var f privateType", you need to call the constructor).

But it's annoying to do. For every type you'd ever want to protect against zero-initialisation, you'd need to declare it private in some other package, and create a constructor. Therefore, most people just don't bother and prefer to live with the increased risk of bugs.


Zero initialization is a pretty fundamental concept in Go. I can see why people might not like it, but if you are trying to prevent a large number of types from being zero initialized then you are just going against the grain of the language. In most cases you can arrange to make the zero value valid.

To me it seems extremely weird to phrase this kind of criticism by saying that Go lacks proper support for newtypes, which simply isn’t true. If the author’s real complaint is with zero initialization then it would be a lot easier to understand their point if they made this explicit.

> But it's annoying to do. For every type you'd ever want to protect against zero-initialisation, you'd need to declare it private in some other package, and create a constructor.

How is this annoying? You’ll need a constructor anyway if you’re doing validation on the value. Apart from that you’re just complaining about having to make a package, but that’s really simple. I don’t see how Go would be improved by layering additional privacy mechanisms on top of the package system.


> Zero initialization is a pretty fundamental concept in Go.

I don't get it. There's nothing particularly special about it other than it being an explicit choice (I suspect Go's developers were lazy and did whatever was easier to implement). If, instead of automatically initializing to zero, the compiler said "error: uninitialized struct field", we wouldn't be having this conversation, and we wouldn't have this class of bugs. I would consider it "fundamental" if there was an obvious benefit from this choice, but I think a more appropriate word is "arbitrary".

> In most cases you can arrange to make the zero value valid.

Valid doesn't mean correct. Corrupting the DB with zero-initialized data can be worse than crashing early due to an unitialized (nil) pointer.

> If the author’s real complaint is with zero initialization then it would be a lot easier to understand their point if they made this explicit.

They did, it's mentioned in several places. E.g.:

---

Go fails to prevent many other classes of errors: it makes it easy to accidentally copy a mutex, rendering it completely ineffective, or leaving struct fields uninitialized (or rather, initialized to their zero value), resulting in countless logic errors.

---

> Apart from that you’re just complaining about having to make a package, but that’s really simple.

In order to prohibit direct struct initialization (which can be a source of unintended bugs) and enforce using the constructors, two types "related" to each other would have to live in different packages. E.g., if a type A's method constructs a type B, you'd segregate them and keep only the constructors public. In the context of a medium-sized project, you will end up with MANY packages. Sure, it's simple to create packages, but it can becomes painful to manage once you need to understand the code. So at the end of the day, people don't do it and instead elect to be "more careful", which isn't a good method of preventing bugs.


>There's nothing particularly special about it other than it being an explicit choice

Yes, this is what I meant. Go is explicitly designed according to the philosophy that it is beneficial overall for every type to have a default zero value. You may disagree with this, but if you find you are constantly fighting zero initialization, you should probably just use a different programming language.

>Valid doesn't mean correct.

I understand that. However, in most cases, you can arrange for zero to be a valid value. It may require a little creativity, but it's rare for it to be impossible in my experience.

As to packages, I still don't see the issue. The related types can be in subpackages of a parent package. And as you say, it's simple to create packages.

>So at the end of the day, people don't do it and instead elect to be "more careful", which isn't a good method of preventing bugs.

I don't think this choice has anything to do with the overhead of packages. Most people just don't like the style of 'bondage and discipline' coding where the type system is maximally exploited to enforce every possible invariant. Again, this is just fundamental to Go. It was designed by people who have explicitly said that they don't see value in using the type system this way.

By the way, I am no stranger to the possibilities in this space, having been paid to write Haskell code for a while. I've even done absurd things with the type system like this: https://adrummond.net/posts/cooper However, I tend to think the Go folks are right on this. It's good to have a basic type system, but there are rapidly diminishing returns on the fancier stuff.


You generally want newtypes as proofs that a value has gone through some validation.


And what prevents you using them this way in Go? Just make a package and export a constructor function that does the validation.


I think that was the author's point. You _can_ use them this way but it's a hassle and most people don't do it. The language itself doesn't protect you against zero-initialisation errors, even though it could.


Yeah, his complaint about newtypes is just incorrect.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: