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.
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.
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.
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.
}
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.
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.
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.
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.
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? :-)