Instead of putting all your startup logic in main.go, consider a run function. Let's see why.
After writing Go for long enough, you'll become very familiar with main
functions that look like this.
Where we call a function, get back an error, check to see if it's OK or not, and log.Fatal
if it's not. We might get some other thing, do something with it. There's nothing necessarily wrong with this, but one challenge is that it's not very readable. There's a lot going on, and you have to walk through it to figure out everything that's happening. There's no sort of error propagation; we're just calling log.Fatal
on everything.
It's also hard to reason about adding more things to it, and it's not very testable. While you can test the main
function, it's quite challenging to do so. So one pattern that people have started to adopt as a community is creating a separate function called run
. For now, I'm going to do it like this, and we'll talk about some improvements we can make in a moment.
Effectively, what we do is move all of this bootstrapping logic into the run
function and return errors instead. Then our main
function can simply be called like this. So this probably doesn't look too different for now, but one thing we could do is start to propagate errors and add context. One improvement we might make is something like this: "do a thing" — errored. We've now given a little more logic to our function here as to what failed.
Also notice I exported this. So this is really testable now. I can import this anywhere and test it, which is great. Another improvement people like to make is to pass a context
into this function too.
Now, this isn't super useful in this example, but in future examples—like with an HTTP server or clients—this means you can propagate a context
all the way through as well, which is just really neat and looks much simpler to use. The main advantage of this approach is that all the logic is encapsulated inside the run
function. We've now centralized our error-handling concerns, and this is much simpler to test.
One final thing I'll call out is that this pattern works especially well if you're passing arguments from standard input, because now they become very easy to test in a unit test as well. So I hope you found this useful. You'll see this pattern quite widely used within Go, so I thought it was worth teaching you about. Let's go.