top | item 17859963

Go 2 Draft Designs

909 points| conroy | 7 years ago |go.googlesource.com | reply

473 comments

order
[+] pcwalton|7 years ago|reply
Looks good. The control flow of "handle" looks a bit confusing, but overall I definitely like the direction this is going.

The decision to go with concepts is interesting. It's more moving parts than I would have expected from a language like Go, which places a high value on minimalism. I would have expected concepts/typeclasses would be entirely left out (at least at first), with common functions like hashing, equality, etc. just defined as universals (i.e. something like "func Equal<T>(a T, b T) bool"), like OCaml does. This design would give programmers the ability to write common generic collections (trees, hash tables) and array functions like map and reduce, with minimal added complexity. I'm not saying that the decision to introduce typeclasses is bad—concepts/typeclasses may be inevitable anyway—it's just a bit surprising.

[+] the_duke|7 years ago|reply
I don't like the possibility of nested handle() blocks.

This could make grasping the control flow in a function very cumbersome.

Everything else looks like a very good change to me and would draw me back in to Go.

[+] centril|7 years ago|reply
I don't think the Go 2 generics proposal amount to type classes;

From what I inferred from the proposal, contracts are structurally typed rather than nominally such that you don't have a location at which you explicitly denote that you are implementing something. Rather, the fact that a type coincidentally has the right methods and such provided becomes the implementation / type class instance.

Also; I didn't check this in a detailed way, but does contracts as proposed have the coherence property / global uniqueness of instances? I would consider that a requirement to call a scheme for ad-hoc polymorphism on types "type classes". In other words, Idris does not have type classes but Haskell and Rust do.

[+] chombier|7 years ago|reply
I'm also surprised, manual dictionary passing ("scrap your typeclasses"-style) seems way simpler.
[+] zbobet2012|7 years ago|reply
I dislike concepts because it seems to be a half measure; one that is already causing them pains in the design.

For example the issues with, implied constraints, infinite types, and parametric methods could be solved by using something more properly founded in type theory. Concepts are effectively refinement types[1] and a properly founded implementation from type theory would mitigate these issues.

[1] https://en.wikipedia.org/wiki/Refinement_type

[+] JelteF|7 years ago|reply
Overall really great that Go is addressing it's current biggest issues. I do think the argument for the check keyword instead of a ? operator like in Rust is quite weak. Mainly because a big advantage of the ? operator (apart from the brevity) is that it can be used in method calling chains. With the check keyword this is not possible. AFAICT the check keyword could be replaced for the ? operator in the current proposal to get this advantage, even while keeping the handle keyword.

Furthermore the following statement about rust error handling is simply not true:

> But Rust has no equivalent of handle: the convenience of the ? operator comes with the likely omission of proper handling.

That's because the ? operator does a min more than the code snippet in the draft design shows:

    if result.err != nil {
	    return result.err
    }
    use(result.value)

Instead it does the following:

    if result.err != nil {
	    return ResultErrorType.from(result.err)
    }
    use(result.value)
This means that a very common way to handle errors in Rust is to define your own Error type consumes other errors and adds context to them.
[+] munificent|7 years ago|reply
> I do think the argument for the check keyword instead of a ? operator like in Rust is quite weak. Mainly because a big advantage of the ? operator (apart from the brevity) is that it can be used in method calling chains.

In Dart (as in a couple of other languages) we have an `await` keyword for asynchrony. It has the same problem you describe and it really is very painful. Code like this is not that uncommon:

    await (await (await foo).bar).baz);
It sucks. I really wish the language team had gone with something postfix. For error-handling, I wouldn't be surprised if chaining like this was even more common.
[+] rsc|7 years ago|reply
You're right, I oversimplified that. But having a "ResultErrorType" is not really context, not by itself. The interesting context would be additional fields recorded in that type.
[+] marcus_holmes|7 years ago|reply
Go doesn't chain things much. I like that it doesn't.

Verbosity can be a pain. But needlessly dense code is (imho) less readable. Having a single character that changes the entire context of a statement is not fun (I have the same problem with !). Reading a set of a dozen chained functions, some with ? and some without... that's not fun for anyone.

