Chaining middlewares is basically just making the handlers of the chain call the next one, often based on a condition whether everything went well. Or in another approach some external mechanism may call handlers one-by-one.
However, all things come down to that the handlers will be called. The Handler.ServeHTTP()
method looks like this:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
A simple method with 2 parameters and no return values. The parameters are of type http.ResponseWriter
(an interface type) and *http.Request
(a pointer type).
So a call to a handler's ServeHTTP()
involves 2 things: making a copy of its arguments - which is fast since they are small, and actually making the call (taking care of stack update like create a new stack frame, record return address, save used registers, and execute the called function) - which is also very fast (see quote at the end of the answer).
So should you worry about calling functions? No. Will this be less performant compared to a handler which contains everything? Yes. Is the difference significant? No. Serving an HTTP request could take hundreds of milliseconds (network latency included). Calling 10 functions in your handler will not make it noticeably slower.
If you'd worry about the performance loss due to function calls, then your app would consist of one single main()
function. Obviously nobody wants that. You create functions to break down your initially large problem to smaller ones (recursively until it is "small enough" to be on its own) which you can oversee and reuse and test independently from others, and you assemble your large problem from the smaller ones. It's not really a question of performance but maintainability and reusability. Would you really want to copy that 100-line code which checks the user's identity to all your 10 different handlers?
One last thing. Should you be concerned about "consuming" the stack (resulting in a stack overflow error)? The answer is no. A goroutine starts with a small 4096 byte stack which grows and shrinks as needed without the risk of ever running out. Read more about it at Why is a Goroutine’s stack infinite? Also detailed at FAQ: Why goroutines instead of threads?
To make the stacks small, Go's run-time uses resizable, bounded stacks. A newly minted goroutine is given a few kilobytes, which is almost always enough. When it isn't, the run-time grows (and shrinks) the memory for storing the stack automatically, allowing many goroutines to live in a modest amount of memory. The CPU overhead averages about three cheap instructions per function call.