An unexpected journey into the documentation of go, git, and shell.
While working with git, the most popular version control system, you’ve probably committed or even pushed to remote repository files with errors that could have been easily found and solved. I’ve most definitely done that… a lot. Until I got my first job as a software engineer. While working with Node.js my team’s using husky, an npm package that helps you manage git hooks. I sincerely recommend using it when working with Node.
A little while ago I started learning the Go programming language, which btw is superfun! I didn’t have my favorite npm package — husky with me, though. I decided that as a proof of concept I will write a simple pre-commit hook by myself.
TL;DR you can check out my pre-commit hook for go files here.
The whole process consisted mostly of googling the right things, so it wasn’t that hard. It was fun, but it took some time until I finally got it working as expected. That is why I want to share with you this story.
Follow me along to write your
pre-commit hook step by step. I am assuming that you have a foundation of git, Unix shell, and go. Also, that you have git and Go installed, and
$GOPATH/bin is inside your
$PATH environmental variable. I am not using
We will start by creating a new git repository but you can also manipulate your git hooks inside the already existing one.
$ mkdir $GOPATH/src/precommit
$ cd $GOPATH/src/precommit
$ git init
Paste each of these commands into your terminal, to create and get into an empty git repository.
As you can read here, there are two general types of git hooks — Client and Server side. As our goal is to prevent ourselves from committing errors that Go tools can detect for us, we are only concerned about the client side hooks, more precisely
pre-commit. To set it up we need to create the
pre-commit file in the
hooks folder inside the mysterious
.git directory in the root of our repository. Use the following commands to create the
pre-commit file and give it right to execute.
$ touch .git/hooks/pre-commit
$ chmod +x .git/hooks/pre-commit
Now, let’s open it in our favorite editor. Although it has to be called
pre-commit without any extension, it actually can be any kind of executable script. In this article we will be writing a shell script, so we can start our script gently, by telling which interpreter to use:
Since we want to commit only already staged files, all we need to do is get to all the files with
.go extension and run the chosen Go tools against them. To find the targeted files I’ve found this simple(?) command:
STAGED_GO_FILES=$(git diff --cached --name-only | grep ".go$")
Let’s break it down.
git diffis a command used to show differences between commits.
--cachedis a synonym of
--stagedand it’s used to show staged files.
--name-onlyshows only the names of the files.
grep ”.go$”selects only file names that have
This command should give us the files that we want to check for correctness. Of course, there is a possibility that there are no such files and in that case the script should end.
STAGED_GO_FILES=$(git diff --cached --name-only | grep ".go$")if [[ "$STAGED_GO_FILES" = "" ]]; then
Alright, let’s test our script. We can do this by simply adding two
echo commands in and outside our
if statement. Also at the end of the script, you should add the
exit 1 command to make sure that we won’t commit anything in this test.
#!/bin/shSTAGED_GO_FILES=$(git diff --cached --name-only | grep ".go$")if [[ "$STAGED_GO_FILES" = "" ]]; then
echo "no go files were stashed"
fiecho "found some interesting files!"
Now, we can run
git commit in our console, and as an output, we should see “no go files were stashed”. Sounds about right. Let’s create and stash a simple Go file to test if our
if statement isn’t always true.
$ touch test.go
$ git add test.go
This time when we run
git commit we should see “found some interesting files!” in our console. If both times you got expected output — congratulations, you just created a
pre-commit git hook. You can remove the
echo calls now.
Finding the right tools
Let’s take a step back from writing a script and take a look at what we might want to check in our
pre-commit hook. You can find various tools that support the Go programming language. The most impressive ones, like godoc, are from official Go binaries. I’ve chosen only 3 of the available tools to include in my
pre-commit hook, but feel free to mix them up depending on your needs.
gofmt and goimports
One of Go benefits is that programmers don’t feel the fatigue of formatting their code. Go provides a tool called
gofmt which sets up the formatting rules for them. Example: there is no fuss about whether gophers should use tabs or spaces,
gofmt will turn your indentation into tabs anyway. You can read more on
goimports provides all the benefits that
gofmt is giving and in addition to that, it’s managing the imports of the file. I‘ve configured vscode to run
goimports after saving every Go file, and I thought that it might be a good idea to run it on every staged file before committing, in case updating it in a different editor. Click here for more information about
golint is a linter for Go programming language. It can tell us what styling mistakes we’ve made.
Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes.
A quick example why you should use
golint— if you create an exported type and don’t give it a documenting comment
gofmt won’t create a comment for you since it doesn’t know what this type is.
golint doesn’t know it either but it will print information about the missing documentation. You can find out more here.
govet is similar to
golint since it’s also printing mistakes we have made. Different type of mistakes though.
Golint differs from govet. Govet is concerned with correctness, whereas golint is concerned with coding style.
govet is examining Go source code to find possible bugs and suspicious constructs. To find out more about
govet and why it’s important, you should check its docs.
Implementing the solution
Let’s get back to our script, shall we?
We’ve tested that our hook is working so far. Now, all we need to do is iterate over the filenames that
git diff in combination with
grep gave us, but before that, we can create a boolean
pass flag that will later tell us if
govet or any other gotool has found mistakes in our code. Let’s remove the
exit 1 line from the end of our script and add the following.
PASS=truefor FILE in $STAGED_GO_FILES
# This is where we will be testing each of our files.doneif ! PASS; then
printf "COMMIT FAILED\n"
printf "COMMIT SUCCEEDED\n"
What’s left for us to do is to run tools that we want inside the
for loop. Let’s start with the easiest one.
goimports isn’t included in binary Go distributions we have to install it using the
go get command.
$ go get golang.org/x/tools/cmd/goimports
We use it by simply adding the following line inside the loop.
goimports -w $FILE
-w option tells
goimports to update the file, rather than print its formatted content to the standard output.
goimport — if you don’t have it, get it.
go get -u golang.org/x/lint/golint
The following block of code contains the usage of
golint inside our script.
golint "-set_exit_status" $FILE
if [[ $? == 1 ]]; then
Firstly, we are calling
golint to inspect our file. Providing the
set_exit_status flag makes it possible for us to examine the result of the
golint check. If the result was
1, which indicates that
golint found some problems, we are setting the
PASS variable to
false, which as you may remember will result in failing the whole commit.
govet comes along with Go binaries so there is no need for
go get command. We’ll use the
govet tool similarly to
go vet $FILE
if [[ $? != 0]]; then
As you might have noticed, we are not checking if running
go vet on the file returned
1, but if it didn’t return
0. Without going into details — there is a problem with the value that
govet is returning on compilation errors it finds. For more information on this topic read this.
We did it! 🎉 We now have a
pre-commit git hook for our awesome repository full of Go code! 🚀
There should be no output after running
git commit on clean stashed files, and on files containing errors the output could, for example, look like this:
Here is the complete code from this article.
You can also check out more colorful implementation here.