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 its go.mod file as well as subdirectories of that directory, up to the next subdirectory containing another go.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 directory cmp/. That package’s import path is github.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 the PackageName 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;

references