[+] kyrra|7 years ago|reply
I get the feeling that the core Go team is rather against 1-character operators. Also, the Go community doesn't tend to do a lot of call chaining from a general stylistic standpoint.
[+] orblivion|7 years ago|reply
> chaining

Could you do this?

check (check func1()).func2()

Looks ugly but maybe they can clean it up further. In fact maybe they could just introduce something that looks like ? but works like check.

[+] JulianMorrison|7 years ago|reply
It looks like check can be used in calling chains.

That is, if F is string to string,error, and G is empty to string,error then

  s := check F(check G())
either assigns a string to s, or calls the handler.
[+] infogulch|7 years ago|reply
These are all great starts, I'm glad the Go team is finally able to get some purchase around these slippery topics!

Unless implied constraints are severely limited, I don't think they're worth it. The constraints are part of the public interface of your generic type, I'm worried that we could end up with an accidental constraint or accidentally missing a constraint and having the downstream consumer rely on something that wasn't an intended behavior.

For Go 2, I would really like to see something done with `context`. Every other 'weird' aspect of the language I've gone through the typical stages-of-grief that many users go through ("that's weird/ugly" to "eh it's not that bad" to "actually I like this/it's worth it for these other effects"). But not context. From the first blog post it felt kinda gross and the feeling has only grown, which hasn't been helped by seeing it spread like an infection through libraries, polluting & doubling their api surface. (Please excuse the hyperbole.) I get it as a necessary evil in Go 1, but I really hope that it's effectively gone in Go 2.

[+] majewsky|7 years ago|reply
How would a replacement for `context ` look?

Also, I don't share your sentiment. I actually find Context to be a quite nice, minimal interface. Having a ctx argument in a function makes it very clear that this function is interruptible. I certainly prefer this over introducing a bunch of new keywords to express the same.

[+] evincarofautumn|7 years ago|reply
As a type theorist and programming language developer, I’ll admit that’s a fairly reasonable design for generics.

I’m still a bit disappointed by the restrictions: “contracts” (structural typeclasses?) are specified in a strange “declaration follows use” style when they could be declared much like interfaces; there’s no “higher-level abstraction”—specifically, higher-kinded polymorphism (for more code reuse) and higher-rank quantification (for more information hiding and safer APIs); and methods can’t take type parameters, which I’d need to think about, but I’m fairly certain implies that you can’t even safely encode higher-rank and existential quantification, which you can in various other OOP languages like C#.

Some of these restrictions can be lifted in the future, but my intuition is that some features are going to be harder to add later than considering them up front. I am happy that they’re not including variance now, but I feel like it’ll be much-requested and then complicate the whole system for little benefit when it’s finally added.

[+] kyrra|7 years ago|reply
They are truly looking for feedback on these proposals. If you have thoughts on how could change to be potentially more compatible for future additions, consider doing a blog post write up and submitting it to them.
[+] ianlancetaylor|7 years ago|reply
Thanks for the comments. We tried for some time to declare contracts like interfaces, but there are a lot of operations in Go that can not be expressed in interfaces, such as operators, use in the range statement, etc. It didn't seem that we could omit those, so we tried to design interface-like approaches for how to describe them. The complexity steadily increased, so we bailed out to what we have now. We don't know whether this is is OK. We'll need more experience working with it.

I'm not sure that Go is a language in which higher-level abstraction is appropriate. After all, one of the goals of the language is simplicity, even if it means in some cases having to write more code. There are people right here in this discussion arguing that contracts adds too much complexity; higher order polymorphism would face even more push back.

[+] conroy|7 years ago|reply
Old and busted:

    func printSum(a, b string) error {
        x, err := strconv.Atoi(a)
	if err != nil {
		return err
	}
	y, err := strconv.Atoi(b)
	if err != nil {
		return err
	}
	fmt.Println("result:", x + y)
	return nil
    }
New hotness:

    func printSum(a, b string) error {
	x := check strconv.Atoi(a)
	y := check strconv.Atoi(b)
	fmt.Println("result:", x + y)
	return nil
    }
[+] reificator|7 years ago|reply
Why the handler function over something like Rust's propagation operator? [0]

I see that it adds significant flexibility, but at the cost of verbosity and a new keyword that largely conflicts with one of Go's niches right now, web servers. And that flexibility seems likely to go unused in my experience. I would be shocked to see anything other than `return err` inside that block.

