[HN Gopher] How I write HTTP services in Go after 13 years
___________________________________________________________________
How I write HTTP services in Go after 13 years
Author : matryer
Score : 315 points
Date : 2024-02-09 19:00 UTC (3 hours ago)
(HTM) web link (grafana.com)
(TXT) w3m dump (grafana.com)
| ballresin wrote:
| What is the value making main.go as small as possible?
|
| Whose dreams come true in this scenario?
| sebastianz wrote:
| The initialization has to be done in a separate function that
| you call from the setup code for your end-to-end tests.
| janosdebugs wrote:
| If you do, you can use the application as a library and most of
| your code will also be easier to test.
| mattboardman wrote:
| Usually 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 wrote:
| "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 wrote:
| not 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 wrote:
| For 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 wrote:
| That'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 wrote:
| The 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 wrote:
| I'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 wrote:
| The 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.
| arccy wrote:
| i really like the patterns in this post, pretty much what i've
| also settled on after much experimentation with different styles.
| romantomjak wrote:
| Great 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 wrote:
| TLDR: optimize for unit tests and do DI with explicit function
| arguments. Looks kind of similar to Dropwizard.
| lelandbatey wrote:
| I 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 wrote:
| It 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.
| jbmsf wrote:
| I 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.
| matt_callmann wrote:
| is there a git repo with example code?
| mtlynch wrote:
| Not 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
|
| Particularly these files:
|
| https://github.com/mtlynch/picoshare/blob/2cd9979dab084ca781...
|
| https://github.com/mtlynch/picoshare/blob/2cd9979dab084ca781...
| karolist wrote:
| out of curiosity, why no sort-of-established pkg and internal
| dirs? What do you think of
| https://github.com/photoprism/photoprism structure?
| mtlynch wrote:
| I'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.
| mtlynch wrote:
| I 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.
| jxramos wrote:
| I'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 wrote:
| I 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.
| vjerancrnjak wrote:
| It means the object created by NewServer is dealing with too
| much. Probably has too many data types coupled to it and too
| much behavior.
|
| Simple example is adding a logger. If you add it as a
| dependency to the constructor, the object starts doing a bit
| more than initial simple implementation. It's fine to do it,
| but shame to not figure out how to log without editing the
| implementation of a simple thing.
|
| Higher order functions (a logger decorator) get there to allow
| composition, but even they have their drawbacks.
|
| It's still some form of structure that you can deal with, not a
| mistake.
| oogali wrote:
| I agree that too many arguments to the constructor may have
| the smell of too much coupling.
|
| But if I really feel I can't avoid the need to pass a good
| amount of external context, I create a dedicated "options"
| struct and pass that into the constructor as a pointer. The
| purpose of the pointer (rather than pass by value) is if I
| want default arguments, I can pass nil.
| type ServerOptions struct { logger
| *magic.Logger secretKey string }
| func NewServer(options *ServerOptions) (*Server, error) {
| ... }
| j1elo wrote:
| I think a useful iteration of that pattern is one called
| _Functional Options_ :
|
| * https://dave.cheney.net/2014/10/17/functional-options-for-
| fr...
|
| * https://github.com/uber-
| go/guide/blob/master/style.md#functi...
| sethammons wrote:
| I 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",
| 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.
| jopsen wrote:
| I've recently been playing with 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 wrote:
| The 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
| zemo wrote:
| > 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 wrote:
| I think that's a valid criticism. What do you think would be a
| more ergonomic pattern?
| Raynos wrote:
| I wrote a static config class that reads configuration for
| the entire app / server from a JSON or YAML file ( https://gi
| thub.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 wrote:
| I 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).
| tejinderss wrote:
| The keyword here is "mutable" config object and not config data
| object in general. I use immutable config dataclass liberally
| in one of my python projects and i pass it around in all
| modules. Many functions rely on multiple values and instead of
| passing all of them as function parameters (which requires
| their own function typings), the dataclass has all variables
| with typing definitions in one place, its pretty handy design
| pattern.
| gloryjulio wrote:
| I agree. We ran into sev by changing the top level config
| object before. You DO NOT want to modify it. The wasted man
| hour is not worth. You will never know where or how it get
| used. If you make changes it's better to derive from it
| instead.
|
| Update: What's funny was, in our design the config object was
| kinda immutable. You have to use the WARNING_DO_NOT_USE api to
| make modification. We did mutate the object and we caused a sev
| fnordlord wrote:
| I've tended to create a Config struct for each package and then
| a configs.Config struct that's just made up of each package's
| Config. It might not be a Go best practice but I like that I
| can setup the entire system's configuration on startup as one
| entity but then I only pass in the minimally required
| dependencies for each package. It also makes testing a little
| easier because I don't have to fake out the entire
| configuration for testing one package.
| MrDarcy wrote:
| My favorite way to prevent this is to make the config truly
| immutable, but still configurable with something like this:
| package config type options struct { name
| string } type Option func(o *options)
| func Name(name string) Option { return func(o *options)
| { o.name = name } } type
| Config struct { opts *options } func
| New(opts ...Option) *Config { o := &options{}
| for _, option := range opts { option(o) }
| return &Config{opts: o} } func (c *Config)
| Name() string { return c.opts.name }
|
| Use it with: cfg :=
| config.New(config.Name("Emanon")) fmt.Println(cfg.Name())
| mtlynch wrote:
| > _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-
| va...
| oorza wrote:
| Conceptually equivalent to the ancient arts of private
| constructors and factory methods.
| xboxnolifes wrote:
| Crazy that actually using your type system leads to better
| code. Stop passing everything around as `string`. Parse them,
| and type them.
| stickfigure wrote:
| There's a name for this anti-pattern: "Stringly typed"
| asp_hornet wrote:
| Now what? the username is in an unexported field and unusable?
| I can kind of see what its going for but it seems like a way
| just to add another layer of wrapping and indirection.
| pants2 wrote:
| It would need a getter here. Probably good to keep it
| immutable, if you want guarantees that it will never be
| changed to something that violates the username rules.
| asp_hornet wrote:
| > need a getter
|
| Yeah, thats what I figured. Im not sure if I want the
| tradeoff of calling .GetValue in multiple places just to
| save calling validate in maybe 2 or 3 places.
|
| Not to mention I cant easily marshal/unmarshal into it and
| next week valid username is a username that doesnt already
| exist in the database.
|
| Maybe this approach appeals to people and Im hesitant to
| say "that's not how Go is supposed to be written" but for
| me this feels like "clever over clear".
| cogman10 wrote:
| The issue is DRY often comes to wreck this sort of thing. Some
| devs will see "Hmm, Username is exactly the same as just a
| string so let's just use a string as Username is just added
| complexity".
|
| I've tried it with constructs like `Data` and `ValidatedData`
| and it definitely works, but you do end up with duplicate
| fields between the two objects or worse an ever growing
| inheritance tree and fields unrelated to either object shared
| by both.
|
| For example, consider data looking like Data
| { value string }
|
| and ValidatedData looking like ValidatedData
| { value int }
|
| There's a mighty temptation for some devs to want to apply DRY
| and zip these two things together. Unfortunately, that can
| really be messy on these sorts of type changes and the where of
| where validation needs to happen gets muddled.
| xboxnolifes wrote:
| Except Username is _not_ exactly the same as string, and that
| 's important. Username is a _subset_ of string. If they were
| equivalent, we wouldn 't need to parse/validate.
|
| The often misinterpreted part of DRY is conflating "these are
| the same words, so they are the same", with "these are the
| same concept, so they are the same". A Username and a String
| are conceptually different.
| cogman10 wrote:
| DRY is just "Do not repeat yourself". And a LOT of devs
| take that literally. It's not "Do not repeat concepts"
| (which is what it SHOULD be but DRC isn't a fun acronym).
|
| Unfortunately "This is the same character string" is all a
| DRY purist needs to start messing up the code base.
|
| I honestly believe that "DRY" is an anti-pattern because of
| how often I see this exact behavior trotted out or
| espoused. It's a cargo cult thing to some devs.
| oorza wrote:
| That's why I like to tell people to always remember to
| stay MOIST - the Most Optimal is Implicitly the Simplest
| Thing.
|
| When you add complexity to DRY out your code, you're
| adding a readability regression. DRY matters in very few
| context beyond readability, and simplicity and low
| cognitive load need to be in charge. Everything else you
| do code-style wise should be in service of those two
| things.
| hombre_fatal wrote:
| This seems less about DRY and more a story about a
| hypothetical junior dev making a dumb mistake
| masquerading as commentary about "DRY purism".
| cogman10 wrote:
| Man I wish it was just jr devs. I cut jrs a ton of slack,
| they don't know any better. However, it's the seniors
| with the quick quips that are the biggest issue I run
| into. Or perhaps senior devs with jr mentalities
| ikiris wrote:
| most srs are just jrs with inflated egos and titles
| sroussey wrote:
| Like everything, it depends is the right answer.
| devjab wrote:
| One of the major issues with a lot of the outdated concepts
| in programming is that we still teach them to young people. I
| work a side gig as an external examiner for CS students.
| Especially in the early years they are taught the same OOP
| content that I was taught some decades ago, stuff that I
| haven't used (also) for some decades. Because while a lot of
| the concepts may work well in theory, they never work out in
| a world where programmers have to write code on a Thursday
| afternoon after a terrible week.
|
| It's almost always better to repeat code. It's obviously not
| something that is completely black and white, even if I
| prefer to never really do any form of inheritance or
| mutability, it's not like I wouldn't want you to create a
| "base" class with "created by" "updated by" and so on for
| your data classes and if you have some functions that do
| universal stuff for you and never change, then by all means
| use them in different places. But for the most part,
| repeating code will keep your code much cleaner. Maybe not
| today or the next month, but five years down the line nobody
| is going to want to touch that shared code which is now so
| complicated you may as well close your business before you
| let anyone touch it. Again, not because the theoretical
| concepts that lead to this are necessarily flawed, but
| because they require too much "correctness" to be useful.
|
| Academia hasn't really caught on though. I still grade first
| semester students who have the whole "Animal" -> "duck",
| "dog", "cat" or whatever they use into their heads as the
| "correct way" to do things. Similar to how they are often
| taught other processes than agile, but are taught that agile
| is the "only" way, even though we've seen just how wrong that
| is.
|
| I'm not sure what we can really do about it. I've always
| championed strongly opinionated dev setups where I work. Some
| of the things we've done, and are going to do, aren't going
| to be great, but what we try to do is to build an environment
| where it's as easy as possible for every developer to build
| code the most maintainable way. We want to help them get
| there, even when it's 15:45 on a Thursday that has been full
| of shit meetings in a week that's been full of screaming
| children and an angry spouse and a car that exploded. And
| things like DRY just aren't useful.
| shrimp_emoji wrote:
| > _It's almost always better to repeat code._
|
| God no. Stop the copy pasta disease! It's horrible,
| mindless programming.
|
| When reviewing code, I'm astonished anything was
| accomplished by copy pasting so much old code (complete
| with bugs and comment typos).
|
| Incidentally, OOP encourages you to copy a lot. It's just
| an engine for generating code bloat. Want to serialize some
| objects? Here's your Object serializer and your overloaded
| Car serialize and your overloaded Boat serializer, with
| only a few different fields to justify the difference!
|
| OOP is bad. Copy pasta is bad. DRY is good. All hail DRY,
| forever, at any cost.
| skybrian wrote:
| This is a good design pattern, but be wary of doing validation
| too early. The design pattern allows you to do it as early or
| late as you like, but doesn't tell you when to do it. Often
| it's best to do it as part of parsing/validating some larger
| object.
|
| See Steven Witten's "I is for Intent" [1] for some ideas about
| the use of unvalidated data in a UI context.
|
| [1] https://acko.net/blog/i-is-for-intent/
| andrus wrote:
| I've found it hard to apply this pattern in Go since, if
| Username is embedded in a struct, and you forget to set it,
| you'll get Username's zero value, which may violate your
| constraints.
| 3pm wrote:
| AKA 'Value Object' from DDD or a similar 'Quantity' accounting
| pattern. Another angle is that this fixes 'Primitive Obsession'
| code smell.
| costco wrote:
| Just do type Username string
|
| And replace return Username{username}
|
| with return Username(username)
| daveFNbuck wrote:
| If you do that, people outside the package can also do
| Username(x) conversions instead of calling NewUsername.
| Making value package private means that you can only set it
| from outside the package using provided functionality.
| mtlynch wrote:
| The problem there is that you lose the guarantee that the
| parser validated the string value.
|
| A caller can just say: // This is returning
| an error for some reason, so let's do it directly. //
| username, err := parsers.NewUsername(raw) username :=
| parsers.Username(raw)
|
| You also get implicit conversions in ways you probably don't
| want like: var u Username u =
| "<hello>" // Implicitly converts from string to Username
| costco wrote:
| That's true I did not think of that.
| Scarblac wrote:
| > and a human-readable explanation of the issue is set as the
| value.
|
| This is annoying to translate later. At least also include some
| error code string that is documented somewhere and isn't prone
| to change randomly.
| klodolph wrote:
| I mean, you may end up just wanting something like,
| type UsernameError struct { name string
| reason string } func (e *UsernameError)
| Error() string { return fmt.Errorf("invalid
| username %q: %s", e.name, e.reason) }
|
| And reason can be "username cannot be empty" or "username may
| not contain '<'" or something like that.
|
| This is fine for lots of different cases, because it's likely
| that your code wants to know how to handle "username is
| invalid", but only humans care about _why_.
|
| I have personally never seen a Go codebase where you parse
| error strings. I know that people keep complaining about it
| so it must be happening out there--but every codebase I've
| worked with either has error constants (an exported var set
| to some errors.New() value) or some kind of custom error type
| you can check. Or if it doesn't have those things, I had no
| interest in parsing the errors.
| Scarblac wrote:
| I write mostly frontends. Sometimes the APIs I talk to give
| back beautiful English error messages - that I can't just
| show to the user, because they are using a different
| language most of the time. And I don't want to write logic
| that depends on that sentence, far too brittle.
| patrickkristl wrote:
| em ai have a problem from cars
| jupp0r wrote:
| I found 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.
| liampulles wrote:
| I 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.
| earthboundkid wrote:
| The validator should return map[string][]string so that a request
| can have multiple problems with one field.
| earthboundkid wrote:
| The sync.Once should be sync.OnceValues instead.
| bumpa wrote:
| The 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 wrote:
| I 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.
| sylware wrote:
| I 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).
___________________________________________________________________
(page generated 2024-02-09 23:00 UTC)