I'm using the net/http package and wondering how I can exit the handler from anywhere in the code. Say I have this code:
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request){
err := checkSomeThing(w, r)
if err != nil {
return
}
fmt.Println("End of Handler.")
return
}
func checkSomeThing(w http.ResponseWriter, r *http.Request) error{
http.Error(w, "Bad Request!", http.StatusBadRequest)
return errors.New("bad request")
}
Ideally I'd like to exit the handler from within the checkSomeThing function without having to return and then return again up a level, which will get worse as the application grows. This is purely for code readability.
CodePudding user response:
Use panic and recover following the pattern in the encoding/json package.
Define a unique type for panic:
type httpError struct {
status int
message string
}
Write a function to be used in a defer statement. The function checks for the type and handles the error as appropriate. Otherwise, the function continues the panic.
func handleExit(w http.ResponseWriter) {
if r := recover(); r != nil {
if he, ok := r.(httpError); ok {
http.Error(w, he.message, he.status)
} else {
panic(r)
}
}
}
Write a helper function for the call to panic:
func exit(status int, message string) {
panic(httpError{status: status, message: message})
}
Use the functions like this:
func example() {
exit(http.StatusBadRequest, "Bad!")
}
func someHandler(w http.ResponseWriter, r *http.Request) {
defer handleError(w)
example()
}
A more idiomatic approach is to check error returns up the call chain.
CodePudding user response:
My answer:
First, the common pattern established in Golang is to have errors "bubble up" from callee back to caller as a return value. It has a lot of advantages with regards to readability and re-use. The side effect is that there's a lot of if err != nil {return}
checks.
My suggestion if you really want to break from the norm
I'm going to pitch an idea, that I don't think is common or standard with respect to golang coding styles and patterns. But I didn't see anything online suggesting this was catastrophic. Let's see what I get in the comments to say this is awful.
You could use runtime.Goexit() to achieve what you want. The handler just waits on another goroutine to do the work. If the inner code running in the go-routine wants to abort processing, it can call Goexit(). It has the advantage that all defer
statements will still execute.
This just seems like a weak version of exception handling that Golang currently doesn't support. But I'm throwing it out there.
func handler(w http.ResponseWriter, r *http.Request) {
var cleanExit bool = false
var ch = make(chan bool)
// the actual handler implementation in a goroutine
go func() {
defer close(ch)
handlerImpl(w, r)
cleanExit = true // if handlerImpl invokes goExit, this line doesn't execute
}()
// wait for goroutine to exit
<-ch
if cleanExit {
fmt.Println("Handler exited normally")
} else {
fmt.Println("Hanlder was aborted")
}
}
func handlerImpl(w http.ResponseWriter, r *http.Request) {
checkSomeThing(w, r)
}
func checkSomeThing(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Request!", http.StatusBadRequest)
runtime.Goexit()
}
CodePudding user response:
If checkSomeThing()
is specific to that route, you should probably keep going with the code sample you pasted.
If checkSomeThing()
is a function common to all your routes (or to a subset of routes), you can choose a way to run a middleware before calling the handler for specific routes.
See for example this answer or this answer, or here is a way to do it using only code from the standard http package :
func checkSomething(...) error {
...
}
func WrapWithCheck(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
err := checkSomething(w, req)
if err != nil {
return
}
handler.ServeHTTP(w, req)
})
}
func setupRouter() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/foo/", handleFoo)
mux.HandleFunc("/bar/", handleBar)
mux.HandleFunc("/baz/", handleBaz)
// add your common call to 'checkSomething' here :
handler := WrapWithCheck(mux)
return handler
}
note : I tried using httptest
in the playground above, and for some reason it deadlocks in the playground. It works fine if you copy/paste this code in a sample.go
file and use go run sample.go