Sure errors are values and all that, and maybe I'm just working on the wrong codebases or following worst practices. But generally I see three approaches to errors in Go code, in order of frequency:

1. Blindly propagate down the stack as seen here. "It's not my problem- er, I mean, calling code will have a better idea of what to do here!"

2. Handle a specific type of error and silently recover, which does not typically need a construct like this in the first place.

3. Silently swallow errors, driving future maintainers nuts.

This seems to only really help #1, but `return thingThatCanError()?.OtherThing()?` can handle that just as well.

[0]: https://doc.rust-lang.org/book/second-edition/ch09-02-recove...

[+] jerf|7 years ago|reply
In your new hotness, I believe that handler is already implicit, so you can just leave it off.

What I am still curious about, and can't see whether or not is possible from the current draft (possible I've just skimmed too hard) is whether printSum can become:

    func printSum(a, b string) error {
        fmt.Println("result:",
            check strconv.Atoi(a) +
            check strconv.Atoi(b))
        return nil
    }
Possibly with an addition paren around the check.

(I'm just using this as an example; in this case I probably would assign x & y on separate lines since in this case this saves no lines, but there are other cases where I've wanted to be able to call a function that has an error but avoid the 4-line dance to unpack it.)

[+] millstone|7 years ago|reply
Serious question: why is the error return from Println not handled?
[+] spyspy|7 years ago|reply
Only seems useful when you have a function and want every error handled the exact same way, and don't have any requirement to add context the way https://github.com/pkg/errors does.
[+] rdsubhas|7 years ago|reply
To be frank, I was for a long time on the camp that Generics is a much-needed feature of Go. Then, this post happened: https://news.ycombinator.com/item?id=17294548. The author made a proof-of-concept language "fo" on top of go with generics support. I was immediately thrilled. Credit to the author, it was a great effort.

But then, after seeing the examples, e.g. https://github.com/albrow/fo/tree/master/examples/, I could see the picture of what the current codebase would become after its introduced. Lots of abstract classes such as "Processor", "Context", "Box", "Request" and so on, with no real meaning.

Few months back, I tried to raise a PR to docker. It was a big codebase and I was new to Golang, but I was up and writing code in an hour. Compared to a half-as-useful Java codebase where I have to carry a dictionary first to learn 200 new english words for complicated abstractions and spend a month to get "assimilated", this was absolute bliss. But yes, inside the codebase, having to write a copy-pasted function to filter an array was definitely annoying. One cannot have both I guess?

I believe Golang has two different kinds of productivity. Productivity within the project goes up quite a lot with generics. And then, everyone tends to immediately treat every problem as "let's create a mini language to solve this problem". The second type of productivity - which is getting more and more important - is across projects and codebases. And that gets thrown out of the window with generics.

I don't know whether generics is a good idea anymore. If there was an option to undo my vote in the survey...

[+] floatingsmoke|7 years ago|reply
A bunch of pioneer project like docker, kubernetes, etcd, prometheus etc. has been built with go and I don't believe that the maintainers suffered lack of generics and error handlers. On the other hand, as a new go programmer, I can really dive into their code base and understand each line of code without thinking twice. This comes from simplicity.

But these possible nested error handlers and generics will lead developers to think more than twice during writing or reading a code base. These ideas is not belong to go era but Java, C++ etc which go doesn't wanted to be like.

Someone here has mentioned that internal support of generics for slices, maps and other primitives. I think this can be the best solution for generics in go. For the error handling I think more elegant way could be found.

Please do not rush.

[+] spullara|7 years ago|reply
I really thought they were going to allow their idealism to win over pragmatism. Versioned modules, generics and exception handlers all going in is really going to put the defenders of them being missing in a tough spot.
[+] wvenable|7 years ago|reply
I'm disappointed the Java has given exceptions such a bad wrap that all new programming languages dance around them like crazy. I understand Rust going with error returns due to it's low-level nature but Go is pretty high level.

This semi-explicit error handling code optimizes for fairly trivial cases as very few real-world functions don't have some kind of exception condition. The difference between prefixing nearly every function with "check" vs. having that implicit is very small.

