I don't see why check
should work like a unary operator and have
its own special syntax rule instead of acting like any function call.
Go doesn't have anything like the proposed check
, it has unary
operators but they are all symbolic (for example !
, unary *
and &
). It would be the first thing in the go language to look
like C's sizeof
.
Adding this new syntax makes parsing old code either impossible or context sensitive, for example:
check +a
Does this parse as check(+(a))
or as (check)+(a)
? In Go1 it's
the latter in Go2 it would have to be the former, unless the
parser first checks whether there is a check
identifier defined.
On the other hand there is plenty of precedent for special builtins
in Go: make
and new
both look like ordinary function calls
even though they could never be defined as such.
I argue that check
should be like make
or new
, not like
sizeof
.
I don't see any justification for why handler chains are necessary. IMHO requiring every handle block to terminate with a return is completely fine.
I can see how a handler chain would be useful in some circumstance but those are definitely a small minority and even in those cases I think the handler chain will make writing the code marginally easier at the expense of making reading the code harder.
Go has always been about sacrificing syntactic sugar in favor of readability. I would prefer to see no handler chains in Go2.
Going through the examples in the draft document:
func process(user string, files chan string) (n int, err error) {
handle err { return 0, fmt.Errorf("process: %v", err) } // handler A
for i := 0; i < 3; i++ {
handle err { err = fmt.Errorf("attempt %d: %v", i, err) } // handler B
handle err { err = moreWrapping(err) } // handler C
check do(something()) // check 1: handler chain C, B, A
}
check do(somethingElse()) // check 2: handler chain A
}
Turns into this without handler chains, which is clearer:
func process(user string, files chan string) (n int, err error) {
handle err { return 0, fmt.Errorf("process: %v", err) } // handler A
for i := 0; i < 3; i++ {
handle err { return 0, fmt.Errorf("process: attempt %d: %v", i, moreWrapping(err)) } // handler B
check do(something()) // check 1: handler B
}
check do(somethingElse()) // check 2: handler A
}
The SortContent
example already uses the single handler style and would be unchanged.
The ProcessFiles
example, turns into this:
type Error struct {
Func string
User string
Path string
Err error
}
func (e *Error) Error() string
func ProcessFiles(user string, files chan string) error {
e := Error{ Func: "ProcessFile", User: user}
handle err { e.Err = err; return &e } // handler A
u := check OpenUserInfo(user) // check 1
defer u.Close()
for file := range files {
handle err {
e.Err = err
e.Path = file
return &e
}
check process(check os.Open(file)) // check 2
}
...
}
Which is slightly more verbose but I think clearer.
In cases where code sharing between handlers in a chain is substantial I'd rather see it factored out into a helper function.
I think check and handle both become new keywords and will break any existing Go code that uses either of those to name a variable, constant, function, method, or type. They both are very generic keywords.
I agree with @mfhholmes that this has the same issues as try and catch. My first experience and annoyance with try and catch was within a single function in Python. You could not tell which of the 15 function calls actually caused the error. This will lead to lazy programming. The proposal seems to say the FAQ about exceptions got it wrong.