Skip to content

Instantly share code, notes, and snippets.

@candlerb
Last active October 1, 2024 08:33
Show Gist options
  • Save candlerb/3cb11576b2d73800b58f3b548dc2ba4a to your computer and use it in GitHub Desktop.
Save candlerb/3cb11576b2d73800b58f3b548dc2ba4a to your computer and use it in GitHub Desktop.
Suggestions for go project layout

If someone asked me the question "what layout should I use for my Go code repository?", I'd start by asking back "what are you building: an executable, or a library?"

Single executable

Stage 1: single source file

Create a directory named however you want your final executable to be called (e.g. "mycommand"), change into that directory, and create the following files:

go.mod      # module github.com/me/mycommand  (create using "go mod init github.com/me/mycommand")
main.go     # package main

The source file does not need to be called "main.go": call it whatever you like.

Usage:

  • If your code imports any third-party libraries, type go mod tidy to fetch them
  • Run locally with go run .
  • Format source with go fmt .
  • Verify source with go vet .
  • Build locally with go build . (creates executable "mycommand" in top-level directory)
  • To add tests, create main_test.go and run with go test .
    • Normally this would be package main too
    • You can use package main_test instead, but then it has to explicitly import the main package, and can only access public exported names (starting with a capital letter)
  • Turn this into a git repository and publish
    git init
    git add .
    git commit -m "first commit"
    git branch -M main
    git remote add origin [email protected]:me/mycommand.git
    git push -u origin main
    
  • End-user can install it with go install github.com/me/mycommand@latest and will get a binary called "mycommand"

Stage 2: multiple source files, single package

Simply add additional files into top level directory, all with "package main"

go.mod        # module github.com/me/mycommand
main.go       # package main
other.go      # package main
stuff.go      # package main
some_test.go  # package main

Usage as before.

Stage 3: multiple source files, multiple packages

This is when you want to break your application into semi-independent pieces, perhaps with a view to making them their own public library at some point in the future.

go.mod      # module github.com/me/mycommand
main.go     # package main; import "github.com/me/mycommand/internal/foo"
...
internal/foo/xxx.go       # package foo
internal/foo/yyy.go       # package foo
internal/foo/test_xxx.go  # package foo  // OR:
internal/foo/test_yyy.go  # package foo_test; import "github.com/me/mycommand/internal/foo"

Recommendations:

  • Use the "internal" subtree to prevent your subpackages being importable by anyone else
  • Name the innermost subdirectory the same as the package to avoid confusion
    • That is, preferably "github.com/me/mycommand/internal/foo" provides a package called "foo"
    • You can, however, change the package name on import: import bar "github.com/me/mycommand/internal/foo"

Usage:

  • go test ./... to run tests in all subdirectories
  • go fmt ./... to reformat code in all subdirectories
  • go run . and go build . still work as before

Playground example

Multiple executables in same repo

Stage 1: standalone commands

Create separate subdirectories for each executable, where the directory name matches the desired executable name. There is an optional convention that these live under a "cmd" directory.

go.mod           # module github.com/me/mytools
cmd/foo/main.go  # package main
cmd/foo/more.go  # package main
cmd/bar/main.go  # package main
cmd/bar/stuff.go # package main

Usage:

  • Run locally with go run ./cmd/foo, go run ./cmd/bar (or: cd cmd/foo; go run .)
  • Build locally with mkdir bin; go build -o bin/ ./... (creates executables "foo" and "bar" in "bin" directory"
  • go test ./... to run all tests
  • go fmt ./... to reformat all code
  • End-user installs with go install github.com/me/mytools/cmd/...@latest (or replace "..." with "foo" or "bar" to get a single executable)

Stage 2: shared code

go.mod           # module github.com/me/mytools
cmd/foo/main.go  # package main; import "github.com/me/mytools/internal/baz"
cmd/bar/main.go  # package main; import "github.com/me/mytools/internal/baz"
...
internal/baz/xxx.go  # package baz
internal/baz/yyy.go  # package baz

Usage as above.

Library

Single package

Create a directory named however you want your final library to be called (e.g. "mylib"), change into that directory, and create some files:

go.mod        # module github.com/me/mylib  (create using "go mod init github.com/me/mylib")
some.go       # package mylib
files.go      # package mylib
some_test.go  # package mylib or package mylib_test

Usage:

  • End users will import "github.com/me/mylib"
  • go test . to run your tests

Note: it's usual, but not always the case, that the last component of the module path (which in this cases is the repo name) matches the name of the package. If not, the user may be surprised that the imported package name doesn't match import path. However, they can always override it: import foo "github.com/me/mylib"

Internal packages

These should go under internal; they can be imported by the library itself, but no external direct imports.

Multiple visible packages in same module

go.mod           # module github.com/me/mylib  (create using "go mod init github.com/me/mylib")
foo/whatever.go  # package foo
bar/whatever.go  # package bar

Usage:

  • End users will import "github.com/me/mylib/foo", import "github.com/me/mylib/bar"
  • go test ./... to run all your tests

Again, it's suggested that each package name match the innermost directory name (last part of path)

These libraries can import each other, using their full module import path just like an end-user.

You can have a top-level package as well, i.e. import "github.com/me/mylib".

TODO: VERSIONING

Both executable and library

You can have a single repo which contains one or more executables ("package main") and is a library providing one or more importable packages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment