gofind - First steps with Go
Aug 25, 2019 • 1311 words • 7 min read
It's been a while that I wanted to learn Go. Also, I've always been fascinated with searching speed from the day I saw Apple's Spotlight on OS X while I was still stuck with the incremental search of Windows XP, and later with the command line search tools.
It all started with ack
, which is currently at its 3rd
iteration which was a much better and easier-to-use
grep. Suddenly you had line numbers, match separation by file and match highlight enabled
by default without having to search for the right incantation.
Then I found the-silver-searcher which was insanely faster than anything that I had ever worked with before which makes it one of the first things that I install on a new system and something I use almost daily.
Finally, a few weeks ago I ran into ripgrep which claims to be even faster so I just had to try it out. Plus it's written in Rust, which I'm always curious to see what it can do.
Using these as inspiration and given that I kinda missed writing in a statically typed language, I decided to give it a go and have my first go at Go (puns 100% intended 😆)
Table of contents
gofind
So I ended up with something that works decently enough for the amount of time I spent on it and you can find the code here: https://gitlab.com/sakis/gofind.
Obviously the performance on large directories is nowhere near as good as the tools I
mentioned earlier, but the point of this exercise was to learn Go, not to replace ag
and
rg
.
Impressions
I had tried learning Go a couple of times in the past by going through the Tour of Go, but I never found it enticing enough and I gave up at some point halfway through.
This time I just started typing.
I looked up things as I went along, trying to figure out how the language works and spending time debugging my mistakes.
Here are my first impressions:
Type declarations are written backwards
The main thing that annoyed me with Go, was that everything that has to do with type declaration is written backwards compared to the older languages.
E.g.
Adding two numbers in C:
int addNumbers(int x, int y) {
int result;
result = x + y;
return result;
}
Adding two numbers in Go:
func addNumbers(x, y int) int {
var result int
result = x + y
return result
}
They have a lengthy explanation about this, but it still takes some getting used to.
Concurrency is easy
This is one of the main selling points of Go and when I got to use it I was impressed by
its simplicity. The first step, to just run a function in a background thread
(goroutine)
is as simple as prepending your function call with go
before the name of the function.
The second step was to make it so that the program waits for all the goroutines to finish
before exiting. This was also surprisingly simple to do by using the
sync.WaitGroup
methods to capture how many
goroutines are still executing and waiting for them to finish.
There's definitely more to concurrency than just getting methods to run in the background and waiting for them to finish, and things definitely get more complicated when you want to synchronize or communicate between your goroutines, but this was a very welcome surprise.
There are no classes
At least not in the traditional sense, i.e. Go doesn't have a keyword "class".
As with the type declarations, I thought I would be annoyed by this too, but if you shift your perception it's just naming conventions, i.e. a class is essentially a collection of attributes and methods that (usually) operate on these attributes.
In Go, you group your attributes in structs and you can have methods that can only be called by an instance of a certain struct. And these methods have access to that instance's attributes.
- But wait! With classes you have inheritance and polymorphism and other words!
Well, true and as far as I can tell Go's solution to these issues is interfaces, but I
haven't looked into them yet so...
¯\_(ツ)_/¯
It has useful error messages
When you write in a compiled language you can depend on the compiler to protect you from certain things and Go is no exception.
What I particularly enjoyed though is that in some cases it goes the extra mile to be actually helpful instead of just erroring out, for example here I tried to reference a field that doesn't exist in my struct:
$ go build
# _/home/sakis/projects/gofind
./aFile.go:47:7: file.isbinary undefined (type *aFile has no field or method isbinary, but does have isBinary)
It's fast
I mean, yeah, obviously a compiled language is faster than your typical scripted language, but I did not expect that much of a difference.
I started to write the same code in python just to make this point, but I didn't need to finish. Just 29 lines of straightforward code that just print the contents of a directory:
#!/usr/bin/env python3
import os
import sys
def main():
needle = ""
path = "."
if len(sys.argv) == 1:
print("Usage: {} <search_term> [path]".format(sys.argv[0]))
return 0
if len(sys.argv) >= 2:
needle = sys.argv[1]
if len(sys.argv) >= 3:
path = sys.argv[3]
path = os.path.abspath(path)
items = os.listdir(path)
print(items)
if __name__ == "__main__":
sys.exit(main())
The quickest it did against its own repo is this:
$ time python3 main.py main
['.main.py.swp', '.git', 'main.py']
real 0m0.038s
user 0m0.030s
sys 0m0.008s
Compare this against running a search with gofind again against its own repo:
$ time ./gofind gofind
/home/sakis/projects/gofind/.gitignore
gofind
/home/sakis/projects/gofind/Makefile
@./gofind
/home/sakis/projects/gofind/README.md
# gofind
Install go and then either `make` or `go build; ./gofind <word>`
real 0m0.008s
user 0m0.005s
sys 0m0.009s
0.038s vs 0.008s !!!
It took Python 38ms to list the contents of a directory with 3 items, whereas Go needed 8ms to find the relevant files, decide whether or not they are searchable, spawn multiple goroutines that each would load a file and look for a match and finally print the results.
I don't know about you, but I did not expect such a difference.
And if you think 30ms is not a noticeable difference, you are wrong. It might not make a lot of difference, but it is very much noticeable.
go fmt
A last thing I want to mention is the go fmt
command, which takes your code and reformats
it according to certain rules so that all your code has the same style.
You finish your sloppy typing, you run go fmt
and you have nice, consistently formatted
code and, more importantly, everyone else's code is also formatted the same way!
Final thoughts
Bottom line is that I find myself really interested in the language. I thought that having the type declarations written backwards would be constantly annoying me, but it's surprisingly easy to get used to it - especially if you haven't used any strongly typed languages in the last years.
As with every new thing, in the beginning you are so excited about it and everything is nice and peachy. Will it remain like this? Will it replace my love for python? Remains to be seen.
How about you? Have you tried Go? What's your favourite/most annoying thing about it?