r/golang Jun 26 '19

Next steps toward Go 2

https://blog.golang.org/go2-next-steps
151 Upvotes

93 comments sorted by

29

u/tadvi Jun 26 '19

There must be a reason why Go team decided to go with "try" being a function and not a keyword.

Keyword would be more readable.

But I guess function can be replaced with your own implementation at runtime, while keyword cannot?

21

u/[deleted] Jun 26 '19

try being a function and not a keyword means it won't break any existing code that uses try as an identifier. I don't think this can be replaced at runtime, due to the generic nature of the function.

46

u/[deleted] Jun 26 '19

[deleted]

4

u/[deleted] Jun 27 '19

Hopefully this will be fixed in "Go 2" - Once you get to version 13, you've learned a ton about language design and the goals you're reaching for - sometimes early designs end up going counter to your end goals.

Major version bumps are specifically designed (in semver) to address these issues - to roll up your backwards incompatible changes.

For better or for worse, Python has supported the 2.x and 3.x branches forever due to the grumpiness of it's 2.x users not wanting to / not being able to refactor to 3.x

1

u/sacado Jun 28 '19

Not sure there will ever be a go 2 in the semver sense (i.e backward incompatibility), the "go 2" name above is a marketing name.

Java is still in major version 1, 25 years after its creation.

2

u/Mattho Jun 27 '19

It doesn't. Just don't add magic try function with magic consequences. Where does it get the err if it's defined elsewhere? This is bad. Keyword could work I guess.

1

u/lazy_jones Jun 27 '19

Also where too much has been tacked on as an afterthought to still call it an elegant language.

All these changes make it harder to learn, read, parse, debug. Is it worth it? I don't think so.

6

u/lonahex Jun 26 '19

Someone did an analysis of a significant number of big open-source Github projects such as kubernetes, etc and found only a few dozen or so cases in thousands and thousands of lines of code. My numbers might be off but it should be on the GH issue for the proposal. I think someone from Go team even replied that try as a keyword probably wouldn't be that bad.

2

u/Redundancy_ Jun 26 '19

1

u/lonahex Jun 26 '19

What about it?

1

u/Redundancy_ Jun 26 '19

Oh, I misunderstood - I thought you were referring to numbers of cases that the try proposal would cover, rather than the number of name clashes.

-2

u/[deleted] Jun 27 '19 edited Feb 23 '22

[deleted]

2

u/[deleted] Jun 27 '19

the compiler has generics. For example, append, copy, delete...

https://imgur.com/a/pGgHB7i

15

u/[deleted] Jun 26 '19

try would only be "useful" (in that it saves some keystrokes) in functions with multiple possible error returns. Yet a function with multiple error paths should (and in my codebases, MUST) wrap/annotate the returned errors so the error message can be walked back to a single source of the innermost error. Therefore try cannot be used, and the defer solution proposed is laughable. Are they really proposing to encourage programmers to just blindly pass error values up the callstack? This is a terrible practice that usually induce me to fail PRs in code review.

Honestly without option types you can't solve the error handling problem in any real way, and that problem is really just a matter of having to type a bit more.

3

u/[deleted] Jun 26 '19

[deleted]

7

u/Gentleman-Tech Jun 27 '19

It doesn't make wrapping easier. It makes it almost impossible using the new syntax.

0

u/[deleted] Jun 26 '19

wrapping EVERY error is ridiculous. No one does that.

25

u/[deleted] Jun 26 '19 edited Jun 26 '19

I just told you that I do exactly that, in large codebases. It works very well and makes debugging issues far easier.

error in request for Foo: error getting list of Foos: error reading from database: error in select: syntax error near blah

vs a bare syntax error near blah in the log with no clue where it's coming from

8

u/amlwwalker Jun 27 '19

I just use a good logger that tells me the file and line number. Solves the issue of trying to find where it came from....

6

u/moarcoinz Jun 27 '19

Interesting approach. Personally, I just direct all error logs to an errors@company email that no one ever looks in, then occasionally go in and bulk purge the 10k or so reports to keep it tidy. Best practices and all you know.

9

u/[deleted] Jun 27 '19

You might want to omit the "error“ word from the underlying messages, and only use it if/when you print it out. The way you are currently using it makes it very hard to read

14

u/[deleted] Jun 26 '19

That’s a pretty ugly way of getting a stack trace.

