Live data from Hacker News

How I write HTTP services in Go after 13 years

grafana.com

21–30 of 259 posts

Re: How I write HTTP services in Go after 13 years

#21

What is the value making main.go as small as possible? Whose dreams come true in this scenario?

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.

Re: How I write HTTP services in Go after 13 years

#22
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.

Re: How I write HTTP services in Go after 13 years

#23
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.

Re: How I write HTTP services in Go after 13 years

#24
> 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.

Re: How I write HTTP services in Go after 13 years

#25
>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...

Re: How I write HTTP services in Go after 13 years

#26
post #24

> 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…

I think that's a valid criticism. What do you think would be a more ergonomic pattern?

Re: How I write HTTP services in Go after 13 years

#27
post #25

> 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 check…

Conceptually equivalent to the ancient arts of private constructors and factory methods.

Re: How I write HTTP services in Go after 13 years

#28
post #19

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 ab…

[deleted]

Re: How I write HTTP services in Go after 13 years

#29
post #26
post #24

> 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…

I think that's a valid criticism. What do you think would be a more ergonomic pattern?

I 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_... ).

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.

Re: How I write HTTP services in Go after 13 years

#30
post #19

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 ab…

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".

Post reply on HN