43 ms·
How I write HTTP services in Go after 13 years
- ballresin 3y agoWhat is the value making main.go as small as possible? Whose dreams come true in this scenario?
- sebastianz 3y agoThe initialization has to be done in a separate function that you call from the setup code for your end-to-end tests.
- janosd 3y agoIf you do, you can use the application as a library and most of your code will also be easier to test.
- mattboardman 3y agoUsually your main function can't be used by any other part of your program. You should move all component implementations to modules so they can be re-used elsewhere.
- mosselman 3y ago“Whose dreams come true in this scenario?” I love this! I will use this as well. There are so many situations where I have a feeling that people are solving problems that don’t exist. In code I run into at work, code and projects I see online, etc The “whose dreams are you making come true” really applies here, because dreams are exactly what they are. I spent quite some time writing an automatic image resizer and optimiser for my blog. Does it matter? No! Should I have spent that time writing blog posts instead? Yes! Still I was chasing some dream. Thanks for this image
- arccy 3y agonot main.go but func main. This allows your run function to return an error and you only need to deal with the abruptness of using os.Exit once
- pphysch 3y agoFor bespoke internal services, I like to keep main.go as flat as reasonable, like a "script". Handlers can have their own files but the bulk of the control flow and moving parts should be apparent from reading the main file. Abstracting things away from main makes it less readable and is general pointless for bespoke services that will be deployed in exactly one configuration.
- donio 3y agoThat's a nice way of putting it. When exploring a new codebase for the first time it can be very helpful to have main.go give you a high level idea about the overall structure of the program.
- abuani 3y agoThe author goes on to explain a few scenarios where the pattern is helpful. It's not to keep main.go as small as possible, it's so that you can test parts of your main.go file properly. In my experience, if all of my logic is stuffed into `func main() {}`, then I can't actually test it. If I have a helper method(like run in this case), I can test out specific scenarios and ensure the application handles it properly. Some of the examples Mat gave were handling context cancellations properly.
- jrockway 3y agoI've never been a fan of making main.go one line. I create the logger, parse the flags, create objects from the flags, and call Run() or something. In the tests, you aren't ever going to do those things in the same way, so there is really no point in putting them in some other file.
- dilyevsky 3y agoThe idea is to keep the untestable code as small as possible but in practice you just add a layer of indirection and all of your untestable init code is in a different castle.
- perbu 3y agoI assume you mean main() and not main.go. main() is the only place where you can't return an error. In order to keep as much of the code as idiomatic as possible you just call something like run() where you can do so. In addition there is the testing aspect. You can't invoke main() from your tests.
- arccy 3y agoi really like the patterns in this post, pretty much what i've also settled on after much experimentation with different styles.
- romantomjak 3y agoGreat article with lots of interesting ideas. Can't believe I didn't know about signal.NotifyContext. Finally I'll be able to actually rememeber how to respond to signals instead of copy-pasting that between projects.
- sesm 3y agoTLDR: optimize for unit tests and do DI with explicit function arguments. Looks kind of similar to Dropwizard.
- matt_callmann 3y agois there a git repo with example code?
- mtlynch 3y agoNot OP, but I design my Go projects with a very similar pattern that I learned from OP's 2018 post. I think this is a pretty good example of a real-world implementation: https://github.com/mtlynch/picoshare https://github.com/mtlynch/picoshare Particularly these files: https://github.com/mtlynch/picoshare/blob/2cd9979dab084ca781e125f725e48297b6e183c1/handlers/server.go https://github.com/mtlynch/picoshare/blob/2cd9979dab084ca781... https://github.com/mtlynch/picoshare/blob/2cd9979dab084ca781e125f725e48297b6e183c1/handlers/routes.go https://github.com/mtlynch/picoshare/blob/2cd9979dab084ca781...
- karolist 3y agoout of curiosity, why no sort-of-established pkg and internal dirs? What do you think of https://github.com/photoprism/photoprism https://github.com/photoprism/photoprism structure?
- mtlynch 3y agoI'm not familiar with that package structure, unfortunately. It might be good, but I'm not sure what the reasons are for structuring the project that way.
- lelandbatey 3y agoI want to see a greater acceptance of this idea: > My handlers used to be methods hanging off a server struct, but I no longer do this. If a handler function wants a dependency, it can bloody well ask for it as an argument. No more surprise dependencies when you’re just trying to test a single handler. For HTTP services in any language, your handlers will usually end up with a lot of business logic, logic which probably has many dependencies. I see single handlers using all of the following on a regular basis: DB, cache, blob storage, some kind of special authz thing specific to your endpoints, maybe some fancy licensing checker, a queue or two, a specialized logger, and specialized metrics client. Many of those (metrics, request/response logging) can live in middlewares most of the time, but in every code base there will be times where you need to do something custom with one or the other. As time passes, the more I wonder "why aren't these all just function parameters?" Yes, that would be a lot of function parameters (9+ for a single handler, before even getting into the request or custom params themselves), and we all have many rules of thumb and linter rules which try to keep us from having lots of function parameters. But it's not like we're not writing code which depends on all those dependencies, instead we're just sticking them on the "server" class/struct and pretending that because the method signature is shorter, we have fewer dependencies! As time passes, I find myself wishing more and more for code that takes all its dependencies in the function/method signature, even if there's 20 of them; at least then we wouldn't be lying about how complex the code's getting...
- wereHamster 3y agoIt doesn't have to be 9+ separate arguments, in some languages it can be a single 'context' or 'env' object that contains just what the handler needs, something like `handleHello({ db, cache, blobStore, authz }, req, res)`. That way, if two handlers use the exact same context you can reuse, but it's also easy enough to declare a per-handler context at the call site.
- topicseed 3y agoI've always have my handlers individually set as a struct each with a method to handle the route/request. type CreateUser struct { store storage.Store cache caching.Cache logger logging.Logger pub events.Publisher // etc } func (op CreateUser) ServeHTTP(ctx, req, rw) {} // or if you have custom handlers func (op CreateUser) ServeHTTP(ctx, input) (output, error) {} And in my main.go, or where I set up my dependencies, I create each operation, passing it its specific dependencies. I love that because I can keep all the helper methods for that specific operation/handler on that specific struct as private methods. It does get tedious when you have one operation needing another, as you might start passing these around or you extract that into its own package/service.
- jbmsf 3y agoI don't write go, but I like these patterns. Feels fairly universal for testable code. I never want to see another (esp. Python) Quick Start guide that treats dependencies as implicit/static/untestable.
- mtlynch 3y agoI really like Mat Ryer's work, and I've applied most of the ideas in the 2018 version of this article to all of my Go projects since then. The one weak spot for me is this aspect: >NewServer is a big constructor that takes in all dependencies as arguments... In test cases that don’t need all of the dependencies, I pass in nil as a signal that it won’t be used. This has always felt wrong to me, but I've never been able to figure out a better solution. It means that a huge chunk of your code has a huge amount of unnecessary shared state. I often end up writing HTTP handlers that only need access to a tiny amount of the shared state. Like the HTTP handler needs to check if the requesting user has access to a resource, and then it needs to call one function on the datastore. I'd love to write tests where I only mock out those two methods, but I can't write simple tests because the handler is part of this giant glob where it has access to all of the datastore and every object the parent server has access to because it's all one giant object. Nothing against Mat Ryer, as his pattern is the best I've found, but I still feel like there's some better solution out there.
- deleted 3y ago[deleted]
- jxramos 3y agoI've become increasingly sensitive to these high afferent coupling points in the repos I work on, especially the deeper I embed into the world of bazel and how dependency management and physical design influence the code I author. Where possible plugins are a great strategy to lay down these code seam points that don't force all possibilities upon some body of code, because fundamentally with plugin architectures you pick and choose what you want. Plugins are opt out by default, you must explicitly opt into a plugin for it to manifest. I've been calling software that has this quality going as being an "a la carte" style. But in general you do what you need to do to avoid "doing everything so you can do anything".
- zer00eyz 3y agoI tend to write most of my logic in packages... so a "users" package or a "comments" package (if we were building HN). These have NO http interface! They do however each have their own "main" and some sort of CLI interface: "//go:build ignore" in the comment of that file is your friend.
- sethammons 3y agoI like a lot of what they've done here. My testing looks a bit different however. srv, err := newTestServer() require.NoError(t, err) defer srv.Close() resp, err := http.Post(fmt.Sprintf("http://localhost:%d/signup/json http://localhost:%d/signup/json", srv.Port()), "application/json", strings.NewReader(` {"email":"test@example.com", "password": "p@55Word", "password_copy": "p@55Word"} `)) In my newTestServer, I spin up a server with fakes for my dependencies. If I want to test a dependency error, I replace that property with a fake that will return an error. I can validate my error paths. I can validate my log entries. I can validate my metric emission. I can validate timeouts and graceful shutdowns. After the server starts, I inspect to determine which port it is running on (default is :0 so I have to wait to see what it got bound to). My "unit" tests can test at the handler level or the http level, making sure that I can fully test the code as the users of my system will see it, exercising all middleware or none. I can spin up N instances and run my tests in parallel.
- zemo 3y ago> func NewServer(... config *Config ...) http.Handler one of my biggest pet peeves is when people take a Config object, which represents the configuration of an entire system, and pass it around mutably. When you do that, you're coupling everything together through the config object. I've worked on systems where you had to configure the parts in a specific order in order for things to work, because someone decided to write back to the config object when it was passed to them. Or another case was where I've seen it such that you couldn't disable a portion of the system because it wrote data into the config object that was read by some other subsystem later. The pattern of "your configuration is one big value, which is mutable" is one of the more annoying patterns that I've seen before, both in Go and in other languages.
- doh 3y agoI think that's a valid criticism. What do you think would be a more ergonomic pattern?
- Raynos 3y agoI wrote a static config class that reads configuration for the entire app / server from a JSON or YAML file ( https://github.com/uber/zanzibar/blob/master/runtime/static_config.go https://github.com/uber/zanzibar/blob/master/runtime/static_... ). Once you've loaded it and mutated it for testing purposes or for copying from ENV vars into the config, you can then freeze it before passing it down to all your app level code. Having this wrapper object that can be frozen and has a `get()` method to read JSON like data make it effectively not mutable.
- doh 3y agoI use similar pattern myself. Was curious if the OP is using some other, like for instance splitting the struct into two (im/mutable) and then passing them around, or what. BTW kudos on zanzibar. Love the tech and the code).
- zemo 3y agoI just use a struct literal, and then I have the type define a `func (t *Thing) ready() error { ... }` method and call the ready method to check that its valid. I prefer this over self-referential options, the builder pattern, supplying a secondary config object as a parameter to a constructor, etc.
- mtlynch 3y ago>The Valid method takes a context (which is optional but has been useful for me in the past) and returns a map. If there is a problem with a field, its name is used as the key, and a human-readable explanation of the issue is set as the value. I used to do this, but ever since reading Lexi Lambda's "Parse, Don't Validate," [0] I've found validators to be much more error-prone than leveraging Go's built-in type checker. For example, imagine you wanted to defend against the user picking an illegal username. Like you want to make sure the user can't ever specify a username with angle brackets in it. With the Validator approach, you have to remember to call the validator on 100% of code paths where the username value comes from an untrusted source. Instead of using a validator, you can do this: type Username struct { value string } func NewUsername(username string) (Username, error) { // Validate the username adheres to our schema. ... return Username{username} } That guarantees that you can never forget to validate the username through any codepath. If you have a Username object, you know that it was validated because there was no other way to create the object. [0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
- oorza 3y agoConceptually equivalent to the ancient arts of private constructors and factory methods.
- Cthulhu_ 3y agoWhich (in Java) were then abstracted away in... interesting annotations.
- xboxnolifes 3y agoCrazy that actually using your type system leads to better code. Stop passing everything around as `string`. Parse them, and type them.
- stickfigure 3y agoThere's a name for this anti-pattern: "Stringly typed"
- jopsen 3y agoI've recently been playing with ogen: https://github.com/ogen-go/ogen https://github.com/ogen-go/ogen Write openapi definition, it'll do routing, definition of structs, validation of JSON schemas, etc. All I need to do is implement the service. Validating an integer range for a querystring parameter is just too boring. And too easy to mistype when writing it manually. Anyways, so far only been playing, so haven't found the bad parts yet.
- dilyevsky 3y agoThe problem with this approach is writing openapi by hand from scratch is incredibly tedious process. Writing Protobufs, capnproto or any such similar idl feels much more productive
- xyzzy123 3y agoIts a bit icky but LLMs / copilot can speed up the creation of openapi specs a lot. Agree it doesn't fix the "root" problem that the overall syntax is not ergonomic.
- jopsen 3y agoMy point was that writing an openapi, or other IDL is faster than writing the code to manually do these things. And more accurate than LLMs. Feels like whenever an LLMs could code it, you'd be better of not having the boilerplate code at all.
- xyzzy123 3y agoAgree 100% with all points. I love contract-first. Better for producers and provides multiple tooling options and better docs for consumers. I was agreeing with parent that some spec formats frankly suck to write by hand and openapi yaml is IMHO one of those (as opposed to say .protos which are nice for humans to read/write). I use LLM as glorified interactive autocomplete to speed up writing the yaml specs (not the code! Use deterministic generators for that!) and it works great for me, personally (n=1 anecdote).
- jupp0r 3y agoI found fx(https://github.com/uber-go/fx https://github.com/uber-go/fx) to be a super simple yet versatile tool to design my application around. All the advice in the article is still helpful, but it takes the "how do I make sure X is initialized when Y needs it" part completely out of the equation and reduces it from an N*M problem to an N problem, ie I only have to worry about how to initialize individual pieces, not about how to synchronize initialization between them. I've used quite a few dependency injection libraries in various languages over the years (and implemented a couple myself) and the simplicity and versatility of fx makes it my favorite so far.
- Philip-J-Fry 3y ago>All the advice in the article is still helpful, but it takes the "how do I make sure X is initialized when Y needs it" part completely out of the equation and reduces it from an N*M problem to an N problem, ie I only have to worry about how to initialize individual pieces, not about how to synchronize initialization between them. I gotta say, I hate these dependency injection frameworks. In a well designed system this should be trivial. Making sure something is initialised when you want to use it is just a matter of it being available to pass in a constructor as a parameter. stockService := NewStockService() orderService := NewOrderService() orderProcessor := NewOrderProcessor(stockService, orderService) There shouldn't be any sort of "synchronisation" of initialisation needed because your code won't compile if you do something wrong. If you add a cyclic dependency you will clearly see that because you won't be able to construct things in the right order without an obvious workaround.
- jupp0r 3y agoIf you have ever topologically sorted 100 components connected in a complex graph by hand or found the right spot to insert the 101st, you'd quickly appreciate more help than a compiler check.
- therealdrag0 3y agoI’m sure there’s a place for them. But when micro-services are so common, it seems like people use them (Spring) because everyone else does, not because they actually provide needed value.
- liampulles 3y agoI agree with a lot of this, I'll add my own opinions: * I would pass a waitgroup with the app context to service structs. This way the interrupt can trigger the app shutdown via the context and the main goroutine can wait on the waitgroup before actually killing the app. * If writing a CLI program, then testing stdout, stdin, stderr, args, env, etc. is useful. But for an http server, this is less true. I would pass structured config to the run function to let those tests be more focused. * I disagree with parsing templates using sync.Once in a handler because I don't think handlers should do template parsing at all. I would do this when the app starts: if the template cannot be parsed, the app should not become ready to receive any requests and should rather exit with a non-zero exit code.
- hyeomans 3y agoI find your first point interesting, wouldn’t be that solved by context propagation and waiting for the server to shutdown? Thanks!
- liampulles 3y agoIf you just have a context, than your app cannot kill itself and the environment has to do it. That is better than nothing, but having the app do the killing is advantageous because: A) it can die faster (and so you can e.g. do your blue-green rollout faster) and B) you can write a log to say that your app is finished shutting down all its components, which can be useful for troubleshooting if your app was mid-transaction when it was killed.
- Eransbens 3y ago[flagged]
- Jakesjeff 3y ago[flagged]
- Eransbens 3y ago[dead]
- earthboundkid 3y agoThe validator should return map[string][]string so that a request can have multiple problems with one field.
- earthboundkid 3y agoThe sync.Once should be sync.OnceValues instead.
- bumpa 3y agoThe encode example contains a bug and a lint issue. Firstly, calling w.Header().Set after w.WriteHeader is likely a bug, as the w.WriteHeader method call should occur after setting the headers. The second issue involves passing an unused *http.Request, which will likely cause the linter to flag it.
- Animats 3y agoI just run Go servers under fcgi. You get orchestration and crash recovery with a very simple interface. Fcgi will launch server processes as needed, feed them events, and shut it down when there's no traffic. Performance is good, and you can run on cheap hosting.
- chubot 3y agoWhich hosting do you use? I use fastcgi with python on Dreamhost and it works fine, but I’m sorta worried that they’ll turn it off because it seems kind of niche and under-documented
- Animats 3y agoDreamhost too. Dreamhost will let you run a continuously running process. The amount of work you can get done on low-end shared hosting is really quite impressive.
- sylware 3y agoI did write my own HTTP stuff in C (and more generally internet stuff), on linux (sometimes without a libc, namely direct syscalls), running on ARM64 and x86_64. And I plan to move to rv64 assembly once I can get reasonably performant hardware (it is already here, but it extremely hard to get some where I am from and how I operate). I dunno if it will be bare metal or with a linux kernel first (coze a minimal TCP stack is already a big thingy).
- Linda231 3y ago[dead]
- Donaldoben 3y ago[dead]
- Jakesjeff 3y ago[dead]
- Giovanniamien 3y ago[flagged]
- Castellanavito 3y ago[dead]
- jurschreuder 3y agoThis is 100% not how I write it. Only thing I agree on is putting all the paths in one file. In most other programming languages I've done a lot of research how to make it nice and clean. Was hoping this was it for Go because I'm cleaning up a big project. But my very basic no nonsense current setup seems better to me than this in many ways. If anybody has another example that is a lot better (and I don't mean complexer I don't have those ego issues), I am very interested. But this I want to hide as best as I can from my dev team this is all wrong. It's clever in a lot of ways but it's wrong. It does not have unit testing at all, all these tests would be duplicated in the end-to-end test. I also like end-to-end tests better but why put them here, way better to put them in postman for example then you have the most up to date documentation always auto-generated. Passing the config, man I had so many discussions with junior developers about this, don't do that you'll make things dependent on the config and cannot reuse them in other programs. But that was already mentioned a lot here. There are also a lot of functions with like 10 arguments passed. If you have that many arguments just pass a stuct containing a lot of the arguments it's always super confusing when people make functions with 12 arguments. I'm always counting them an after 14 times counting I rewrite their function. It's a matter of style so keep doing it this way if you like it, but it's not my style at all it makes no sense at all to me. If anybody knows a better example please tell me.
- samkl 3y agoDon't worry, you will get there with more experience.