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.

man reading books looking in manpages
for the right flags of grep


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?

/go/ /search/ /speed/ /command-line/