Inspired by a recent conversation about an ex-colleague wanting to learn go, I cast my mind back to the traps and pitfalls that I experienced when I was first picking it up. My background was mostly in dynamic languages (perl, php, python).

I’m assuming you have some existing development knowledge here.

Don’t worry too much about packages and code structure

It’ll probably go against everything you have been taught or learned about other languages, but there is nothing wrong with just smashing everything into a single file to begin with.

It’s obviously not the best thing to do, forever! But refactoring Go code is much, much easier then in other languages. You can defer refactoring things into different packages to a later date, and Go’s package structure and tooling around it will make this a very easy task.

How do packages work?

Speaking of packages, this is something that tripped me up initially, so maybe it’s worth delving into a little.

They way Go splits files into packages is very simple. Essentially, all the .go files in a particular directory belong to a single package - and they should all have a matching package declaration at the top.

So, in your source code you might have:

main.go
foo/foo.go

File foo/foo.go should have a declaration at the top:

package foo

You can also have:

main.go
foo/thing.go
foo/other.go

Both thing.go and other.go are in package foo and both should have the package foo declaration at the top.

But, if you had:

main.go
big/long/subdir/foo/thing.go
big/long/subdir/foo/other.go

Nothing changes as far as thing.go and other.go .. go. They are still in package foo.

The only thing that changes is how they are imported by users of that package. The package import path, its location on the disk is only important when specifying the import. It does not affect how that code is used in any other way, it does not affect variable names, function references and so on.

By the way, there is no difference as to the accessibility of types, variables and functions depending on whether they are in thing.go or other.go. The compiler effectively mashes them all together when building. Organising your code into different files is purely to make your life easier and more organised.

So, to import your foo package in each case, looking at main.go (assuming your module is called somemodule):

package main

import "somemodule/foo"

In the second case:

package main

import "somemodule/big/long/subdir/foo"

In both cases, you would use it like:

result := foo.SomeFunction()

SomeFunction() might be defined in thing.go or other.go - it makes no difference.

Referencing things in the current package

This threw me for a little while. Probably easiest to explain with code:

foo/foo.go:

package foo

func SomePublicFunc() { ... }

bar/bar.go:

package bar

import "foo"

func SomeOtherFunc() { ... }

func testFunc() {
  // this is how I refer to the package 'foo' function:
  foo.SomePublicFunc()

  // but this is how I refer to the package 'bar' function (I am 'bar):
  SomeOtherFunc()

  // note that bar.SomeOtherFunc() DOES NOT WORK, even though this is package bar.
}

go mod

Remember I said “assuming your module is called somemodule” above?

I’m going to give you the cheat sheet version here. In basic terms, treat every single go project as a “module”. You might never actually release it, it might be a standalone “thing” and not a library. But it’s all treated the same way.

When you start your new project, in an empty directory, type:

go mod init modulename

The ‘modulename’ is arbitrary. If you aren’t going to release your code to anyone, it can be nonsense. It just has to be consistent nonsense :-). Remember the import statements above? Everything inside this codebase that uses another package inside the same codebase must prefix it with whatever modulename you choose.

After you type that command, you’ll see a new file be created:

$ cat go.mod
module fff

go 1.20

It shows the module name, and the go version.

Module names can also look like github.com/tardisx/somemodule. The module name can also double as the URL to fetch it from, if it is a publicly accessible package. If you are going to publish your module, it will be need to correctly match that URL. But again, refactoring is easy - it would simply be a search and replace, so don’t worry about it too much yet.

When you add dependencies to your code, they will appear here, as well as in a new file called go.sum.

The cheat-sheet version here is that if you want to add the module github.com/gin-gonic/gin to your code, add it to an import statement:

import (
  ...
  "github.com/gin-gonic/gin"
)

And then run go mod tidy.

By the way, both go.mod and go.sum should absolutely be committed to your source control repo.

Interfaces

I initially brushed interfaces off as “too hard” and “will learn one day”. It’s certainly nothing to be ashamed of if they do not “click” at first. Depending on the language you have come from, it may be a fairly foreign concept.

I’m not going to teach you how to use interfaces here, it’s several topics all by itself. However, do take the time to learn at least the basic mechanics of how they work.

The go tour topics on interfaces are not (in my opinion) terribly useful at enlightening one on why they exist or what you can do with them.

After you understand the mechanics, it will take time to really appreciate how you can use them.

