To me this is one of the most underrated qualities of go code.
Go is a language that I started learning years ago, but did't change dramatically. So my knowledge is still useful, even almost ten years later.
To me this is one of the most underrated qualities of go code.
Go is a language that I started learning years ago, but did't change dramatically. So my knowledge is still useful, even almost ten years later.
On the frontend side it's typically been much more difficult. There are tons of dependencies, everything depends on everything else, there are typically many new major releases, and things can break in pretty non-obvious ways.
It's not so much the language itself, it's the ecosystem as a whole. There is nothing in JavaScript-the-language or npm-the-package-manager that says the npm experience needs to be so dreadful. Yet here we are.
Like one dependency upgrading needing you to move to node v16 but doing so causes half your app to implode.
I don't really see this type of things in modern projects with other languages.
Not really, or at least not always.
I once tried to run some old Node project on Apple Silicon. It relied on some package that wanted to download binaries, but that ancient version didn't support Apple Silicon yet. Upgrading the package to an Apple Silicon supporting version required updating half of the other dependencies, and that broke everything. Eventually, I just gave up.
If you have a template to derive from or sufficient Go experience you'll be fine, but selecting from a grab bag of small libraries early on in a project can be a distraction that slows down feature development significantly.
I love Go but for rapid project development, like working on a personal project with limited time, or at a startup with ambitious goals, Go certainly has its tradeoffs.
I'm not saying you need to stay there, but if your project environment feels like Rails or Flask or whatever in your first month or two, you may have done something wrong.
The http router now handles path parameters and methods, so you might just pull in a library to run a middleware stack.
There is structured logging in the stdlib, which works with the existing log package for an easy transition.
The thing I’ve struggled with is structuring a project nicely, what with the way modules work, especially for services that aren’t exactly ‘micro’, and especially when the module and workspace system is still pretty unintuitive.
Both Flask and Golang's http package have simplicity and minimalism as their philosophy. Of course, most mature projects will eventually diverge from that. But both can start out as a single file with just a few lines of code.
For your database driver, just use pgx. For migrations, tern is fine. For the tiniest bit of sugar around scanning database results into structs, use sqlx instead of database/sql.
I wouldn't recommend using a testing framework in Go: https://go.dev/wiki/TestComments#assert-libraries
Here's how I do dependency injection:
func main() {
foo := &Foo{
Parameter: goesHere,
}
bar := &Bar{
SomethingItNeeds: canJustBeTypedIn,
}
app := &App{
Foo: foo,
Bar: bar,
}
app.ListenAndServe()
}
If you need more complexity, you can add more complexity. I like "zap" over "slog" for logging. I am interested in some of the DI frameworks (dig), but it's never been a clear win to me over a little bit of hand-rolled complexity like the above.A lot of people want some sort of mocking framework. I just do this:
- func foo(x SomethingConcrete) {
- x.Whatever()
- }
+ interface Whateverer { Whatever() }
+ func foo(x Whateverer) {
+ x.Whatever()
+ }
Then in the tests: type testWhateverer {
n int
}
var _ Whateverer = (*testWhateverer)(nil)
func (w *testWhateverer) Whatever() { w.n++ }
func TestFoo(t *testing.T) {
x := &testWhateverer{}
foo(x)
if got, want := x.n, 1; got != want {
t.Errorf("expected Whatever to have been called: invocation count:\n got: %v\n want: %v", got, want)
}
}
It's easy. I typed it in an HN comment in like 30 seconds. Whether or not a test that counts how many times you called Whatever is up to you, but if you need it, you need it, and it's easy to do.Only exception is you should use my migration library [0] instead of tern — you don't need down migrations, and you can stop worrying about migration number conflicts.
One other suggestion I'll make is you probably at some point should write a translation layer between your API endpoints and the http.Handler interface, so that your endpoints return `(result *T, error)` and your tests can avoid worrying about serde/typeasserting the results.
The go tools for managing DB schema migrations have always felt lacking to me, and it seems like your tool ticks all of the boxes I had.
Except for one: lack of support for CREATE INDEX CONCURRENTLY (usually done by detecting that and skipping the transaction for that migration). How do you handle creating indexes without this?
Long-running index creation is a problem for pgmigrate and anyone else doing “on-app-startup” or “before-app-deploys” migrations.
Even at moderate scale (normal webapp stuff, not megaco size) building indexes can take a long time — especially for the tables where it’s most important to have indexes.
But if you’re doing long-running index building in your migrations step, you can’t deploy a new version of your app until the migration step finishes. (Big problem for lots of reasons.)
The way I’ve dealt with this in the past is:
- the database connection used to perform migrations has a low statement timeout of 10seconds.
- a long-running index creation statement gets its own migration file and is written as: “CREATE INDEX … IF NOT EXISTS”. This definition does not include the “CONCURRENTLY” directive. When migrations run on a local dev server or during tests, the table being indexed is small so this happens quickly.
- Manually, before merging the migration in and deploying so that it’s applied in production, you open a psql terminal to prod and run “CREATE INDEX … CONCURRENTLY”. This may take a long time; it can even fail and need to be retried after hours of waiting. Eventually, it’s complete.
- Merge your migration and deploy your app. The “CREATE INDEX … IF NOT EXISTS” migration runs and immediately succeeds because the index exists.
I’m curious what you think about this answer. If you have any suggestions for how pgmigrate should handle this better, I’d seriously appreciate it!
The approach I’ve seen flyway take is to allow detecting / opting out of transactions on specific migrations
As long as you always apply migrations before deploying and abort the deploy if they time out or fail, then this approach is perfectly safe.
On the whole I think flyway does a decent job of making the easy things easy and the harder things possible - it just unfortunately comes with a bunch JVM baggage - so a Go based tool seems like a good alternative
I guess having a new up migration to cover the case is better, but its nice to have a documented way of rolling back (which would be the down migration) - without applying it programmatically. But it helps if other team members can see how a change should be rolled back ideally.
So the same as every other language that lacks these in the standard lib?
That is a highly opinionated grouping of tools that gets you to a poor persons version of a full stack framework that many in the Go community would flat out reject.
I chock it up to people enjoy writing the same boiler plate stuff over and over in their paid jobs where they have the time to do it.
To your point, I've got my own set of libraries that I think are The Best™ for all my reasons that at least keep me productive.
Echo for the routing... sqlc for the db interface... I actually _love_ gomega and ginkgo for BDD but it makes people actually angry so I settle for testify and for logging we have slog now but I usually grab the logging lib from charm bracelet for nice colorized, leveled logging.
1 - https://www.reddit.com/r/golang/comments/1anmoqg/what_go_lib...
Thanks for opening my eyes to this shortcoming.
Not affiliated in any way, but to me it’s better than most of the alternatives
Point being you’re probably going to spend those first five days evaluating the options. The “community” doesn’t know your taste or your needs. You have no idea what their goals are, or what the average skill level is. All of those things can make a big difference in selecting tech to build atop of.
Razor pages are very interesting as well. I haven't used them enough to have a solid opinion yet but I really liked them for quick server rendered pages.
On the other hand, I find it sad how terrible stdlib's flag library is. I'd love to have something like Python's argparse, which is not perfect but enough for most of the time. Go's flag doesn't even work well for small programs. It should've been more like spf13/pflag, but as we've often seen with Go, they went "screw decades-old conventions" and did something strictly worse.
A lot of people underrate that quality I feel. C had that quality but pushed it to neurotic levels where it would change so slowly even when it needed to do it faster. Other language in contrast change too fast and try to do too many things at once.
You do not often get credit for providing boring, stable programs that work. I hope they continue to do it this way and do not get seduced into overloading the language.
We have a lot of fast moving languages already, so having one that moves slowly increases potential choice.
I never had any issue. The program still compiles perfectly, cross-compiles to windows, linux and macos, no dependency issue, no breaking change in the language, nothing. For those use-cases, go is a godsend.