If Go supported method overloading, you could actually write the types of those functions out. The first one is either a string or a regexp.Regexp, and the second one is one of four variations on an http.Handler, giving a total of 8 varieties of each function.
I decided that the sin of exposing an interface{} as a parameter was less egregious than the sin of multiplying Goji's API surface area by a factor of 8, but you'll be happy to know that passing a value of the wrong type causes the invocation of Get (Post, etc.) to fatally exit immediately. If you're defining all your routes in a single goroutine before calling goji.Serve() (which is probably the most common way to define routes), your application will crash before it even binds to the socket.
So, not quite as good as a guarantee enforced at compile time, but it'll have to do.
Don't get me wrong, I perfectly understand what interface{} is and why someone is tempted to use it. It just kills the type checking and converts your golang code to a compiled python, without all those nice static analysis things. Yes, API would be larger, but it would be compile time type-checked and you wouldn't depend on things like "well, it will most probably crash very early enough".
I totally agree with the parent: far better to have 8 methods doing the same thing with different names and statically-typed parameters, than 1 method taking interface{}s. Strong +1 to a change in API.
Huh. I'm not sure I saw that particular project during my search. It looks neat!
Without having looked in detail at httprouter, I think the most obvious reason it might not be sufficient is that it doesn't support regular expressions. This might not be a dealbreaker, but I'm fond of the occasional regex route, and I'd have to think long and hard about whether it's worth giving up for a faster router. And plus, I'm still not sure router speed actually matters for most applications.
In any event, I do have a long plane trip coming up, and I'm sure Goji will grow itself something at least slightly more efficient than a linear scan then. I'm think Goji's router and middleware stack are already zero-allocation, so it'll just be finding a way to binary search through routes.
Yeah, this is a good question, and the unfortunate answer is that it was an engineering tradeoff. I wrote a pretty long reply to cypriss (https://news.ycombinator.com/item?id=7632956) which I think covers this.
Actually, Goji grew out of a single deficiency in "pat": the fact that it does not have a standard way of defining request context.
The big use case here is how you'd write a middleware that did authentication (using API keys, session cookies, ???) and emitted a username for other middleware to consume. With net/http, you end up with a lot of coupling: your end handler needs to know about every layer of middleware above it, and you start losing a lot of the benefit of having middleware in the first place. With an explicit middleware stack and a universal interface for middleware contexts, this is easy: everyone can code to the same single context object, and instead of standardizing on weird bound variables (or a global locked map a la gorilla), you just need to standardize on a single string key and a type.
I think my ideal world would involve Go providing a map[string]interface{} as part of the http.Request struct in order to implement this behavior, but until we get that, I think Goji's web.C ("the context object") is the next best thing.
There's one other thing pat hacks around: the issue of how to pass bound URL variables to the resulting handler. At first I was a little grossed out at how pat did it, but I've sort of come to terms with it. I still think Goji's way is better, but I don't think it's the reason I wrote (or a reason to use) Goji.
I agree that 'context' and passing this context between middleware is the biggest missing item from the standard http+"pat".
This library (Goji) solves it with a map[string]interface{}. This works, but has the downside that you need to type-assert your values from this map if you want to use them.
I wrote a library a while ago (http://www.github.com/gocraft/web) that uses reflection so that your contexts can be strongly typed. Martini offers a similar approach.
Unless I'm not understanding this correctly, this sounds exactly like how middleware state is implemented in Ruby's Rack and it is one of the biggest warts of Rack that the core team is trying to fix.
Yeah, I looked at gocraft/web for a long time before writing Goji. It's a good library, and I think it does a lot of things right. But at the end of the day, just like my disagreement with Martini, this comes down to a difference in principles. I think there are two theories of Go library design here. One of us has chosen one, one the other, and let me prefix this by saying that I'm not sure the way I chose is correct.
On one hand, a library can allow its users access to typed structs—this is the way gocraft/web does it. The upside here is huge: applications are now type-safe, and can lean on the compiler to enforce their own correctness. The library code that backs this behavior can be arbitrarily gnarly, but you only have to write it once, after which it'll Probably Be Correct (tm).
On the other hand, you can provide a standard "universal" context type like Goji does. Since the library makes no statements about the sorts of fields you have or their types, applications have to do a bunch of shenanigans with type casts in order to extract the typed values they expect. The upside here, however, is the standardization and what that allows. I've found that in most of the web applications I've built, only a small fraction of the middleware is authored by me: I end up pulling in the standard request logging middleware, or the middleware that assigns request IDs, or the middleware that does CSRF protection. There's a huge amount of value in being able to write (read: "let someone else write") those middlewares once and being able to use them in any application. With arbitrary application-provided structs, this isn't possible [1], but if you can instead standardize on types and keys out-of-band, you can mix and match arbitrary middleware with impunity.
This alone is one of those awkward engineering tradeoffs where you're exchanging one nice thing (application-level type safety) for another (standard interface, and hopefully an ecosystem of drop-in middleware), and given just this I'd argue that the ecosystem is probably more important. But the frosting on this particular cake is that, with only marginally more awkwardness, you can actually get application-level type safety too: look at what I did for the request ID middleware that ships with Goji, for instance (https://github.com/zenazn/goji/blob/master/web/middleware/re...). Again, it's not quite as nice as a user-defined struct, but in the grand scheme of tradeoffs and sacrifices, I don't think it's so terribly bad.
So TL;DR, Goji chose standardization and composability in the design of its context object over application type safety, but if you're willing to write a few helper functions I don't think you end up losing the type safety anyways.
[1]: actually, come to think of it, with a lot of reflection and some struct tags you could probably pull it off. (Something like "struct { RequestID string `context:"reqid"`}", and the request ID assigning middleware would hunt around for a string field tagged "reqid"). But it seems like it'd introduce a lot of coupling between middleware and the application which I'm not sure will turn out well—any time a middleware needed a bit of extra storage, every application would have to change. In any event, if someone builds this I'd love to play around with it.
To be perfectly honest, I'm not sure it is better (I was hoping you would help me decide that!), and I wrote it mostly because every aspiring programmer writes a web framework at some point, and it was time I wrote mine (it was a lot of fun :) ).
But I think there's a good chance it is better.
First, I think one important difference is that Goji isn't full of magical reflection. If Go had support for method overloading, its entire interface is type-safe. In contrast, Martini does a lot of magical object injection, and it's not clear until runtime if your routes will even work, or what they'll even do, or where exactly the memory for them is coming from.
Second, I much prefer Goji's way of defining middleware. To me, middleware is like an onion (just like ogres!): each layer is a wrapper around the previous one. The way you write middleware in net/http is by wrapping the old http.Handler with a new one, and that's how I wanted Goji's middleware to work too. There's no magic "context.Next()", there's no magic dependency injection overrides, it's just http.Handlers all the way down.
Anyways, I'd like to know if you think I'm right: again, I'm really not sure this is actually better than Martini (or $YOUR_FAVORITE_FRAMEWORK), but I think it comes from a slightly different set of principles, and ones that I think are worth considering.
The fact that you're trying to stick with strong typing as much as possible is very appealing to me, and now I'm convinced to try to port my new project from Martini to Goji.
I think this is a good point to differentiate yourself on, and perhaps it should be included in your elevator pitch.
Thank you for the detailed explanation. I think using these slim web frameworks makes it easy to refactor your code or swap out the framework. I agree with some of the points you raised, but at this stage Martini has some very crucial features like model validations and sessions etc. I am pretty sure Goji gets those as time passes. Great work!
Blackboard isn't all bad—fixing many of its inadequacies was one of my first big side projects in high school.
There was one Blackboard dashboard unit that allowed you to embed arbitrary HTML, and I injected some JS onto the page that allowed you to set a background image, made clicking on links pop open new tabs (instead of whatever abomination of javascript they had), and all kinds of other little tweaks. I even got most of the way through writing a drag-and-drop module rearranger before I graduated. Maybe 60% of what I knew about JS and CSS at the time I learned through trying to add features to Blackboard through script injection.
Naturally, I wasn't the only one frustrated at Blackboard, and the script travelled by word-of-mouth to a pretty sizable chunk of the school. At that point, it was probably the most widely-used thing I'd ever built.
Any JS-injection-based software that's actually good enough for people to use is probably going to be impressive -- and horrifying. If you try writing some, you will know my words to be true.
Randomized routing isn't all bad. In fact, if Heroku were to switch from purely random routing to minimum-of-two random routing, they'd perform asymptotically better [1].
If Heroku had the data needed to do minimum-of-two random routing, they'd have the data needed to do intelligent routing. The problem is not the algorithm itself: "decrement and reheap" isn't going to be a performance bottleneck. The problem is tracking the number of requests queued on the dyno.
If Heroku had the data needed to do minimum-of-two random routing, they'd have the data needed to do intelligent routing.
Not strictly true; imagine that they can query the load state of a dyno, but at some non-zero cost. (For example, that it requires contacting the dyno, because the load-balancer itself is distributed and doesn't have a global view.)
Then, contacting 2, and picking the better of the 2, remains a possible win compared to contacting more/all.
See for example the 'hedged request' strategy, referenced in a sibling thread by nostradaemons from a Jeff Dean Google paper, where 2 redundant requests are issued and the slower-to-respond is discarded (or even actively cancelled, in the 'tiered request' variant).
I'm not familiar with minimum-of-two-random routing, but it does seem like assigning request to dynos in sequence would perform much better than assigning randomly (ie in a scenario with n dyno capacity, request 1 => dyno 1, request 2 => dyno 2, ... request n => dyno n, request n+1 => dyno 1, ..., repeat)
That'd be probably significantly better than the case of (request i => dyno picked out of hat) for all i
Harvard is seeing a very similar trend. Our intro to CS course, CS 50 [1], had an enrollment of over 600 this Fall (up about 20%), making it one of the largest classes at the College [2]. Even its teaching staff, which numbers over 100, is bigger than most classes (and most of them are undergraduates) [3].
The data isn't in for this term yet, but pre-term planning data suggests that enrollment in CS 51 (our second-term CS course) will go up by about 40% (it experienced similar growth last year), and CS 181, a machine learning class, might nearly triple in size since it was last offered [4].
For smaller departments like Harvard's (and Yale's, which is even smaller still), there just aren't enough professors to go around, and with the enormous influx of students, I think the quality and variety of the courses offered has suffered. Many courses are only offered sporadically--especially systems courses--since there simply aren't enough professors to teach them all.
And that's to say nothing of the (undergraduate) teaching assistants, which bear much of the brunt of this trend. I TFed Harvard's algorithms course (which doubled in size over the two years I taught it), and am TFing our operating systems class this term (which if today's turnout was any guide looks like it might nearly triple in size). The workload was considerable, and I'm sorry to say that the feedback and individual attention I was able to provide students has gotten worse and more tardy because of this.
I don't think Harvard (and Yale, etc.) can cope with this sort of explosive growth for too much longer.
I decided that the sin of exposing an interface{} as a parameter was less egregious than the sin of multiplying Goji's API surface area by a factor of 8, but you'll be happy to know that passing a value of the wrong type causes the invocation of Get (Post, etc.) to fatally exit immediately. If you're defining all your routes in a single goroutine before calling goji.Serve() (which is probably the most common way to define routes), your application will crash before it even binds to the socket.
So, not quite as good as a guarantee enforced at compile time, but it'll have to do.