Live data from Hacker News

How I write HTTP services in Go after 13 years

grafana.com

231–240 of 259 posts

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

#231

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

I find your first point interesting, wouldn’t be that solved by context propagation and waiting for the server to shutdown? Thanks!

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

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

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

Love it, I called it entity factories https://bower.sh/entity-factories

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

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

Maybe this is heresy in the Go community, but this is a problem that automatic dependency injection solves.

https://dagger.dev/hilt/testing-philosophy.html

The biggest problem with the "pass nil for unused dependencies" approach is that when you modify some code to actually use one of those dependencies when it didn't before, you have to go back through every test and populate it.

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

#234
post #71
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…

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…

That’s a no for me, dog. Way too much code. I’d rather enforce a policy that config can’t be mutated.

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

#235

Earlier quoted context omitted.

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

OOP and Dry are compatible! I’ve actually done the thing that the above commenter suggests - create a base object with created on/by so that I never have to think about it. Whether or not you actually care about that, if you implement a descended of that object you’re going to get some stuff for free, and you’re gonna like it!

Nobody, ever, is claiming no abstractions are useful or worthwhile. The issue is DRY implies that you should always look for an abstraction to avoid repeating yourself. Trust me, that way lies madness. It should be “sometimes repeat yourself, based on enough context, consideration and experience”. But that’s not as snappy.

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

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

So far I like the commonly used approach in the Typescript community best: 1. Create your Schema using https://zod.dev or https://github.com/sinclairzx81/typebox or one of the other many libs. 2. Generate your types from the schema. It's very simple to create partial or composite types, e.g. UpdateModel, InsertModels, Arrays of them, etc. 3. Most modern Frameworks have first class support for validation, like Fastify…

This doesn’t solve the problem.

If I have a user type, inferred from a Zod schema:

> { username: string; email: string }

And a function which takes that type:

> storeUser(user: User)

There is absolutely nothing that guarantees that the user object has been parsed by Zod. You can simply:

> storeUser({ username: “”, email: “no” })

And Typescript will not shout at you.

The only way to comparably solve it with Typescript is to inject a symbol into the object during parsing which confirms it has been passed through the correct parser function.

Personally, I just do basic type parsing on input data (usually request data) and more strict parsing where constraints like “is this a valid username, is this a valid email” during output (usually sending to the database). What happens in between I/O doesn’t matter much in many projects (CRUD), and in the places it does you can enforce more rigidity.

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

#237

Earlier quoted context omitted.

Top of the hour again? Time for another Rust advertisement? The topic at hand is about preventing library users from doing things the library author didn't intended using the type system, not "what happens if a language has zero-values". Perhaps you are not able to comprehend this because you are hungry? You're not you when you are hungry. Grab a Snickers.

what happens if a language has zero-values, is that you can't "parse, don't validate". Maybe it's time for you to finally try rust? Or any other language without zero-values, since rust seems to irritate you in particular.

Don't worry, I have tried languages without zero-values. But they have nothing to do with the discussion that was taking place before the ad break. Now back to the show, you cannot prevent library consumers from doing things you don't intend without a compete type system. Rust does not have a complete type system. It leaves holes open for library consumers to do unexpected things and as such it has no relevance here. Sorry that your client's product isn't the be all and end all.

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

#238
post #45
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…

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.

The pattern sounds nice in theory, but very cumbersome since now you have to obsessively ensure you have NewX calls everywhere or some form of "validated bool". In the end, you're just validating in a roundabout way and calling it "parsing".

I personally find being robust to errors and having clear error messages is the best option.

Don't focus so hard on getting things right, but rather dealing with things when they go wrong.

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

#239
post #91

Earlier quoted context omitted.

It's not guaranteed at all, that's where go's zero-values come in. E.g. nested structs, un/marshaljson magic methods etc. How do you deal with that?

Every struct requiring its zero value to be meaningful is probably one of the worst design flaws in the language.

On the contrary, I've found striving to make zero values meaningful makes designs far more succinct and clearer.

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

#240

Earlier quoted context omitted.

So far I like the commonly used approach in the Typescript community best: 1. Create your Schema using https://zod.dev or https://github.com/sinclairzx81/typebox or one of the other many libs. 2. Generate your types from the schema. It's very simple to create partial or composite types, e.g. UpdateModel, InsertModels, Arrays of them, etc. 3. Most modern Frameworks have first class support for validation, like Fastify…

This doesn’t solve the problem. If I have a user type, inferred from a Zod schema: > { username: string; email: string } And a function which takes that type: > storeUser(user: User) There is absolutely nothing that guarantees that the user object has been parsed by Zod. You can simply: > storeUser({ username: “”, email: “no” }) And Typescript will not shout at you. The only way to comparably solve it with Typescript…

Well, I use ajv and they have ways of applying format validation, so not just saying: "this is a string", but rather, "this is a string and must be a valid domain name".

Now, if your complaint is rather that you can call whatever method and pass in your bogus data, I don't see the point in arguing that. It's your code, the only person who can stop you is you.

Post reply on HN