Golang Project Structure

Tips and tricks for writing and structuring Go code

Some Simple Suggestions for Structuring Go Code

Language

  • unknown

by

You don’t always have to think long and hard about structure: the easiest way to begin when writing Go code is just to create a main.go file and run it. In the past, this file would have had to be located in a package within the $GOPATH/src directory. However, since version 1.11 of the Go programming language, it is now possible to create modules that will run and import other local and external modules anywhere on your file system.

First Steps

Because of this update to the language specification, we can can create the following simple project wherever we choose:

main.go

package main

import "github.com/username/example/action"

func main() {
    action.Greet("Visitor")
}

action/greet.go

package action

import "fmt"

func Greet(name string) {
    fmt.Printf("Welcome, %s.\n", name)
}

Initializing the Module

We just need to run the go mod command with our chosen package name, like so, in order that the compiler can work out where to find our imports:

> go mod init github.com/username/example

This gives us the following directory structure:

> tree

.
├── action
│   └── greet.go
├── go.mod
└── main.go

1 directory, 3 files

Our go.mod file will contain the necessary information about our package and current version of Go:

module github.com/username/example

go 1.15

If we had been relying on external packages, it would also contain information about the version numbers, so that we always import exactly the same code, rather than older or newer versions that may not work in the same way.

We can now make the action package internal, because, while it’s useful for our functionality, we don’t want other people to be able to import it into their projects. All we need to do is create a directory called internal and place the action directory inside, and the package will automatically become private, so that it’s only available to its parent packages.

> tree

.
├── go.mod
├── internal
│   └── action
│       └── print.go
└── main.go

2 directories, 3 files

Build Files

It may also be helpful to separate our compiled executable from the source code. We can do this by creating a build directory and explicitly telling the go build command where we want the executable to end up, like so:

> go build -o build/example
> tree

.
├── build
│   └── example
├── go.mod
├── internal
│   └── action
│       └── print.go
└── main.go

3 directories, 4 files

We can now create a Makefile, so that we can compile our program without having to type out the build directory every time.

Makefile

PACKAGE_NAME=example

BUILD_DIR=build/
BUILD_FILE=$(BUILD_DIR)$(PACKAGE_NAME)

make: compile run

compile:
	go build -o $(BUILD_FILE)

clean:
	rm -f $(BUILD_FILE)

run:
	$(BUILD_FILE)

By default, running make will compile and run our program, but there is also a rule to remove the built executable, if we have no use for it any more.

> make
go build -o build/example
build/example
Welcome, Visitor.

Including Assets

We may also want to read files from the disk in order, for example, to retrieve configuration information. In this case, we’d want to create an assets directory.

> mkdir assets

Now we can store the name of the person we want to greet in a text file, like so:

assets/name.txt

John Smith

name.go

package main

import (
	"bufio"
	"os"
	"strings"
)

func name() string {
	file, err := os.Open("assets/name.txt")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	if scanner.Scan() {
		if text := strings.TrimSpace(scanner.Text()); text != "" {
			return text
		}
	}

	return "Visitor"
}

main.go

package main

import (
	"github.com/username/example/internal/action"
)

func main() {
	action.Greet(name())
}

The name function now opens the file that is stored in the assets directory, reads the first line and returns it. If the line is empty, the function simply returns a default value, "Visitor". If the file cannot be opened, perhaps because there’s an issue with the disk drive, then the function will panic, but, of course, you could choose to handle the error differently.

Temporary Directory

Sometimes a program needs to make use of temporary files that are not expected to stay around permanently. One can make use of the operating system’s default temporary directory, but you may also want to keep everything within the project directory. In that case, you can create a tmp directory.

> mkdir tmp

We can now make use of this directory as a cache. For example, if we create a new file called in the main package, we can use the name function to calculate the user’s intials, like so:

initials.go

package main

import "strings"

func initials() string {
	nameWords := strings.Split(name(), " ")
	var builder strings.Builder

	for _, word := range nameWords {
		if len(word) > 0 {
			builder.WriteByte(word[0])
		}
	}

	return builder.String()
}

We may not want to have to recalculate that every time the program runs, however, so we can store the result in our temporary directory. This is just a trivial example, of course, but in reality whatever you’re caching may take much longer to generate, making the time saving more valuable.

Just imagine a complex algorithm that pulls hundreds of millions of records from a database, taking minutes to transform them into another format: you wouldn’t want to make your computer do that more than is absolutely necessary.

initials.go

package main

import (
	"bufio"
	"os"
	"strings"
)

const (
	initialsFilePath = "tmp/initials.txt"
)

func initialsLoad() string {
	if _, err := os.Stat(initialsFilePath); err != nil {
		if os.IsNotExist(err) {
			return ""
		} else {
			panic(err)
		}
	}

	file, err := os.Open(initialsFilePath)
	if err != nil {
		panic(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	if scanner.Scan() {
		if text := strings.TrimSpace(scanner.Text()); text != "" {
			return text
		}
	}

	return ""
}

func initialsSave(value string) {
	file, err := os.Create(initialsFilePath)
	if err != nil {
		panic(err)
	}
	defer file.Close()

	if _, err := file.WriteString(value); err != nil {
		panic(err)
	}
}

func initials() string {
	if value := initialsLoad(); value != "" {
		return value
	}

	nameWords := strings.Split(name(), " ")
	var builder strings.Builder

	for _, word := range nameWords {
		if len(word) > 0 {
			builder.WriteByte(word[0])
		}
	}

	value := builder.String()

	initialsSave(value)

	return value
}

main.go

package main

import (
	"github.com/theTardigrade/example/internal/action"
)

func main() {
	action.Greet(initials())
}

Final Thoughts About Structure

This article has gone over some of the directories you may want to create in order to organize a simple project, but, of course, you’re likely to need a more complicated directory structure as your program evolves and becomes more advanced, requiring more lines of code. Whenever you add a new package or function to your codebase, it’s always important to think about how it links in with the rest of your code and let that relationship dictate the directory structure.

The wall of a structure made out of bricks and mortar.
Sometimes architects design brick walls. Sometimes they design software.

For example, a web server may have a directory for the route-handling packages, another for the data-model packages and another for the HTML templates. On the other hand, a simple library or microservice might have no subdirectories at all. Once you have a clear idea in your head about the architecture of your code, then you can begin to structure it properly.

Leave a Reply

Your email address will not be published.