Proper use of exceptions puts the focus on where you can handle the error rather than where the error occurs. Java screws this up with checked exceptions, which puts the all focus on error site, and programmers have subsequently determined (correctly) that checked exceptions are hardly better than error returns.

However, that is fundamentally the wrong way to look at error management. If a method 17 levels deep on the call stack throws a network error, I shouldn't have to care about those 17 levels because I'm going to restart the whole operation at the point I can do that. The important part is not where the error is raised/returned.

[+] alkonaut|7 years ago|reply
Exceptions are second only to proper return types for error handling such as Result<T, E> or Option<T>.

And in order to have those you have to have generics. Once you have generics result types can be used for 99% if error handling.

[+] mike_hearn|7 years ago|reply
It's not really about Java giving exceptions a bad rap. After all exceptions are a feature of almost any OOP language designed after 1990 and they work nearly the same way in all of them.

I've argued in the past that the reason the current crop of languages that compile to native code tend to avoid exceptions, is to do primarily with implementation cost, the backgrounds of the language designers and a mistaken attempt at over-generalisation in an attempt to find competitive advantage.

https://blog.plan99.net/what-s-wrong-with-exceptions-nothing...

One problem is that the sort of people who write native toolchains that compile to LLVM or direct to machine code like Go, tend to be the sort of people who spent their careers working in C++ at companies that ban C++ exceptions. So they don't really care or miss them much.

Another is complexity. One of the most useful features of exceptions is the captured stack trace. But implementing this well is hard, because generating a good stack trace in the presence of optimised code with heavy use of inlining requires complex deoptimisation metadata and many table lookups to convert the optimised stack back into something the programmer will recognise. Deopt metadata is something the compiler needs to produce as it works, at large cost to the compiler internals, but most compiler toolchains that aren't optimising JIT compilers (like the JVM) don't produce such metadata at all. So stack traces in natively compiled programs are frequently useless unless you compiled in "debug mode" where all optimisations are disabled. VMs like the JVM have custom compilers that generate sufficient metadata, partly because they have more use for it - they speculate as they compile and use the metadata to fall back to slow paths if their speculations turn out to be wrong. But a language like Go or Rust doesn't have a runtime that does this.

Finally, I find many of the arguments cited against exceptions to be very poor.

For example the Go designers cite Raymond Chen's blog posts as evidence that exceptions are bad because it's easy to forget to handle errors and the checks are "implicit". This seems totally backwards to me.

You cannot forget to handle an exception in most languages that use them. If you forget a method can throw and don't catch it, the runtime will catch it for you and print out a stack trace and error message that tells you exactly what went wrong and where. The thread won't continue past the error point and the runtime will stop the thread or entire app for you. But if you forget to check an error code, the error is silently swallowed and no diagnostics are available at all. The program will continue blindly in what is quite possibly a corrupted state.

The Go designers argue that with exceptions you can't see "implicit checks". This is an odd argument because, beyond not being able to see if you are ignoring a return code, all programs must have implicit checks of correctness. For example checking for null pointer dereferences or divide by zero conditions, yet it makes no sense for e.g. divide to be a function that returns an error code you must check every time. In C, such checks turn into signals that can be caught or on Windows, the OS delivers them as exceptions! Yes, even for C programs, it's called SEH and is a part of the OS API - all programs can have exceptions delivered to them at any point. If you don't catch them then the OS will catch it for you and display the familiar crash window. UNIX uses signals which are not as flexible but have similar semantics; if you don't catch the signal you'll get the OS default crash handler.

So this is no knock against exceptions. Moreover, very often you don't want to handle an error. Not all code can or should attempt to handle every single thing that can go wrong at the call site, or even at all. So in practice errors are almost always propagated back up the stack, often quite far up. For many errors there's nothing you can do beyond the default exception handling behaviour anyway of printing an error or displaying a crash dialog and quitting, e.g. many programs can't handle out of memory or out of disk conditions and nor would it make sense to invest the time in making them handle those conditions.

Exceptions were developed for good reasons; they were designed in response to the many enormous problems C-style error handling created and codified common patterns, like propagation of an error to the point where it makes sense to capture it, changing the abstraction level of errors and so on.

The Go designers have realised what many people told them from the start - that their error handling approach is bad. Unfortunately they don't seem to have taken the hint and reflected on why they made these bad decisions in the first place. Instead their language comparison ignores all the other languages that went a different direction, and only looks at Swift and Rust. Neither language is especially popular. Other modern languages like Kotlin which do use exceptions are completely ignored.

In conclusion, nothing in this document makes me think that Go is likely to fix its self-admitted design errors. They're probably just going to make variants of the same mistakes.

[+] pjmlp|7 years ago|reply
Java's explicit exceptions design is based on Modula-3, CLU, C++.
[+] stephen|7 years ago|reply
Well, shoot. I don't like/don't want to like Go, but they keep getting better.

My general annoyance is that they keep things ~just different enough to be irksome, e.g. in this proposal generics is using parens:

type List(type T) []T

Instead of using <> like every other language (okay, I meant Java and C++) with generics:

type List<type T> []T

(Another example of being ~just different enough to be annoying was switching from C style `int i` to `i int` but then not using colons like `i: int`. And then using type name case for public/private (wtf), so I read variable decls of `foo foo`, which happens all the time in methods that are private implementation details, and my polygot brain interrupts: ...damn, which one is the type again?)

As usual, I'm sure `(type T)` vs `<type T>` will be due to "reasons" (...oh, maybe it's that crazy `<-chan` syntax that I dyslexically can never keep straight and wish it was just real type names like `ReadChan[T]` and `WriteChan[T]`), but really I hop languages all day and this makes a big difference to my eyes going back/forth.

They should also go back and fix `map[K]V` to be `Map[K, V]`; iirc one of the touted benefits of `gofmt` was to "make automated source code analysis/fixing easy". Great, take advantage of that and get rid of that inconsistency. Same thing with `Chan[string]` instead of `chan string` (both are public types so should be upper case for consistency). It's a major release, just do it.

Anyway, disclaimer yeah I'm nit picking and this is what made Guido quit Python. To their credit, Go has great tooling, the go mod in 1.11 finally looks sane, etc. Their continual improvement is impressive.

[+] munificent|7 years ago|reply
> Instead of using <> like every other language (okay, I meant Java and C++) with generics

Using angle brackets for generics is really annoying when it comes to the grammar of the language. There are nasty edge cases around things like interpreting `>>` as a shift operator in some places but closing two type argument lists in others. There are occasional ambiguities like:

    foo ( a < b , c > - d)
Is this "foo(a < b, c > -d)" or "foo(a<b,c> - d)"?

Angle brackets are (alas) more familiar, but they aren't simple. Go seems to place a premium on the latter over the former.

[+] enneff|7 years ago|reply
The proposed contracts design is quite different to Java and C++'s generics. Why should they use the same syntax?

If you want Java and C++ then they are there for you to use. Go is a different language and its differences are not gratuitous. Use it for a while and you'll find out why "i int" works better than "int i" (compare Go's pointer and function type specs to C's).

[+] zemo|7 years ago|reply
why would you ever not want to like something? liking something is universally better than disliking something.
[+] dagss|7 years ago|reply
I am disappointed that go seems to follow Java and C++ with generics support instead of taking a more LISP-like turn like e.g. Julia.

I would much rather have powerful hygienic compile-time macros to generate code. Then I would have the power to do all thay generics does but also a lot more.

Just having those powers at compile time evaluation would be rather good, don't need to go all the way to Julia/Lisp.

We know what mess C++ became, with people abusing the template system to do compile-time metaprogramming. Much better to simply provide a good macro system.

[+] gameswithgo|7 years ago|reply
generics are immune to much of the mess that templates cause. It is a somewhat different thing. They are also less powerful, but that is fitting with Go's desire to avoid language constructs which let people make really confusing code, I think.

If you want cool meta programming abilities, there is Rust, Nim, Lisp, Julia, F# and others to choose from. No need for Go.

[+] ainar-g|7 years ago|reply
What about sum types? During 4 years I've been programming Go professionally I missed sum types more times than I missed generics. Writing yet another

  interface SumType {
      isSumType()
  }
won't kill me, but it does get annoying.
[+] arianvanp|7 years ago|reply
for sure. It would also be lovely to add sum types to Protobufs too, while we're at it. many message protocols are very neatly described with sum types in my opinion.

