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.
In ProcessFile example,