Question
How can I create and import packages that are local to a Go project without relying on GOPATH?
For example, I would like a project to contain multiple executables and reusable project-specific packages:
myproject/
├── binary1.go
├── binary2.go
├── package1.go
└── package2.go
I want code used by binary1.go or binary2.go to import functionality from package1, then build an executable without receiving an error that the package cannot be found in GOROOT or GOPATH. What is the correct Go project structure and import approach for larger projects that need multiple packages and files?
Short Answer
Go modules let a project define its own import root, so local packages do not need to live in GOPATH. You will learn why Go packages are directory-based, how to create a go.mod file, how to import project packages using a module path, and how to organize multiple executables cleanly.
Concept
In Go, a package is a directory, not an individual .go file. All ordinary Go source files in one directory must normally declare the same package name.
That means this structure cannot contain both an executable and a reusable package in the same directory:
myproject/
├── binary1.go # package main
└── package1.go # package package1
Go reports an error because a directory cannot mix package main and package package1.
The modern solution is Go modules. A module is a collection of packages identified by a module path in a go.mod file. Imports within that module use the module path plus the package directory.
For example, if go.mod says:
module example.com/alex/myproject
and a package is stored in internal/greeting, its import path is:
import "example.com/alex/myproject/internal/greeting"
Go finds that code relative to the module root. It does not need the package to be installed in GOPATH.
This matters because large Go applications are naturally split into focused packages: HTTP handlers, database access, business rules, configuration, and command-line programs can each live in appropriate directories.
Mental Model
Think of a Go module as a company campus.
- The
go.modfile is the campus address:example.com/alex/myproject. - Each directory is a department, such as
internal/greetingorinternal/config. - An import path is the full mailing address for a department.
- Files inside the same department share the same package name and work together.
A single .go file is not a department by itself. To create a reusable package, place its files in their own directory.
Syntax and Examples
Start a module from the project root:
go mod init example.com/alex/myproject
A practical layout with two executable commands and one reusable package is:
myproject/
├── go.mod
├── cmd/
│ ├── binary1/
│ │ └── main.go
│ └── binary2/
│ └── main.go
└── internal/
└── greeting/
└── greeting.go
internal/greeting/greeting.go:
package greeting
import "fmt"
func Hello(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
cmd/binary1/main.go:
package main
import (
"fmt"
"example.com/alex/myproject/internal/greeting"
)
func main() {
fmt.Println(greeting.Hello("Mina"))
}
Build the command from the module root:
go build ./cmd/binary1
The import path starts with the value in go.mod, followed by the directory containing the package. The package is referenced in code by its package name, greeting.
You can use a regular directory such as pkg/greeting instead of internal/greeting. The internal directory is useful for code that should be reusable only inside this module.
Step by Step Execution
Consider this code:
package main
import (
"fmt"
"example.com/alex/myproject/internal/greeting"
)
func main() {
message := greeting.Hello("Mina")
fmt.Println(message)
}
When you run:
go run ./cmd/binary1
Go performs these steps:
- It starts at the current directory and finds
go.mod. - It reads the module path, such as
example.com/alex/myproject. - It sees the import
example.com/alex/myproject/internal/greeting. - Because that import begins with the current module path, Go maps the remaining path to
internal/greetingin the project. - It compiles the
greetingpackage and itsHellofunction. - It compiles
cmd/binary1aspackage main. maincallsgreeting.Hello("Mina"), receiving"Hello, Mina!".fmt.Printlnprints the message.
Output:
Hello, Mina!
Real World Use Cases
Local packages are used whenever an application grows beyond a few files:
- Web APIs: put routing, request validation, and response handling in separate packages.
- Database code: isolate queries and repository logic from HTTP or command-line code.
- Business rules: keep calculations, permissions, and workflows in testable packages.
- Multiple commands: build an API server, migration tool, worker, and CLI from one module.
- Data processing: separate file parsing, transformation, and output formatting into small packages.
- Shared configuration: load environment variables in one package and use the result across commands.
For example, a service may use cmd/api for its server executable and internal/orders for order-related business logic.
Real Codebase Usage
Most Go codebases place each executable command in its own cmd/<name> directory:
cmd/
├── api/
│ └── main.go
└── worker/
└── main.go
This avoids having several main functions in one directory and makes each build target explicit:
go build ./cmd/api
go build ./cmd/worker
Application-only code is often stored under internal/:
internal/
├── config/
├── database/
├── service/
└── transport/
The internal rule is enforced by Go: packages outside the parent tree of internal cannot import them. This prevents another module from treating private implementation details as a public API.
Common project patterns include:
- Validation: a handler calls a validation package before doing work.
- Early returns: functions return errors immediately when input or configuration is invalid.
- Configuration: one package reads environment variables and returns typed settings.
- Error handling: packages return errors; the command package decides how to log them and exit.
- Tests: each package has focused
_test.gofiles beside its implementation files.
Common Mistakes
Treating a file as a package
This does not create a separate importable package:
myproject/
├── main.go
└── greeting.go
If main.go uses package main and greeting.go uses package greeting, the directory contains mixed packages and will fail to build.
Avoid it: move greeting.go into a subdirectory such as internal/greeting/greeting.go.
Importing only a local folder name
This usually fails in module mode:
import "greeting"
Go expects an import path, not merely the package name.
Avoid it: import the module path plus directory:
import "example.com/alex/myproject/internal/greeting"
Using relative imports
This is not supported in module-based Go code:
import "./greeting"
Avoid it: use a module import path.
Forgetting go.mod
Without a module definition, Go cannot reliably determine your project's import root.
Avoid it: run this once in the project root:
go mod init example.com/alex/myproject
Building a file when you mean to build a package
A command such as this builds only the named file:
go build binary1.go
It can be useful for small experiments, but it is less reliable for a multi-file command and does not match the usual multi-command layout.
Avoid it: build a package directory instead:
go build ./cmd/binary1
Importing an unexported identifier
Only names beginning with an uppercase letter are available to importing packages:
func hello() string { // not accessible outside this package
return "hello"
}
Avoid it: export it when it is part of the package API:
func Hello() string {
return "hello"
}
Comparisons
| Approach | How it works | When to use it |
|---|---|---|
| Go modules | Uses go.mod and module-based import paths | Standard approach for modern Go projects |
GOPATH workspace | Uses a special directory layout under GOPATH/src | Primarily legacy projects |
internal/ package | A module-local package protected from outside importers | Private application implementation |
| Regular package directory | Can be imported by other modules if its module is available | Intended public or reusable APIs |
| One package, many files | Multiple .go files in one directory use the same package declaration | Splitting one cohesive package into files |
| Multiple packages | Each package lives in a different directory | Separating responsibilities and APIs |
A package name and an import path are related but different. For example:
import "example.com/alex/myproject/internal/greeting"
The path identifies the location. The code normally refers to the declared package name:
greeting.Hello("Mina")
Cheat Sheet
# Create a module in the project root
go mod init example.com/you/project
# Run or build a command package
go run ./cmd/app
go build ./cmd/app
# Format all Go files
gofmt -w .
# Test every package in the module
go test ./...
project/
├── go.mod
├── cmd/
│ └── app/
│ └── main.go # package main
└── internal/
└── feature/
└── feature.go # package feature
// Import path = module path + package directory
import "example.com/you/project/internal/feature"
// Exported identifiers begin with uppercase letters.
feature.DoWork()
Rules to remember:
- A Go package is a directory.
- Normal
.gofiles in one directory use one package name. - Every executable has
package mainand amain()function. - Do not use relative imports such as
./featurein module projects. - Use
internal/for packages that must not be imported by unrelated modules. - Prefer
go build ./path/to/packageover naming an individual source file for application builds.
FAQ
Do I need GOPATH to import local packages in Go?
No. Go modules are the standard approach. Create go.mod in the project root and use module-based import paths.
Can I import a Go package by only its folder name?
Usually no. Use the full module import path, such as example.com/you/project/internal/config.
Can different Go packages be in the same directory?
No, except that external test files may use a separate <package>_test package. Ordinary source files in a directory must use the same package name.
Why does Go use a URL-like module path for local code?
The path is a unique identifier. It can use a future repository location, such as github.com/you/project, but it does not need to be publicly hosted for local development.
What is the difference between cmd and internal?
cmd contains executable programs, usually one directory per command. internal contains implementation packages that are restricted to the surrounding project tree.
Can I have two executables in one Go module?
Yes. Put each executable in its own directory, for example cmd/server and cmd/worker.
Should I run go build binary1.go?
It works for simple single-file cases, but building a package directory such as go build ./cmd/binary1 is the conventional and scalable choice.
Does the package name have to match the directory name?
No, but matching them is strongly recommended because it makes imports easier to understand.
Mini Project
Description
Create a small Go module with two commands that share one project-local greeting package. This demonstrates the directory-based package rule, module import paths, and the conventional cmd and internal project layout.
Goal
Build and run two independent executables that import the same local package without using GOPATH.
Requirements
Create a Go module with a go.mod file at the project root.
Create a reusable greeting package in an internal/greeting directory.
Create separate hello and goodbye executable commands under cmd.
Make both commands import and use the local greeting package.
Run each command from the module root.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.