Also, errors would be way more elegant as a sum type:

instead of:

err, a := x

if err != nil { }

we can do:

switch a { Err(err) -> {}, Ok(a) -> {}, }

and avoid null pointers :) and then "check" is literally just "bind/ try!, flatMap, =<<" or however your language du jour calls it :)

[+] weberc2|7 years ago|reply
Annoying and the performance is terrible, since pretty much every instance ends up being an allocation, and allocations in Go are quite a lot more expensive than in other GC languages. Further, it doesn't quite have the same semantics--if you return one of these interfaces, your caller still doesn't know what all of the permutations they have to check for.
[+] tmaly|7 years ago|reply
> The call with type arguments can be omitted, leaving only the call with values, when the necessary type arguments can be inferred from the value

I think if too many different ways are offered in the generics proposal, it will work against the simplicity of the language for newcomers.

[+] _ph_|7 years ago|reply
I am very excited to see the draft designs. They point to some good progress with some open design aspects with Go.

The error handling concept looks to me as if it is ready for implementation in one of the next Go revisions. The fundamental of Go error handling vs. an exception mechanism always was, that Go errors are returned as part of function results. The calling function gets the result and the error and then can decide how to deal with it - handle the error, or return from itself. Exceptions not only mean automatic returns, but also automatic stack unwinding across all functions that do not implement an exception handler. I have always preferred principle of the Go error handling to exceptions.

What is great about the error handling proposal is, that it is fully contained inside each function. It does not change the signature of functions, they still return an error amongst the return values or not. You cannot see when looking at a function, whether they use the proposal or not. But like the defer statement, they offer an elegant way to schedule effects to happen, when a function returns. A plain return will trigger all deferred function calls, a check "catching" and error will trigger all handlers and then all deferred functions. Furthermore check works as a filter, so chaining functions which return values and errors becomes much easier. I also like the choice of "check" vs. "try" or "?". A short word reads better than an operator character and try is too familiar with an exception mechanism, what check/handler is not.

On the first glance, the handlers looked a little bit confusing about the logic flow. But if one considers check/handler the twins to return/defer, they become very obvious and fit very well into the existing framework. Handler can be considered a deferred function for the return in the error case, and check as triggering that error case return.

On the generics, the concept is not finished yet, but what is there so far looks like to be on a good path to adding generic types to Go without making it a much more complex language or too many sacrifices like the type erasure in Java. On the syntax side, I like that they managed to find a syntax, which uses the normal parens used for function and method syntax before, as this makes the code a bit nicer to read. I am very curious how they develop.

[+] adamwk|7 years ago|reply
One nit-pick in the generics overview. It claims Swift introduced generics in Swift 4, but parametric polymorphism has been in the language since Swift 1, though refined after each subsequent release.
[+] quotemstr|7 years ago|reply
FWIW, these changes make it much more likely that I would voluntarily choose to write a Go program.
[+] felipeccastro|7 years ago|reply
I agree, honestly the error handling alone has always been the main reason I never used Go. Yes, I knew there were good reasons for that design, but I prefer to have options in how to handle errors and keep a clean, elegant code.
[+] djhworld|7 years ago|reply
Really excited to see things start to take shape, I'd imagine there will be a lot of rounds in that feedback loop to get it right, but glad to see the Go team have stuff on the roadmap
[+] lostmyoldone|7 years ago|reply
That error handling looks very much FP inspired, intended or not.

I'm not going to say the M-word, someone will maybe say it in ... either case.

[+] incompatible|7 years ago|reply
It seems to be lacking a mechanism for accumulating multiple errors, which can be done using a package like hashicorp/go-multierror.

The usage pattern is that you have a complex function that does something like parse a document and return the data from it. But the file format is so complex, that a lot of minor problems can occur, but they don't necessarily prevent you getting some data out. So the return value is the data and a set of errors (or warnings, if you like).

[+] ainar-g|7 years ago|reply
This might actually be addressed, albeit very quickly, in one of drafts: https://go.googlesource.com/proposal/+/master/design/go2draf....

>To implement formatting a tree of errors, an error list type could print itself as a new error chain, returning this single error with the entire chain as detail. Error list types occur fairly frequently, so it may be beneficial to standardize on an error list type to ensure consistency.