Thoughts on Whatnot
A blog about .
Feedback for Go 2 Design Drafts

Recently, draft design documents were released by the Go development team detailing what the members of the team have been thinking about and researching for the development of features for Go 2. Specifically, these cover potential plans for error handling, changes to error values themselves, and generics. While I haven’t gotten a chance to read through the documents on error values yet, I have read the sections on error handling and generics. While I liked both overall, I did have a number of comments and questions, so, as requested in the documents themselves, I’m writing those comments and questions up as a blog post.

Error Handling

The general idea in the error handling section is to add two keywords, handle and check. handle would be used declare an entry in a ‘handler chain’, essentially a chain of pseudo-functions that are called, one after another, whenever a use of a check contains a non-nil error. For contrived example,

func Example() error {
  handle err {
    return fmt.Errorf("Something broke: %v", err)
  }

  n := check strconv.ParseInt("a", 10, 0)
  fmt.Println(n) // Never gets run.
}

Immediately, I have a number of concerns:

First of all, a minor issue: The use of a check keyword will likely become awkward in the same way that await has in all of the languages jumping on the async/await bandwagon. While it’s clean in this instance, in chained method calls it can be quite messy, as it requires a bunch of extra parentheses. For example, someone could wind up having to do something along the lines of check (check f1().f2()).f3(), which is messy. This is addressed in the section Using a ? operator instead of check, but I think that it got brushed over a little too fast. I agree that check is better than ?, but I wonder if something better might be done.

Second of all, the section Variable shadowing mentions that the existence of check would likely remove the need for the := combo declaration and assignment operator to be able to have both new and existing variables on the left-hand side. However, I don’t think this is true. While check would certainly be useful for a lot cases of repetition, as it is currently proposed it will likely remain cleaner to use the existing error handling methods in cases where specific calls need to do extensive custom error handling. I think this will probably be common enough that modifying := under the assumption that these cases will become rare seems a bit risky.

Finally, I have a suggestion for a potential fix to a problem stated in the draft. The draft mentions that the system, as proposed, doesn’t seem to work very well with usages of defer. I think this problem is being looked at from the wrong angle: It’s not that it doesn’t work with defer, but rather that it doesn’t work with closures. I think the problem could potentially be solved by having closures inherit their parent scope’s handler chain as it was at the point that the closure was declared. For example, to borrow from the document:

func Greet(w io.WriteCloser) error {
	defer func() {
		check w.Close()
	}()
	fmt.Fprintf(w, "hello, world\n")
	return nil
}

In this case, if the closure inherits the handler chain, then when check is used inside the deferred function, since the closure itself has no default handler chain, control jumps to the outer chain, thus causing Greet() to return the error.

Now, as the document points out, because of the order of execution, it could be possible for the handler chain to be run twice. For example, if a check was added before fmt.Fprintf() and it failed, the order of evaluation would dictate that first the handler chain get run, after which, due to the function returning, the deferred function would get run. When the inner check fails, the handler chain would get run again. I don’t think this is actually a problem, however, any more so than if this chain of events had been done manually with if err != nil checks. If the closure inherits the chain at the point of declaration as well, then it will also be a non-issue for cases where clean-up code is added to the chain later.

I could nitpick a few more things, such as handlers always getting run on err != nil, rather than allowing a custom case, but I’ll stop here for now. Things like that might wind up not really applying anymore depending on what other changes are made.

On that note…

Generics

Generics are a feature I’ve wanted in Go for a long time. On nearly every large project I work on I run into cases where they’d have been useful, sometimes very. I have, however, dreaded the potential that Go code might wind up looking like C++ or Rust one day. With that in mind, I have to say that this proposal looks really good. I particularly like the idea of contracts being essentially function bodies, as it really simplifies the syntax for specifying constraints. I do have a few observations, however:

My first thought is that contracts seem to make interfaces obsolete. I thought at first that the section Why not use interfaces instead of contracts? would cover this, but instead it dealt only with the syntactic differences, explaining why interfaces couldn’t do what contracts could. It’s correct, but it’s also backwards. What can interfaces do that contracts can’t? Both contracts and interfaces are specifications of functionality, rather than data structure, but contracts are a straight upgrade in power over interfaces. Although the syntax would be a little more verbose to specify just a basic interface with them, there seems to be no reason to have both, unless interfaces were just shorthand for method-set-only contracts.

While this isn’t exactly a complaint, something else I noticed is that, despite mentioning adding methods to generic types, the section covering Rust’s generics didn’t mention that Rust actually allows this, but only via traits. For example, the following is valid in Rust:

trait Example {
  fn some_iterator_operation(&self);
}

impl<T: Iterator> Example for T {
  fn some_iterator_operation(&self) {
    // In here, self is of type &T.
  }
}

Now, as long as the trait Example is in-scope, anything that implements Iterator will automatically have the some_iterator_operation() method attached to it. If there’s a conflict when you try to call it, due to there being multiple traits adding methods with the same name, you can disambiguate manually in much the same way that you could in Go by inverting the method call using a method expression: Example::some_iterator_operation(&anIterator).

For the record, I’m not advocating that this be added to Go. I just wanted to point out that Rust allows this.

Finally, there’s a feature missing that I think should be looked into. The document mentions variadic type parameters, but it doesn’t seem to mention union types. As proposed, contracts are purely intersection of functionality: The set of types satisfied by a constraint is those types with functionality the intersection of which is a superset of the functionality required by the contract. However, there’s no way to require types where the union of their functionality is a superset of the contract. In other words, contracts are purely ‘and’, not ‘or’.

For example, let’s say you wanted to write a function that operates both on slices and on types that act like slices, such as

func PrintFirst(type T sliceLike)(v T) {
  if v.Len() > 0 {
    fmt.Println(v.At(0))
  }
}

This is not possible under the current proposal. One could write a contract that allows you to call len(v) or a contract that allows you to call v.Len(), but not one that seamlessly lets you do either one. The only solution is to either revert back to interface{} or to write two functions, once again.

In a lot of languages, this issue is bypassed in most common situations via operator overloading. Personally, I really like the lack of operator overloading in Go, even if it makes math/big an absolute pain to deal with sometimes. An alternative might be to allow multiple contracts to be specified, but then require a type switch-like construct in order to determine which to use. For example, the following could be done:

contract slice(t T, e E) {
  len(t)
  var _ E = t[int(0)]
}

contract sliceLike(t T, e E) {
  var _ int = t.Len()
  var _ E = t.At(int(0))
}

func PrintFirst(type T slice(T, E) | sliceLike(T, E), E)(v T) {
  switch v.(contract) {
  case slice:
    if len(v) > 0 {
      fmt.Println(v[0])
    }

  case sliceLike:
    if v.Len() > 0 {
      fmt.Println(v.At(0))
    }
  }
}

Obviously the syntax needs a bit of work, but I think that this shows the general idea.

Conclusion

Even if none of these drafts make it into Go 2 in a way that seems remotely like how they’ve been presented, the amount of time and effort that has clearly gone into this is impressive. I look forward to seeing where this goes next.

comments powered by Disqus