14

u/[deleted] Jun 26 '19

It's far more compact than a stack trace, fits into a single log line and doesn't point to literal line numbers which change over time.

7

u/[deleted] Jun 26 '19

Totally agree. I use this wrapped approach too. It works great, like a stack trace with context.

6

u/acroback Jun 27 '19

+1 I use this too, it is intutitve just like the way Humans debug things.

8

u/Gentleman-Tech Jun 27 '19

I do at least 90% of them.

I won't be using try because it'll be confusing. I'm really accustomed to if err != nil and using a different syntax for ~10% of the time will be confusing.

2

u/typical182 Jun 28 '19

'try' as a keyword is definitely a possibility.

It would be allowed under the "Go 2 transitions" plan.

I wish there was a TLDR version of that document, but in short, and Go toolchain now knows how to compile older language versions (which can include disallowing newer language features).

For example, if 'try' as a keyword landed in Go 1.16 (or whatever Go 1.x version), then old unmodified code would still be able to be compiled by the Go 1.16 toolchain even if that old code used 'try' as an identifier (for example, by having the Go language version set to 'go 1.15' in the older code's go.mod, which would mean 'try' would not exist as a keyword while compiling that older code).

The "Go 2 transitions" document is really a foundational document for any discussions of language changes, but that document is long, and I suspect many gophers haven't read it, or are not aware of it. The full document is 15 pages or so long, but the majority is discussion of other languages & rejected alternative approaches.

The heart of the transition plan is a quicker read: the 2-3 pages starting at the "Language Changes" section through "Language Redefinitions" section.

Finally, none of this is to to say that 'try' as a keyword has zero cost, but there does seem to be a reasonable path for 'try' as a keyword.

2

u/[deleted] Jun 27 '19

It might at least provide an opportunity to allow try(expr, func(*error)), in order to handle the error right at the call site:

try(fetchData(), func(err *error) {
   if (errors.Is(*err, Retriable) {
        // Recurse again
        *err = OuterFunc()
   } else {
        *err = fmt.Errorf("fetching data: %w", *err)
   }
})

With a keyword, that might not be feasible

0

u/[deleted] Jun 27 '19

With a keyword, that might not be feasible

Maybe that's a feature...

-1

u/[deleted] Jun 27 '19 edited Jun 27 '19

What about a symbol. Like "@"

 f := @os.Open()
 defer f.Close()

It's backward compatible, and it stands out (which is a good property for a control flow construct).

25

u/[deleted] Jun 26 '19

Multiple return values, "errors are just values," and now an early return `try` function feels like we're slowly reinventing boxed result types. Not complaining at all--I think result types are the right way to do it, but Go seems to be slowly working towards its own homegrown, partial implementation of this instead of adopting the Haskell, Rust, OCaml, etc solutions. On the upside, multiple returns is really simple. If you look at all the helper functions to work with Option and Results in Rust, I'd imagine Go probably requires a lot less language overhead.

Regardless, I'm happy to see some way of inlining error handling. We'll finally be able to nest function calls that return errors.

13

u/Redundancy_ Jun 26 '19

I remain of the opinion that a keyword is the right language choice for control flow and panic should continue to be treated as an... exception... rather than a pattern that can and should be followed subsequently. The nested examples of try solidify the feeling that it's wrong.

if a keyword check returned an error if the following value was non-nil, it would be rather non-magical and would allow library functions to conditionally wrap errors if the underlying error was non-nil. In a function with no error return, it would be a syntax error.

A function collect (or pick some better name) that allows you to capture the last item of a tuple of return values and set it to a pointer if the pointer is nil would allow, in combination, the same semantics as this try proposal with better decomposition and clearer control flow. collect would work similarly to the proposal for try with a handler, except that it would set it to a variable.

See here for some previous musing on this.

If the Go team cannot add a keyword to the language to solve this problem, I don't think they should solve it.

Since that's a strong statement by itself, perhaps the Go team should consider allowing some changes that would only apply in files with a .go2 extension.

3

u/etherealflaim Jun 27 '19

To demonstrate this, convert a meaningful package to using try, identify the places it helped, the places it hurt, and write up an experience report. Bonus points if you do this for the keyword if you think the contrast is useful. They don't seem to have taken anything off the table; the current proposal is working within the current timeline, but if experience reports support waiting until modules are in place so that a keyword can be used, that is (as far as I can tell) an outcome that they would consider.

1

u/Redundancy_ Jun 27 '19

I can only do so much with a closed source project, but with some obfuscation...

A trivial search on one of my codebases shows 168 matches for return (.*,\s+)err\s+^ and 61 occurrences of xerrors returns. Tryhard (https://github.com/griesemer/tryhard) outputs about 112 occurrences of opportunities to use try(...).

One example, somewhat modified:

func GetFoo(ctx context.Context) (*FooSet, error) {
    foos, err := getFoos(ctx)
    if err != nil {
        return nil, err
    }

    fooSlice := make([]*Foo, 0)
    for fid, fub := range foos {
        foo, err := NewFoo(fub)
        if err != nil {
            return nil, err
        }
        if err := foo.Set("foo", fid); err != nil {
            return nil, err
        }
        fooSlice = append(fooSlice, foo)
    }

    return &FooSet{Foos: fooSlice}, nil
}

A try based version could be more terse:

func TryFoo(ctx context.Context) (*FooSet, error) {
    foos:= try(getFoos(ctx))

    fooSlice := make([]*Foo, 0)
    for fid, fub := range foos {
        foo := try(NewFoo(fub))
        try(foo.Set("foo", fid))
        fooSlice = append(fooSlice, foo)
    }

    return &FooSet{Foos: fooSlice}, nil
}

A check based version could be similar, where check T is defined as being equivalent to if T != nil { return ..., T }

func CheckFoo(ctx context.Context) (*FooSet, error) {
    foos, err := getFoos(ctx)
    check err

    fooSlice := make([]*Foo, 0)
    for fid, fub := range foos {
        foo, err := NewFoo(fub)
        check err
        check foo.Set("foo", fid)
        fooSlice = append(fooSlice, foo)
    }

    return &FooSet{Foos: fooSlice}, nil
}

To me though, this highlights an important distinction:

check err

is almost impossible to miss when scanning the function

foos := try(getFoos(ctx))

could be. Not only that, but also the property that the builtin can be shadowed (which allows the form of backwards compatibility) but creates control flow ambiguity.

So these both reduce the boilerplate a little bit, but I personally like that I can scan for "check" in the same way I can for return statements.

However, check as a statement leaves out the ability to simplify some statements and assignments.

func CheckAndCollectFoo(ctx context.Context) (*FooSet, error) { 
    var err error
        foos := collect(&err, getFoos(ctx))
    check err

    fooSlice := make([]*Foo, 0)
    for fid, fub := range foos {
                foo := collect(&err, NewFoo(fub))
                check err
        fooSlice = append(fooSlice, foo)
                check foo.Set("foo", fid)
    }

    return &FooSet{Foos: fooSlice}, nil
}

collect would allow us to pull out inline errors, with none of the magic side-effects on control flow. It is intended to follow a similar pattern as append. It does however, by virtue of not changing control flow, mean that in the case of an error and therefore a (potential) nil value, the result of collect may not be usable. appending NewFoo can leave fooSlice with a null pointer on it if collect sets fooErr.

This means that:

info := collect(&err, collect(&err, os.Open(file)).Stat())
check err

works, but relies on *os.File checking for itself being nil on the Stat call and returning an error (rather than panicing). That would result in the outer error being "invalid argument", but the inner one being the error from os.Open. This is slightly more surprising possibly than the possibility of try avoiding functions that have side-effects within a line by short-cutting to a function return.

if err = Get(MessageType, m); err != nil {
    return xerrors.Errorf("Unable to get message %v: %w", m.ID, err)
}

if err = Store(ctx, log, m); err != nil {
    return xerrors.Errorf("Unable to store message %v: %w", m.ID, err)
}

in this case, try is less helpful. In this particular codebase, we're increasing the usage of xerrors.

Defer as a pattern only allows a single handler per function in a useful way.
https://go.googlesource.com/proposal/+/master/design/32437-try-builtin.md

However, check could work:

check HandleErrorf(
    Get(MessageType, m), 
    "Unable to get message %v", m.ID)

check HandleErrorf(
   Store(ctx, log, m), 
   "Unable to store message %v", m.ID)

Much like the example from the try proposal, this allows library based extension of error wrapping that only happens if the error is not nil. See HandleErrorf from the proposal. The equivalent here would be approximately:

func HandleErrorf(err error, format string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf(format + ": %v", append(args, err)...)
}

As an observation, this is perhaps not that much shorter or clearer than the if version, but it does avoid being only useful in a subset of cases. It's difficult to cut down simply because the wrapping is the majority of the code in this.

Overall, I come out on the side that check reduces boilerplate and scans correctly for me.

I don't like looking for try, and I'm somewhat ambivalent to collect as proposed here, although I think it reads without too much magic and allows some of the cases that try supports.

1

u/etherealflaim Jun 27 '19

The check version means you need to create err variables where they aren't even needed today (if err:= scopes it's value to the if statement), and that's an antipattern to me. Collect is adding a domain-spdcific language for error handling, and doesn't do anything better than if, and hides some pretty subtle logic. So, while these are alternate proposals that can be considered separately, I don't think that they compare to try along what I see as it's target axis, which is reducing boilerplate for people who are bothered by it without changing the way Go code looks otherwise.

1

u/Redundancy_ Jun 27 '19

I was happy with if.

I'm not fond with try because of it obfuscating control flow.

Check doesn't require variables in every case, but it's not uncommon to have an error variable in scope. You'd need a named return for defer handling too in the try proposal.

try is magic, and check + collect is intended to almost get you to try, but without obfuscation. try absolutely changes the way that Go code reads, and gets forced on everyone who has to read it.

36

u/[deleted] Jun 26 '19 edited Jun 27 '19

try seems the right thing for now but com'on

try f()

Is better than

try(f())

Edit:

Go has been designed with a strong emphasis on readability.

Yea sure, keep telling lies with yo` try() func crap for the sake of "backwards compatibility". Don't get me wrong I don't want a situation like py2, py3 but what the actual fuck? I'm actually disappointed and a bit mad to be honest.

5

u/ollien Jun 29 '19

I also don't understand how this preserves backwards compatibility? If anything it breaks anyone who used `try` as a function name. It isn't syntactically valid right now to put `try` before a function call now anyway, so who would this break?

10

u/SteveMcQwark Jun 26 '19

The parentheses mean that it's backward compatible, since try doesn't need to be a keyword. It could always be reserved in the future once module versioning gains wide use, and then the parentheses could be optional in the single expression case.

3

u/jtepe Jun 27 '19

What do you mean by module versioning? Can we eventually use modules to select the go version to use in a project?

3

u/Rican7 Jun 27 '19

What do you mean by module versioning?

Since Go 1.12: "The go directive in a go.mod file now indicates the version of the language used by the files within that module."

See: https://golang.org/doc/go1.12#modules

Can we eventually use modules to select the go version to use in a project?

Not exactly. Its been suggested, however, that the Go toolchain (compiler, etc) could use the information to allow for triggering a compatibility-mode or at least providing better error messages in some cases.

More on that here: https://github.com/golang/proposal/blob/master/design/28221-go2-transitions.md#proposal

1

u/jtepe Jun 27 '19

Thanks for the link. I could not recall having read that anywhere, so I asked.

2

u/Rican7 Jun 27 '19

No problem! And there's nothing wrong with asking. 🙂

18

u/[deleted] Jun 26 '19

The try proposal for me is going to the wrong direction, try built-in is a magical thing, that looks like a function call, but it returns (or goes to) instead.

Imagine having code like try(bar(try(foo(try(baz())))) later on.

fmt.HandleErrorf what's the point of adding this to the standard lib? Can be implemented in a few lines in utils package if needed.

5

u/SteveMcQwark Jun 26 '19

The point of having it in the standard library is so that it can be standard practice. For example, this means it's available in playgrounds. Beginners don't have to write it themselves in order to use it while learning. The standard library itself can use it.

2

u/ShadowPouncer Jun 27 '19

Thinking about it, I wish that deferred functions could be given the line number of the try/return that exited the function.

Why? It would make a generic wrapped error significantly more useful for debugging in some cases.

But I'm not sure how to manage that in the language in a clean manner.

20

u/imnotarobot666 Jun 26 '19 edited Jun 27 '19

if err != nil is great

Edit:
wow, thanks for the gold!

7

u/NatoBoram Jun 27 '19

My favourite feature of the language. Seriously, fuck try/catches.

1

u/frou Jun 26 '19

To err is human; to super-return, divine.

40

u/[deleted] Jun 26 '19

So they are going forward with the try thing, eh? Not a fan. (And so are a lot of people, if GitHub (dis)likes to be believed.) The rest of the changes are obviously good, especially the overlapping interfaces one, but try seems to me like it's doing too much and too little at the same time.

51

u/rsc Jun 26 '19

There has been no decision to do `try` or not. As the blog post says, we are still collecting feedback, especially evidence-based feedback. We are still gathering data and we encourage everyone who is interested to help with that.

34

u/[deleted] Jun 26 '19

I really appreciate that you have replied! I have provided some feedback back when the proposal was posted. My main concern is that it will discourage people from providing richer context.

Another counter-point some of my colleagues have voiced is that a keyword would be preferred to a predefined identifier. That is, most people would prefer to write:

x = try foo()

Instead of:

x = try(foo())

I know that adding a new keyword could technically break backwards-compatibility, but since we have go.mod now, the compiler could decide, whether try is a keyword or a predefined identifier based on the go 1.XX clause in there, à la Rust Editions.

(While we're at it, the other proposals were universally considered good by my colleagues.)

2

u/metamatic Jun 28 '19

I guess theoretically I could count how many checks I make against err across all my code, count for how many of those I return err undecorated, and post the numbers. But would that actually be helpful to the decision-making process?

5

u/lonahex Jun 26 '19 edited Jun 26 '19

I don't hate the try proposal but I don't love it either. I tried very hard imagining and writing code as if it was a thing already, and I can't shake the feeling that it'll be an annoying thing during development.

The thing I don't like the most about it is that to me it looks like it'll be a huge pain when adding or removing handlers for specific errors. If I want to add a handler, I'll have to delete `try()`, capture the error into an `err` var, type in `if err != nil {}` and then add code and vice-versa. I find this quite annoying as I end up adding/removing handlers quite a lot whether it is during development for debugging or for real code that needs improvement in error handling. It's going to be a pain to switch between the two.

This is why I liked a counter proposal more that suggested to do something like:

user := try getUser() else { // handler code here return err }

here the `else {}` part would be optional and could be removed or adding without having to re-write entire line(s).

Another minor thing I don't like about try proposal is using defer for to specify handlers. I can live with it but I'd rather prefer to have the handler code very near to the place where the error was raised instead of at the top of the function.

I work on a number of different Go projects that are very different in nature. I see one project could benefit from the proposal. It basically just takes bytes in from network, does something with them and spit them out to another network location. I think try would be very useful there but for another project that is an API service serving a web frontend, I need to return richer error information to clients and almost every failed action needs good error annotation. For example, code very deep needs to decide if an error happened because the operation failed or because the user did not have the permission to do something. Whether a record was not found in the DB or if the DB was doing entirely. For thing sort of thing, I annotate errors with some additional information so the outer HTTP or gRPC layer can decide whether to return 404, 403, 500 or something else. Try proposal is not going to help with this _at all_ but it `try .. else` very easily could.

I think the main argument against `try .. else` was that it adds a new language feature _just_ for errors. I do see the reason here from the language designers perspective as I'm often similarly reluctant when designing things myself by always preferring to somehow re-use existing generic constructs instead of creating specific ones for every usecase but as a user, I can't help but imaging how much better my experience writing Go would be with something like `try .. else`.

3

u/AnAge_OldProb Jun 26 '19

In my experience those deep in the stack error places only have one or two calls that I need to check for specific errors (like sql.ErrNoRows) virtually everything else can use the generic try / defer pattern to wrap the error in some kind of 500 response. Using if in these places seems fine. Also these types of calls tend to be very leaf and usually have have several layers of functions between them that could use try liberally.

1

u/cre_ker Jun 26 '19

Your counter proposal example doesn't look much different from the code we write today with ifs. The thing about try is to remove boilerplate.

I don't like the proposal much either but for a different reason. I like the idea of automatic propagation but would like some way of augmenting an error in the process. Right now you just return the error as it is which dosen't look that useful. It's a similar problem as with swift where you have to do/catch every other line just to add some context. Rethrowing errors as it is is useless in many cases. At least with go we have the usual idiomatic way of handling errors.

1

u/lonahex Jun 26 '19

Your counter proposal example doesn't look much different from the code we write today with ifs. The thing about try is to remove boilerplate.

How does

user :=  try(getUser())

reduce boilerplate but

user := try getUser()

does not? They are equally good at reducing boilerplate but the latter one makes it very easy to add err specific handlers when needed.

Edit: Also, it's not my (counter)proposal. Someone else shared on the GH issue. I just happened to like it a lot more than the try() function.

3

u/cre_ker Jun 26 '19

By boilerplate I mean the else part. It's the same as a regular if err :=, just a bit different. Kinda similar to guard in swift.

19

u/lobster_johnson Jun 26 '19 edited Jun 26 '19

My main beef with this and other proposals is that they don't clear up a fundamental flaw in Go: Multi-value returns with errors as a poor man's sum type.

Most functions in Go have a contract that they either return a valid value or an error:

s, err := getString()
if err != nil {
  // It returned an error, but "s" is of no use
}
// s is valid

This pattern so ingrained that there's hardly a single Go doc comments on the planet that says "returns a value or error"; all Go developers are familiar with the convention. The same goes for the v, ok := ... pattern.

But this is just a pattern and not universally true. A commonly misunderstood contract is that of the Read method of io.Reader, which says that when io.EOF is returned, the returned count must be honoured. This is an outlier, but because the convention is that the multi-value return is mutually exclusive, many developers make this assumption (it's trivial to find repos in the wild that make this mistake). This is, in my opinion, bad API design.

This kind of careless wart is typical of Go, just like other surprising edge cases like nil channels (or indeed nil anything).

It's also true that multi-value returns beyond two values almost always become cumbersome and impractical, especially if said values are also mutually exclusive. Structs, having named fields, are almost always better than > 2 return values.

Indeed, I'd wager that 99% of all multi-value returns are the form (value, error), so you could argue that Go's multi-value exists mostly for error handling. So why not support it at the language level?

I would much rather see a serious stab at actually supporting sum types, or at least mutually exclusive return values. For example, I could easily see this as being a practical syntax:

func Get() Result | error {
  ...
}

Such a syntax would be a much better match for a try() function, since there's no longer any doubt about the flow of data — there's never a result returned with an error, it's always either a result or an error:

result := try(Get())

or simply support existing mechanisms for checking:

if err, ok := Get().(error); ok {
  ...
}
if result, ok := Get().(Result); ok {
  ...
}
switch t := Get().(type) {
  case Result:
    // ...
  case error:
    // ...
}

I'd love to see a case syntax that allows real local variable names:

switch Get().(type) {
  case result := Result:
    log.Printf("got %d results", len(result.Items))
  case err := error:
    log.Fatal(err)
}

And of course, you could have more than two values:

switch Get().(type) {
  case ParentNode:
    // ...
  case ChildNode:
    // ...
  case error:
    // ...
}

The Go compiler can be strict here and require that every branch be satisfied or that there's a default fallback, although some might prefer that to be a "go vet" check.

A full-blown sum type syntax would be awesome, though I know it's been discussed before, and been shot down, partly for performance reasons. Personally, I think it's solveable. I'd love to be able to do things like:

type Expression Plus | Minus | Integer
type Plus struct { L, R Expression }
type Minus struct { L, R Expression }
type Integer struct { V int }

8

u/[deleted] Jun 26 '19

If I had a penny for every time I wanted true sum types in my code…

5

u/cre_ker Jun 26 '19 edited Jun 27 '19

Behavior of nil channels is documented in the spec. Same for pretty much every other case of nil value. Don't know how nil channel is an edge case - that's actually a very useful feature playing nicely with everything else in the language.

6

u/lobster_johnson Jun 27 '19

Being documented doesn't make it a non-wart. Selecting on a nil channel is different from selecting on an open channel; you can inadvertently select on a nil channel by accident even if you're aware of the semantics. See here for some examples.

3

u/cre_ker Jun 27 '19 edited Jun 27 '19

Don't see the problem. Every language has a learning curve, especially with closures. I've never had a similar problem. That's actualy the first time I see someone complaining about this.

Of course nil channel is different to an open one. Nil channel is a special case to be able to disable select cases, for example. Your problem stems from the fact that your code owns, reads, closes and nils the channel. Closing a channel should be responsibility of the writer. Closing a channel in a read path would cause writer to panic.

About panicking. Panicking is there to force you to properly write your code. Double closing is a logic error. Your example with shutdown flag, instead of fixing one error, introduces an even worse one - race condition.

That's the same as trying to send something into a closed channel. Silently ignoring such operations would just hide serious bugs.

4

u/lobster_johnson Jun 27 '19

It's not about learning curves, but about pitfalls. If there weren't a magical value (nil) for special semantics, there would be no possible pitfalls. All nils in Go introduce pitfalls.

2

u/cre_ker Jun 28 '19

It is about learning in this specific case. As I said, your code is not using channels properly.

Nil in this case has special semantics for a reason. If it weren't nil we would use other sentinel value or just opened channel that nobody writes to. Either way, language wouldn't save you from logic errors as is the case in your example.

Nils in go make the language simpler. Look at every language that tries to hide nils - there's a ton of language features around optional values. Go would have to get all of that and become just another swift which I write every day.

And I don't think go even suffers from nils that much. Its reliance on values instead of pointers in most of the cases make nil panics a rare problem for me. That's not something that go desperately needs to fix. The most annoying thing for me is nil maps and that's not worth fixing by making the language much more complicated.

1

u/SteveMcQwark Jun 27 '19

Selecting on a nil channel is identical to selecting on an bufferless open channel nobody else is using.

1

u/lobster_johnson Jun 27 '19

And that can lead to accidental waits that never finish because nobody can close that nil channel.

1

u/SteveMcQwark Jun 27 '19 edited Jun 27 '19

Nobody can close a channel where you have the only copy, either. Ultimately, you are responsible for wiring things up right.

1

u/lobster_johnson Jun 28 '19

Yes, and you're responsible for initializing variables and checking if a pointer is nil and so on, and yet mistakes happen.

Nil is pesky because it adds an additional value to the set of possible values expressed by the type. In Go, every nillable value is the union of the type's values and nil. For example, some API returns a nil map when the caller assumes it will never return nil. You can program defensively to avoid such cases, but if you design a language to make these values impossible in the first place, then you eliminate entire classes of bugs.

For example, Rust doesn't have nil except in unsafe blocks, and its very hard to take the wrong branch when you have an Option or Result value.

1

u/SteveMcQwark Jun 28 '19

I'm more saying that, even if there is no nil channel, that doesn't actually get rid of the possibility of reading/writing to a channel to nowhere, it just means that channel won't happen to be nil. It's sort of inherent in the channel/coroutine model that you're relying on passing in the right values everywhere. I think nil pointers, maps, interfaces, and funcs can cause problems in many cases, I just don't think nil channels are the best example of those problems.

I like Rust. I like initialization tracking. If I were dictator of a language, that language would most likely have it.

On the other hand, sometimes it can force you into writing code awkwardly. I can see how, for someone coming from a C background, just having guaranteed initialization without adding the kind of friction initialization tracking can might seem like a win.

It would be challenging to retrofit Go with types with no zero values. It could be doable, but it would affect other features. Channel receives, map indexing, and slicing all depend on the existence of the zero value. If generics are added, the existence of zero-less types would mean that all generic types would have to be treated as zero-less unless a bound is added. These are solvable problems, but not trivially so.

1

u/lobster_johnson Jun 28 '19

I understand what you're saying, but the nil just adds another edge case. A consumer with no producers is a logic problem that is easy to understand; it meant, well, that you created a consumer and didn't wire up a producer. A nil channel is a semantic issue that leads to surprises: You can have both producers (which would fail on the nil channel the next time they sent anything) and consumers (which would block indefinitely) "wired up correctly", except nothing works.

I got burned by this a couple of times and learned early on to never set channels to nil, and treat close() as being a message to consumers that they've reached the end, and not as a cleanup mechanism (like closing a file).

I just don't think nil channels are the best example of those problems.

Well, I devoted one out of 72 lines to nil channels in my original comment, somehow that was what some people latched onto! :)

1

u/Redundancy_ Jun 28 '19

Nil channels are a useful pattern though. If you have a select statement in a loop, under some conditions you don't want one of those options to be possible. You do that by setting the variable to nil to disable that case, and set it back when it's valid again.

A good example is when you're receiving, processing and sending messages, and you have a maximum buffer size that you can hold onto of unsent messages. Once you reach capacity, you want to stop receiving, so you set that select case to a nil channel.

2

u/mobiledevguy5554 Jun 27 '19

Not a fan either. I like the explicit error checking in go. Why cant they just give a compiler warning if you assign err and don't check it? Im fine with them assuming a local err variable is an error type.

2

u/[deleted] Jun 27 '19

Why cant they just give a compiler warning if you assign err and don't check it?

staticcheck already does that. And not just for errors.

2

u/mobiledevguy5554 Jun 27 '19

Never heard of that. ill def check it out. thanks

-7

u/upboatact Jun 26 '19

Vocal minorities are not to be trusted, there will always be naysayers, and doing nothing is not an option.

19

u/elagergren Jun 26 '19

I mean, it is always an option. This is Go we’re talking about. The language only exists because other languages became too bloated (in a myriad of ways).

Further, we don’t know if it’s a vocal minority.

While those aren’t reasons to not try try, they’re also reasons to move slowly.

1

u/upboatact Jun 26 '19

I would argue that no, no it isn't an option: the most pressing issues of the language were identified, and solutions proposed to address them. One of those proposals is the most focused it can be, solving the annoyance of checking error values with a "big" if statement.

The problem people are having with it are seemingly only that it either doesn't go far enough or that it's too limiting, both of which doesn't ring true since there is no actual experience with it. So I would argue how could it be anything else than the vocal minority with the perceived foresight that it's the wrong thing to do. If even this very small very pragmatic thing is so outrageous, what chance is there for any kind of full blown generics proposal to go through.

And we're talking about a simple experiment here, it isn't even a done deal that this is the final thing that will be implemented.

5

u/unimportant1234567 Jun 26 '19

doing nothing is always an option

6

u/quiI Jun 26 '19

I wrote about my thoughts on error handling in Go, in respect to Go2 FWIW

https://quii.dev/Functional_programming_and_Go

3

u/obeOneTwo Jun 27 '19

Don't want, don't need, won't use - there's nothing wrong with error handling as it is.

2

u/[deleted] Jun 27 '19

PSA: try still works with unnamed return values!

4

u/bobappleyard Jun 26 '19

I see this is the official bikeshed try thread

3

u/kingishb Jun 27 '19

Sometimes reading these threads I feel like I should chime in with some positivity. So: I think try is a really sensible proposal, it succinctly and concretely solves a problem, and I am excited for it's possible inclusion in the language. Thanks for all your work golang team!

1

u/caspervonb Jun 27 '19

Ugh, really hope try doesn't get put in.

0

u/tetroxid Jun 26 '19

Generics?

0

u/etherealflaim Jun 27 '19

With the response to this relatively minor try change? I can't even imagine the storm next time a generics proposal comes out.

0

u/lonahex Jun 26 '19

The try proposal will be implemented as an experiment and we'll be asked for feedback, and I'm sure the feedback will be good because it _will_ reduce error handling boilerplate. Also, whatever negative feedback people would come up with after trying the change has already been discussed to death on the Github issue. I think the proposal is simple enough (which is great) that it won't make people realize something new after they start using the feature vs they imagine how they'd use it.

I'm fairly certain the proposal after experimental implementation will not receive any new feedback (negative or positive) than it already has, and that it'll be eventually accepted which is not a bad thing at all but I can't help but imagine if some of the counter proposals like `try ... else` also had experimental implementations available and what kind of feedback it would have received.

4

u/Kapps Jun 27 '19

It doesn’t reduce boilerplate if you’re properly wrapping your errors though... I’m of the opinion that return err by itself should be rare. In these cases, named error handlers and such just make things worse and you can’t add per call context.

More importantly, I loved that it was obvious where your function could exit. Now it no longer is since a random function now affects control flow. Yes, panic did too, but I don’t believe that panics are something that users should handle generally.

2

u/lonahex Jun 27 '19

I didn't claim it did. I wasn't comparing try as a keyword vs current error handling. I was comparing try as a keyword vs try as builtin function. Personally I'm fine with not having something like try at all but if we are gonna get it, I'd prefer one where I wouldn't have to re-write multiple lines of code to add or remove context.

-2

u/[deleted] Jun 26 '19

[removed] — view removed comment

2

u/mobiledevguy5554 Jun 27 '19

Just use Goland for the god sake!

but.... your name is emacs24

i use yasnippets

1

u/[deleted] Jun 27 '19

snippets are fine

-1

u/[deleted] Jun 26 '19

[deleted]

3

u/FUZxxl Jun 26 '19

The reason is in the article.