If you are just starting out with Go, you might want to skip the rest of this section for now - and that’s ok!

I will provide a small example here, a use that really helped clarify interfaces for me, personally.

Let us say there is an imaginary package called zbay, which provides a way for users to search for products, buy, sell and perform other operations on a little-known auction site.

Instantiating the struct for that might look like:

import "github.com/zbay/zbay"

...

z := zbay.NewZbay()

Now you can do things like:

zbay.ListProducts(searchOptions)
zbay.BidOnItem(id, 3.59)
zbay.SellItem(sellOptions)

You’re writing an automated program to automatically sell items on zbay. So you write a function like this:

func SellItemOnZbay(zbay zbay.Zbay, myitem Item) {
  // some code here to prepare the item data and gather it in a format
  // needed by zbay.SellItem
  res, err := zbay.SellItem(data)
  // code here to maybe store the response in a database, handle errors 
  // and so on
}

But you’re a conscientious developer, and you know it’s bad that you haven’t got any tests for SellItemOnZbay. You open up myprog_sell_test.go but you realise quickly you have a problem. SellItemOnZbay needs a zbay.Zbay struct, but if we create one, our test would be hitting the real zbay, and we can’t have our test do that.

Apart from the problem of hitting a real webservice from a test (which is significant), you are making your test dependent on an external service. Even if zbay API offers a “staging” or “test” environment, this is not great for your tests to depend on that being available.

The key point here is that your function SellItemOnZbay doesn’t need an entire zbay.Zbay struct. It doesn’t need to ListProducts or BidOnItem or any of that other functionality. It only needs to be able to SellItem. So, we introduce an interface:

type ZbaySeller interface {
  SellItem(zbay.SellItemOptions) (zbay.Response, error)
}

func SellItemOnZbay(zbay ZbaySeller, myitem Item) error {
  // some code here to prepare the item data and gather it in a format
  // needed by zbay.SellItem
  res, err := zbay.SellItem(data)
  // code here to maybe store the response in a database, handle errors 
  // and so on
}

Now, in our test we can define a mock type that satisfies the ZbaySeller interface, and do whatever we want with it - in this case, simulate an error case:

type mockZbay struct {}

func (z mockZbay) SellItem(zbay.SellItemOptions) (zbay.Response, error) {
  return zbay.Response{}, errors.New("failed to sell item")
}

func TestSellFailedHandling(t *testing.T) {
  mock := mockZbay{}
  err := SellItemOnZbay(mock, item)
  // test that we dealt with the mock error correctly inside SellItemOnZbay
}

Nice - we no longer need a “real live” zBay struct, and we can simulate any problem that might occur in production with zbay.SellItem and ensure we handle it correctly.

There are several modules out there which use codegen to automatically handle creating mocks like this, but they are not necessary and will add complexity to your build chain.

Generics

Avoid. If you think you need them, you’re probably wrong. There is a time and a place for them, but not when you are still learning.

I’ve just spent significant time and effort removing generics from a work codebase, and the result was cleaner, more reliable and easier to reason about.

Embrace the standard library

Most other languages will have you reaching for a third party dependency very quickly, some literally before you can do anything useful at all (ahem, cough javascript cough).

Resist this temptation, at least at first.

It’s not a problem, per se, but it is good to become familiar with the standard library before you start reaching for something in the outside ecosystem. There’s a few good reasons.

There’s a very good chance that whatever functionality you need to implement is already covered in the standard library.

As with many things, the statement “Don’t Repeat Yourself” has become to many a hard-and-fast rule, with all nuance lost. Don’t add a dependency to avoid writing 12 lines of code.

I’m not saying there is no case for abstraction, or to copy and paste everything, but be measured in your approach to introducing dependencies.

Embrace error handling

err := doThing()
if err != nil {
  return fmt.Errorf("could not doThing: %w", err)
}

It’s a common point of contention. Newcomers to Go tend to hate this idiom. I understand the discomfort.

Many of you have probably come from a language with exceptions, in some form.

In my opinion, using error handling like this is the way to write robust software. Conversely, exceptions are an easier way to write code, deferring the handling of errors until they occur, usually at 3am on a Sunday morning.

Put more simply, by putting the onus on you, the programmer, to consider every potential error that might happen while writing the code, you will write more resilient code in the first place. Do you want to consider what to do if your program can’t open a database connection now, or do you want to do that at 3am? :-)


Tags: golang  learn  interface  package  testing