go project structure
the package clause and import declarations in go are a bit mysterious; while it is very easy to compile and run one file, it is not obvious how to properly organize a go project with multiple files for someone with no experience; the go documentation does not have a dedicated section explaining the facts straightforward; here is a brief description of the concepts involved with some working examples; well, the description may not be accurate, but the examples should work; the examples assume we are building/running an executable;
one file
the simplest case is when we put everything in one file; this file must have
main
as both package name and entry function name;
// main.go
package main
import "fmt"
func main() {
fmt.Println("hello")
}
to build the executable, run go build
on this file:
go build main.go
multiple files in one package
Go programs are organized into packages. A package is a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package.
this basically says a package can span multiple files in the same directory; in fact go does not allow multiple packages in the same directory, so a package is kind of one-to-one mapping to a directory;
now we can add another file foo.go
to the same package in the same directory
as main.go
; because they are in the same package, we can define something in
foo.go
and use it in main.go
; in this example, we define a function get
in
foo.go
and use it in main.go
; the name get
starts with a lowercase letter;
there is no need to export it because we use it in the same package (albeit
different file);
// foo.go
package main
func get() int {
return 1234
}
// main.go
package main
import "fmt"
func main() {
fmt.Println("hello", get())
}
to build executable main
, run go build
on all these files and put main.go
at first (otherwise the executable would be foo
);
go build main.go foo.go
multiple packages in one module
A repository contains one or more modules. A module is a collection of related Go packages that are released together. A Go repository typically contains only one module, located at the root of the repository. A file named
go.mod
there declares the module path: the import path prefix for all packages within the module. The module contains the packages in the directory containing itsgo.mod
file as well as subdirectories of that directory, up to the next subdirectory containing anothergo.mod
file (if any).…
An import path is a string used to import a package. A package’s import path is its module path joined with its subdirectory within the module. For example, the module
github.com/google/go-cmp
contains a package in the directorycmp/
. That package’s import path isgithub.com/google/go-cmp/cmp
. Packages in the standard library do not have a module path prefix.
The
PackageName
is used in qualified identifiers to access exported identifiers of the package within the importing source file. It is declared in the file block. If thePackageName
is omitted, it defaults to the identifier specified in the package clause of the imported package. If an explicit period (.) appears instead of a name, all the package’s exported identifiers declared in that package’s package block will be declared in the importing source file’s file block and must be accessed without a qualifier.The interpretation of the
ImportPath
is implementation-dependent but it is typically a substring of the full file name of the compiled package and may be relative to a repository of installed packages.
simply speaking, a go.mod
file in the top-level directory makes a module that
contains all packages in its directory tree; the module path is a prefix of the
import path; the import path says what to import (in an implementation-dependent
way);
here we init the module with an arbitrary module path a/b/c
;
go mod init a/b/c
the directory structure looks like this:
.
├── go.mod
├── lib
│ └── foo.go
└── main.go
the files are almost the same as before; we changed get
to Get
to export it
because now we are using it in a different package:
// go.mod
module a/b/c
go 1.18
// lib/foo.go
package lib
func Get() int {
return 1234
}
// main.go
package main
import "fmt"
import "a/b/c/lib"
func main() {
fmt.Println("hello", lib.Get())
}
to build this module, run go build
in top-level directory:
go build .
it is worth noting that the package name does not have to be its directory name
or one of its file names; the package name is merely like a namespace to qualify
identifiers exported by the imported package; for example, we can change package
name from lib
to libx
and things still work:
// lib/foo.go
package libx
func Get() int {
return 1234
}
// main.go
package main
import "fmt"
import "a/b/c/lib"
func main() {
fmt.Println("hello", libx.Get())
}
we are not changing import path a/b/c/lib
because its relative part lib
must
match actual filesystem path; this should have made things clear: we import the
relative path (lib
) of a package prefixed by the module path (a/b/c
), then
refer to the imported package (in the importing package) using its package name
(lib
, libx
, etc.); the import statement can also specify an explicit package
name which overrides the default package name given in package clause of the
imported package;