Some Simple Suggestions for Structuring Go Code
Language
- unknown
by James Smith (Golang Project Structure Admin)
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.
Table of Contents
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.
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.